diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index 5c98c2a..6de9f65 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -53,9 +53,6 @@ jobs: run: | echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore/trmnl-app-release.keystore - - name: Validate USE_FAKE_API configuration - run: ./gradlew validateUseFakeApiConfig - - name: Build Release APK and AAB run: ./gradlew assembleRelease bundleRelease env: diff --git a/CLAUDE.md b/CLAUDE.md index 4ed6dc2..2588ddf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -172,13 +172,10 @@ The project uses optimized Gradle settings based on best practices from the [Now **Note:** This project uses only build TYPES (debug/release), NOT product flavors. **Debug:** -- `buildConfigField("Boolean", "USE_FAKE_API", "true")` - Uses debug keystore with password "android" (included in repo) -- Fake API can be overridden in `RepositoryConfigProvider` - HttpLoggingInterceptor enabled with BODY level **Release:** -- `buildConfigField("Boolean", "USE_FAKE_API", "false")` - Requires production keystore from CI secrets or `secret.properties` - Keystore env vars: `KEYSTORE_PASSWORD`, `KEY_ALIAS` - Code shrinking: `isMinifyEnabled = true`, `isShrinkResources = true` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f45dbc5..3bff3e0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -75,11 +75,6 @@ android { buildTypes { release { - // Always force `USE_FAKE_API` to `false` for release builds - // See https://github.com/usetrmnl/trmnl-android/issues/16 - // â„šī¸ To override during local development, change the value in `RepositoryConfigProvider` - buildConfigField("Boolean", "USE_FAKE_API", "false") - // Enables code shrinking, obfuscation, and optimization // See https://github.com/usetrmnl/trmnl-android/issues/199 isMinifyEnabled = true @@ -99,13 +94,6 @@ android { } debug { - // Allow developers to configure this value for debug builds - // Use fake API response for local development and testing purposes. - // â„šī¸ To override during local development, change this value to `"false"` - // or, you can change the value in the `RepositoryConfigProvider` - // Ideally, this value should always be set to `true` for debug builds. - buildConfigField("Boolean", "USE_FAKE_API", "true") - signingConfig = signingConfigs.getByName("debug") } } @@ -221,83 +209,3 @@ ksp { tasks.withType { jvmArgs("-XX:+EnableDynamicAgentLoading") } - -/** - * Validation task to ensure USE_FAKE_API is correctly configured for release builds. - * This task will fail the build if: - * 1. Release build type has USE_FAKE_API set to anything other than "false" - * 2. RepositoryConfigProvider doesn't return `BuildConfig.USE_FAKE_API` - * - * Run manually: ./gradlew validateUseFakeApiConfig - * Automatically runs before: assembleRelease, bundleRelease - */ -val validateUseFakeApiConfig = tasks.register("validateUseFakeApiConfig") { - group = "verification" - description = "Validates that USE_FAKE_API is correctly configured for release builds" - - // Capture file paths at configuration time to avoid configuration cache issues - val buildGradleFile = project.file("build.gradle.kts") - val repositoryConfigFile = project.file("src/main/java/ink/trmnl/android/data/RepositoryConfigProvider.kt") - - doLast { - println("🔍 Validating USE_FAKE_API configuration...") - - // Check 1: Validate build.gradle.kts release configuration - val buildGradleContent = buildGradleFile.readText() - - // Find the release block and check USE_FAKE_API value - val releaseBuildPattern = Regex( - """release\s*\{[^}]*buildConfigField\s*\(\s*"Boolean"\s*,\s*"USE_FAKE_API"\s*,\s*"false"\s*\)""", - RegexOption.DOT_MATCHES_ALL - ) - - if (!releaseBuildPattern.containsMatchIn(buildGradleContent)) { - throw GradleException( - """ - ❌ BUILD VALIDATION FAILED! - - Release build type MUST have USE_FAKE_API set to "false" - Expected: buildConfigField("Boolean", "USE_FAKE_API", "false") - - Location: app/build.gradle.kts (release buildType) - """.trimIndent() - ) - } - - // Check 2: Validate RepositoryConfigProvider.kt - if (repositoryConfigFile.exists()) { - val repositoryConfigContent = repositoryConfigFile.readText() - - // Check that shouldUseFakeData returns BuildConfig.USE_FAKE_API - val correctReturnPattern = Regex( - """val\s+shouldUseFakeData\s*:\s*Boolean[^}]*return\s+BuildConfig\.USE_FAKE_API""", - RegexOption.DOT_MATCHES_ALL - ) - - if (!correctReturnPattern.containsMatchIn(repositoryConfigContent)) { - throw GradleException( - """ - ❌ BUILD VALIDATION FAILED! - - RepositoryConfigProvider.shouldUseFakeData MUST return BuildConfig.USE_FAKE_API - Expected: return BuildConfig.USE_FAKE_API - - Location: app/src/main/java/ink/trmnl/android/data/RepositoryConfigProvider.kt - - Do NOT hardcode or override this value in committed code! - """.trimIndent() - ) - } - } else { - println("âš ī¸ RepositoryConfigProvider.kt not found, skipping validation") - } - - println("✅ USE_FAKE_API configuration is valid") - } -} - -// Automatically run validation before release builds (after Android tasks are registered) -afterEvaluate { - tasks.findByName("assembleRelease")?.dependsOn(validateUseFakeApiConfig) - tasks.findByName("bundleRelease")?.dependsOn(validateUseFakeApiConfig) -} diff --git a/app/src/main/java/ink/trmnl/android/data/RepositoryConfigProvider.kt b/app/src/main/java/ink/trmnl/android/data/RepositoryConfigProvider.kt deleted file mode 100644 index 7776a64..0000000 --- a/app/src/main/java/ink/trmnl/android/data/RepositoryConfigProvider.kt +++ /dev/null @@ -1,25 +0,0 @@ -package ink.trmnl.android.data - -import ink.trmnl.android.BuildConfig -import javax.inject.Inject - -/** - * Provides configuration for repository data sources. This class helps control whether repositories - * should use fake/mock data instead of real API data, which is useful for development and testing. - */ -class RepositoryConfigProvider - @Inject - constructor() { - /** - * Indicates if the app should use fake data instead of real API responses. - * - * @return Boolean value from [BuildConfig.USE_FAKE_API] - */ - val shouldUseFakeData: Boolean - get() { - // To change this value, update the `buildConfigField` in the app's build.gradle file - // Or, change the value here for local development. Do not commit this change. - // âš ī¸ Return value should always be `BuildConfig.USE_FAKE_API` - return BuildConfig.USE_FAKE_API - } - } diff --git a/app/src/main/java/ink/trmnl/android/data/TrmnlDisplayRepository.kt b/app/src/main/java/ink/trmnl/android/data/TrmnlDisplayRepository.kt index b8ea488..b1c2b01 100644 --- a/app/src/main/java/ink/trmnl/android/data/TrmnlDisplayRepository.kt +++ b/app/src/main/java/ink/trmnl/android/data/TrmnlDisplayRepository.kt @@ -3,9 +3,6 @@ package ink.trmnl.android.data import com.slack.eithernet.ApiResult import com.slack.eithernet.exceptionOrNull import com.squareup.anvil.annotations.optional.SingleIn -import ink.trmnl.android.BuildConfig.USE_FAKE_API -import ink.trmnl.android.data.fake.generateFakeDeviceSetupInfo -import ink.trmnl.android.data.fake.generateFakeTrmnlDisplayInfo import ink.trmnl.android.di.AppScope import ink.trmnl.android.model.SupportedDeviceModel import ink.trmnl.android.model.TrmnlDeviceConfig @@ -27,12 +24,6 @@ import javax.inject.Inject /** * Repository class responsible for fetching and mapping display data. - * - * âš ī¸ NOTE: [USE_FAKE_API] is set to `true` in debug builds, meaning it will - * use mock data and avoid network calls. In release builds, it is set to `false` - * to enable real API calls. - * - * You can override this behavior by updating [RepositoryConfigProvider.shouldUseFakeData] for local development. */ @SingleIn(AppScope::class) class TrmnlDisplayRepository @@ -40,12 +31,10 @@ class TrmnlDisplayRepository constructor( private val apiService: TrmnlApiService, private val imageMetadataStore: ImageMetadataStore, - private val repositoryConfigProvider: RepositoryConfigProvider, private val androidDeviceInfoProvider: AndroidDeviceInfoProvider, ) { /** * Fetches display data for next plugin from the server using the provided access token. - * If the app is in debug mode, it uses mock data instead. * * @param trmnlDeviceConfig Device configuration containing the access token and other settings. * @return A [TrmnlDisplayInfo] object containing the display data. @@ -53,11 +42,6 @@ class TrmnlDisplayRepository suspend fun getNextDisplayData(trmnlDeviceConfig: TrmnlDeviceConfig): TrmnlDisplayInfo { Timber.i("Fetching next playlist item display data from server for device: ${trmnlDeviceConfig.type}") - if (repositoryConfigProvider.shouldUseFakeData) { - // Avoid using real API in debug mode - return generateFakeTrmnlDisplayInfo(imageMetadataStore = imageMetadataStore, apiUsed = "next-image") - } - val result = apiService .getNextDisplayData( @@ -126,7 +110,6 @@ class TrmnlDisplayRepository /** * Fetches the current display data from the server using the provided access token. - * If the app is in debug mode, it uses mock data instead. * * âš ī¸ NOTE: This API is not available on BYOS servers. * See https://discord.com/channels/1281055965508141100/1331360842809348106/1382863253880963124 @@ -141,11 +124,6 @@ class TrmnlDisplayRepository Timber.w("Current display image data API is not available for BYOS service.") } - if (repositoryConfigProvider.shouldUseFakeData) { - // Avoid using real API in debug mode - return generateFakeTrmnlDisplayInfo(imageMetadataStore = imageMetadataStore, apiUsed = "current-image") - } - val result = apiService .getCurrentDisplayData( @@ -197,11 +175,6 @@ class TrmnlDisplayRepository Timber.w("Device setup is only applicable for BYOS devices.") } - if (repositoryConfigProvider.shouldUseFakeData) { - // Avoid using real API in debug mode - return generateFakeDeviceSetupInfo() - } - val result = apiService.setupNewDevice( fullApiUrl = constructApiUrl(trmnlDeviceConfig.apiBaseUrl, TrmnlApiService.SETUP_API_PATH), diff --git a/app/src/main/java/ink/trmnl/android/data/TrmnlUserRepository.kt b/app/src/main/java/ink/trmnl/android/data/TrmnlUserRepository.kt index 0e4f1da..4b8bd93 100644 --- a/app/src/main/java/ink/trmnl/android/data/TrmnlUserRepository.kt +++ b/app/src/main/java/ink/trmnl/android/data/TrmnlUserRepository.kt @@ -33,7 +33,6 @@ class TrmnlUserRepository @Inject constructor( private val userApiService: TrmnlUserApiService, - private val repositoryConfigProvider: RepositoryConfigProvider, private val androidDeviceInfoProvider: AndroidDeviceInfoProvider, ) { /** @@ -52,23 +51,6 @@ class TrmnlUserRepository ): Result { Timber.i("Validating user API token") - if (repositoryConfigProvider.shouldUseFakeData) { - // Return fake user data in debug mode - return Result.success( - TrmnlUser( - id = 42, - name = "Test User", - email = "test@example.com", - firstName = "Test", - lastName = "User", - locale = "en", - timeZone = "Eastern Time (US & Canada)", - timeZoneIana = "America/New_York", - utcOffset = -14400, - ), - ) - } - val result = userApiService.getUserInfo( fullApiUrl = constructApiUrl(apiBaseUrl, USER_INFO_API_PATH), diff --git a/app/src/main/java/ink/trmnl/android/data/fake/FakeNetworkCommunication.kt b/app/src/main/java/ink/trmnl/android/data/fake/FakeNetworkCommunication.kt deleted file mode 100644 index 4c8baf9..0000000 --- a/app/src/main/java/ink/trmnl/android/data/fake/FakeNetworkCommunication.kt +++ /dev/null @@ -1,52 +0,0 @@ -package ink.trmnl.android.data.fake - -import ink.trmnl.android.data.DeviceSetupInfo -import ink.trmnl.android.data.ImageMetadataStore -import ink.trmnl.android.data.RepositoryConfigProvider -import ink.trmnl.android.data.TrmnlDisplayInfo -import ink.trmnl.android.model.TrmnlDeviceType -import ink.trmnl.android.util.HTTP_200 -import timber.log.Timber - -// Contains fake implementations of network communication for local development -// and testing without depending on real servers. - -/** - * Generates fake display info for debugging purposes without wasting an API request. - * - * â„šī¸ This is only used when [RepositoryConfigProvider.shouldUseFakeData] is true. - */ -internal suspend fun generateFakeTrmnlDisplayInfo( - imageMetadataStore: ImageMetadataStore, - apiUsed: String, -): TrmnlDisplayInfo { - Timber.d("DEBUG: Using mock data for display info") - val timestampMin = System.currentTimeMillis() / 60_000 // Changes every minute - val mockImageUrl = "https://picsum.photos/300/200?grayscale&time=$timestampMin&api=$apiUsed" - val mockRefreshRate = 600L - - // Save mock data to the data store - imageMetadataStore.saveImageMetadata(mockImageUrl, mockRefreshRate) - - return TrmnlDisplayInfo( - status = HTTP_200, - trmnlDeviceType = TrmnlDeviceType.TRMNL, - imageUrl = mockImageUrl, - imageFileName = "mocked-image-" + mockImageUrl.substringAfterLast('?'), - error = null, - refreshIntervalSeconds = mockRefreshRate, - ) -} - -/** - * Generates fake setup info for debugging purposes without wasting an API request. - * - * â„šī¸ This is only used when [RepositoryConfigProvider.shouldUseFakeData] is true. - */ -internal fun generateFakeDeviceSetupInfo(): DeviceSetupInfo = - DeviceSetupInfo( - success = true, - deviceMacId = "A1:B2:C3:D4:E5:F6", - apiKey = "mocked-api-key-${System.currentTimeMillis()}", - message = "Mocked device setup successful", - ) diff --git a/app/src/main/java/ink/trmnl/android/ui/settings/AppSettingsScreen.kt b/app/src/main/java/ink/trmnl/android/ui/settings/AppSettingsScreen.kt index 16ddf21..2f4de35 100644 --- a/app/src/main/java/ink/trmnl/android/ui/settings/AppSettingsScreen.kt +++ b/app/src/main/java/ink/trmnl/android/ui/settings/AppSettingsScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -60,7 +59,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -85,7 +83,6 @@ import ink.trmnl.android.R import ink.trmnl.android.data.AppConfig.DEFAULT_REFRESH_INTERVAL_SEC import ink.trmnl.android.data.AppConfig.TRMNL_API_SERVER_BASE_URL import ink.trmnl.android.data.DeviceSetupInfo -import ink.trmnl.android.data.RepositoryConfigProvider import ink.trmnl.android.data.TrmnlDeviceConfigDataStore import ink.trmnl.android.data.TrmnlDisplayRepository import ink.trmnl.android.di.AppScope @@ -144,7 +141,6 @@ data class AppSettingsScreen( val accessToken: String, val deviceMacId: String, val isByodMasterDevice: Boolean, - val usesFakeApiData: Boolean, val isLoading: Boolean = false, val validationResult: ValidationResult? = null, val isDeviceSetupLoading: Boolean = false, @@ -273,7 +269,6 @@ class AppSettingsPresenter private val deviceConfigStore: TrmnlDeviceConfigDataStore, private val trmnlWorkScheduler: TrmnlWorkScheduler, private val trmnlImageUpdateManager: TrmnlImageUpdateManager, - private val repositoryConfigProvider: RepositoryConfigProvider, ) : Presenter { @Composable override fun present(): AppSettingsScreen.State { @@ -286,7 +281,6 @@ class AppSettingsPresenter var validationResult by remember { mutableStateOf(null) } var isDeviceSetupLoading by remember { mutableStateOf(false) } var deviceSetupMessage by remember { mutableStateOf(null) } - val usesFakeApiData = repositoryConfigProvider.shouldUseFakeData val scope = rememberCoroutineScope() val focusManager = LocalFocusManager.current @@ -354,7 +348,6 @@ class AppSettingsPresenter accessToken = accessToken, deviceMacId = deviceMacId, isByodMasterDevice = isByodMasterDevice, - usesFakeApiData = usesFakeApiData, isLoading = isLoading, validationResult = validationResult, isDeviceSetupLoading = isDeviceSetupLoading, @@ -668,15 +661,6 @@ fun AppSettingsContent( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - if (state.usesFakeApiData) { - FakeApiInfoBanner( - modifier = - Modifier - .fillMaxWidth() - .padding(bottom = 24.dp), - ) - } - Icon( painter = painterResource(R.drawable.trmnl_logo_plain), contentDescription = "TRMNL Logo", @@ -1298,52 +1282,6 @@ private fun WorkScheduleStatusCard( * A composable function that displays a banner indicating that the app is in developer mode * and is using mock data instead of real API calls. */ -@Composable -private fun FakeApiInfoBanner(modifier: Modifier = Modifier) { - Card( - modifier = modifier, - colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - ), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary), - ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Icon( - imageVector = Icons.Outlined.Warning, - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary, - ) - - Column { - Text( - text = "Developer Mode - Using Fake API", - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold, - ) - - Text( - text = "This app is currently using mock data instead of real API calls.", - style = MaterialTheme.typography.bodySmall, - ) - - Text( - text = "Set `BuildConfig.USE_FAKE_API` to `false` using build.gradle to use real API.", - style = MaterialTheme.typography.bodySmall, - fontStyle = FontStyle.Italic, - ) - } - } - } -} - @Preview(name = "App Settings Content - Initial State") @Composable private fun PreviewAppSettingsContentInitial() { @@ -1356,7 +1294,6 @@ private fun PreviewAppSettingsContentInitial() { accessToken = "", deviceMacId = "aa:bb:cc:dd:ee:ff", isByodMasterDevice = true, - usesFakeApiData = true, isLoading = false, validationResult = null, nextRefreshJobInfo = null, @@ -1378,7 +1315,6 @@ private fun PreviewAppSettingsContentLoading() { accessToken = "some-token", deviceMacId = "aa:bb:cc:dd:ee:ff", isByodMasterDevice = true, - usesFakeApiData = false, isLoading = true, validationResult = null, nextRefreshJobInfo = null, @@ -1400,7 +1336,6 @@ private fun PreviewAppSettingsContentSuccess() { accessToken = "valid-token-123", deviceMacId = "aa:bb:cc:dd:ee:ff", isByodMasterDevice = true, - usesFakeApiData = false, isLoading = false, validationResult = ValidationResult.Success( @@ -1426,7 +1361,6 @@ private fun PreviewAppSettingsContentFailure() { accessToken = "invalid-token", deviceMacId = "aa:bb:cc:dd:ee:ff", isByodMasterDevice = true, - usesFakeApiData = false, isLoading = false, validationResult = ValidationResult.Failure( @@ -1455,7 +1389,6 @@ private fun PreviewAppSettingsContentWithWork() { accessToken = "valid-token-123", deviceMacId = "aa:bb:cc:dd:ee:ff", isByodMasterDevice = true, - usesFakeApiData = false, isLoading = false, validationResult = null, // Can also be Success state nextRefreshJobInfo = @@ -1487,7 +1420,6 @@ private fun PreviewWorkScheduleStatusCardScheduled() { accessToken = "some-token", deviceMacId = "AA:BB:CC:DD:EE:FF", isByodMasterDevice = true, - usesFakeApiData = false, nextRefreshJobInfo = NextImageRefreshDisplayInfo( workerState = WorkInfo.State.ENQUEUED, @@ -1513,7 +1445,6 @@ private fun PreviewWorkScheduleStatusCardNoWork() { accessToken = "some-token", deviceMacId = "aa:bb:cc:dd:ee:ff", isByodMasterDevice = true, - usesFakeApiData = false, nextRefreshJobInfo = null, eventSink = {}, ), @@ -1521,14 +1452,6 @@ private fun PreviewWorkScheduleStatusCardNoWork() { } } -@Preview(name = "Fake API Info Banner") -@Composable -private fun PreviewFakeApiInfoBanner() { - TrmnlDisplayAppTheme { - FakeApiInfoBanner() - } -} - @Preview(name = "App Settings Content - BYOD Selected") @Composable private fun PreviewAppSettingsContentByod() { @@ -1541,7 +1464,6 @@ private fun PreviewAppSettingsContentByod() { accessToken = "byod-access-token-here", deviceMacId = "", isByodMasterDevice = false, - usesFakeApiData = false, isLoading = false, validationResult = null, nextRefreshJobInfo = null, @@ -1564,7 +1486,6 @@ private fun PreviewAppSettingsContentByos() { accessToken = "byos-access-token-here", deviceMacId = "AA:BB:CC:DD:EE:FF", isByodMasterDevice = true, - usesFakeApiData = false, isLoading = false, validationResult = null, nextRefreshJobInfo = null, diff --git a/app/src/test/java/ink/trmnl/android/data/TrmnlDisplayRepositoryTest.kt b/app/src/test/java/ink/trmnl/android/data/TrmnlDisplayRepositoryTest.kt index c4f4ac7..c95a902 100644 --- a/app/src/test/java/ink/trmnl/android/data/TrmnlDisplayRepositoryTest.kt +++ b/app/src/test/java/ink/trmnl/android/data/TrmnlDisplayRepositoryTest.kt @@ -30,7 +30,6 @@ class TrmnlDisplayRepositoryTest { private lateinit var repository: TrmnlDisplayRepository private lateinit var apiService: TrmnlApiService private lateinit var imageMetadataStore: ImageMetadataStore - private lateinit var repositoryConfigProvider: RepositoryConfigProvider private lateinit var deviceConfigDataStore: TrmnlDeviceConfigDataStore private lateinit var androidDeviceInfoProvider: AndroidDeviceInfoProvider @@ -61,18 +60,14 @@ class TrmnlDisplayRepositoryTest { @Before fun setup() { apiService = mockk() - repositoryConfigProvider = mockk() deviceConfigDataStore = mockk() imageMetadataStore = mockk(relaxed = true) androidDeviceInfoProvider = mockk(relaxed = true) - every { repositoryConfigProvider.shouldUseFakeData } returns false - repository = TrmnlDisplayRepository( apiService = apiService, imageMetadataStore = imageMetadataStore, - repositoryConfigProvider = repositoryConfigProvider, androidDeviceInfoProvider = androidDeviceInfoProvider, ) } @@ -242,46 +237,6 @@ class TrmnlDisplayRepositoryTest { coVerify(exactly = 0) { imageMetadataStore.saveImageMetadata(any(), any()) } } - @Test - fun `getNextDisplayData should return fake data when shouldUseFakeData is true`() = - runTest { - // Arrange - every { repositoryConfigProvider.shouldUseFakeData } returns true - - // Act - val result = repository.getNextDisplayData(testDeviceConfig) - - // Assert - assertThat(result.status).isEqualTo(200) - assertThat(result.imageUrl).contains("picsum.photos") - assertThat(result.imageFileName).contains("mocked-image-grayscale&time") - assertThat(result.refreshIntervalSeconds).isEqualTo(600L) - assertThat(result.error).isNull() - - // Verify API was NOT called - coVerify(exactly = 0) { apiService.getNextDisplayData(any(), any()) } - } - - @Test - fun `getCurrentDisplayData should return fake data when shouldUseFakeData is true`() = - runTest { - // Arrange - every { repositoryConfigProvider.shouldUseFakeData } returns true - - // Act - val result = repository.getCurrentDisplayData(testDeviceConfig) - - // Assert - assertThat(result.status).isEqualTo(200) - assertThat(result.imageUrl).contains("picsum.photos") - assertThat(result.imageFileName).contains("mocked-image-grayscale&time") - assertThat(result.refreshIntervalSeconds).isEqualTo(600L) - assertThat(result.error).isNull() - - // Verify API was NOT called - coVerify(exactly = 0) { apiService.getCurrentDisplayData(any(), any()) } - } - @Test fun `constructApiUrl should handle URLs with and without trailing slashes`() = runTest {