diff --git a/app/src/test/java/ink/trmnl/android/data/HttpResponseMetadataTest.kt b/app/src/test/java/ink/trmnl/android/data/HttpResponseMetadataTest.kt new file mode 100644 index 0000000..bf90ff8 --- /dev/null +++ b/app/src/test/java/ink/trmnl/android/data/HttpResponseMetadataTest.kt @@ -0,0 +1,150 @@ +package ink.trmnl.android.data + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +/** + * Unit tests for HttpResponseMetadata data class and its companion object methods. + */ +class HttpResponseMetadataTest { + @Test + fun `empty creates metadata with default values`() { + val result = HttpResponseMetadata.empty() + + assertThat(result.url).isEqualTo("https://example.com") + assertThat(result.protocol).isEqualTo("http/1.1") + assertThat(result.statusCode).isEqualTo(0) + assertThat(result.message).isEqualTo("Not applicable") + assertThat(result.contentType).isNull() + assertThat(result.contentLength).isEqualTo(-1) + assertThat(result.serverName).isNull() + assertThat(result.requestDuration).isEqualTo(-1) + assertThat(result.etag).isNull() + assertThat(result.requestId).isNull() + assertThat(result.timestamp).isGreaterThan(0L) + } + + @Test + fun `empty creates metadata with current timestamp`() { + val beforeTimestamp = System.currentTimeMillis() + val result = HttpResponseMetadata.empty() + val afterTimestamp = System.currentTimeMillis() + + assertThat(result.timestamp).isAtLeast(beforeTimestamp) + assertThat(result.timestamp).isAtMost(afterTimestamp) + } + + @Test + fun `constructor creates metadata with all provided values`() { + val result = + HttpResponseMetadata( + url = "https://api.example.com/test", + protocol = "h2", + statusCode = 200, + message = "OK", + contentType = "application/json", + contentLength = 1234L, + serverName = "TestServer/1.0", + requestDuration = 500L, + etag = "W/\"abc-123\"", + requestId = "req-456", + timestamp = 1234567890L, + ) + + assertThat(result.url).isEqualTo("https://api.example.com/test") + assertThat(result.protocol).isEqualTo("h2") + assertThat(result.statusCode).isEqualTo(200) + assertThat(result.message).isEqualTo("OK") + assertThat(result.contentType).isEqualTo("application/json") + assertThat(result.contentLength).isEqualTo(1234L) + assertThat(result.serverName).isEqualTo("TestServer/1.0") + assertThat(result.requestDuration).isEqualTo(500L) + assertThat(result.etag).isEqualTo("W/\"abc-123\"") + assertThat(result.requestId).isEqualTo("req-456") + assertThat(result.timestamp).isEqualTo(1234567890L) + } + + @Test + fun `constructor uses default timestamp when not provided`() { + val beforeTimestamp = System.currentTimeMillis() + + val result = + HttpResponseMetadata( + url = "https://api.example.com/test", + protocol = "h2", + statusCode = 200, + message = "OK", + contentType = "application/json", + contentLength = 1234L, + serverName = "TestServer", + requestDuration = 100L, + etag = null, + requestId = null, + ) + + val afterTimestamp = System.currentTimeMillis() + + assertThat(result.timestamp).isAtLeast(beforeTimestamp) + assertThat(result.timestamp).isAtMost(afterTimestamp) + } + + @Test + fun `data class equality works correctly`() { + val metadata1 = + HttpResponseMetadata( + url = "https://api.example.com", + protocol = "h2", + statusCode = 200, + message = "OK", + contentType = "application/json", + contentLength = 100L, + serverName = "Server", + requestDuration = 50L, + etag = "abc", + requestId = "123", + timestamp = 1000L, + ) + + val metadata2 = + HttpResponseMetadata( + url = "https://api.example.com", + protocol = "h2", + statusCode = 200, + message = "OK", + contentType = "application/json", + contentLength = 100L, + serverName = "Server", + requestDuration = 50L, + etag = "abc", + requestId = "123", + timestamp = 1000L, + ) + + assertThat(metadata1).isEqualTo(metadata2) + } + + @Test + fun `data class copy works correctly`() { + val original = + HttpResponseMetadata( + url = "https://api.example.com", + protocol = "h2", + statusCode = 200, + message = "OK", + contentType = "application/json", + contentLength = 100L, + serverName = "Server", + requestDuration = 50L, + etag = "abc", + requestId = "123", + timestamp = 1000L, + ) + + val copied = original.copy(statusCode = 404, message = "Not Found") + + assertThat(copied.statusCode).isEqualTo(404) + assertThat(copied.message).isEqualTo("Not Found") + assertThat(copied.url).isEqualTo(original.url) + assertThat(copied.protocol).isEqualTo(original.protocol) + } +} diff --git a/app/src/test/java/ink/trmnl/android/data/TrmnlDisplayInfoTest.kt b/app/src/test/java/ink/trmnl/android/data/TrmnlDisplayInfoTest.kt new file mode 100644 index 0000000..6508bf2 --- /dev/null +++ b/app/src/test/java/ink/trmnl/android/data/TrmnlDisplayInfoTest.kt @@ -0,0 +1,174 @@ +package ink.trmnl.android.data + +import com.google.common.truth.Truth.assertThat +import ink.trmnl.android.model.TrmnlDeviceType +import ink.trmnl.android.util.ERROR_TYPE_DEVICE_SETUP_REQUIRED +import ink.trmnl.android.util.HTTP_200 +import ink.trmnl.android.util.HTTP_500 +import org.junit.Test + +/** + * Unit tests for TrmnlDisplayInfo data class and its companion object methods. + */ +class TrmnlDisplayInfoTest { + @Test + fun `setupRequired creates display info with correct values`() { + val result = TrmnlDisplayInfo.setupRequired() + + assertThat(result.status).isEqualTo(HTTP_500) + assertThat(result.trmnlDeviceType).isEqualTo(TrmnlDeviceType.BYOS) + assertThat(result.imageUrl).isEmpty() + assertThat(result.imageFileName).isEqualTo(ERROR_TYPE_DEVICE_SETUP_REQUIRED) + assertThat(result.error).isEqualTo("Device setup required") + assertThat(result.refreshIntervalSeconds).isEqualTo(0L) + } + + @Test + fun `setupRequired creates display info without http metadata`() { + val result = TrmnlDisplayInfo.setupRequired() + + assertThat(result.httpResponseMetadata).isNull() + } + + @Test + fun `constructor creates display info with all provided values`() { + val httpMetadata = + HttpResponseMetadata( + url = "https://api.example.com/display", + protocol = "h2", + statusCode = 200, + message = "OK", + contentType = "application/json", + contentLength = 1234L, + serverName = "TestServer", + requestDuration = 500L, + etag = "abc", + requestId = "123", + ) + + val result = + TrmnlDisplayInfo( + status = HTTP_200, + trmnlDeviceType = TrmnlDeviceType.TRMNL, + imageUrl = "https://test.com/image.png", + imageFileName = "test-image.png", + error = null, + refreshIntervalSeconds = 600L, + httpResponseMetadata = httpMetadata, + ) + + assertThat(result.status).isEqualTo(HTTP_200) + assertThat(result.trmnlDeviceType).isEqualTo(TrmnlDeviceType.TRMNL) + assertThat(result.imageUrl).isEqualTo("https://test.com/image.png") + assertThat(result.imageFileName).isEqualTo("test-image.png") + assertThat(result.error).isNull() + assertThat(result.refreshIntervalSeconds).isEqualTo(600L) + assertThat(result.httpResponseMetadata).isEqualTo(httpMetadata) + } + + @Test + fun `constructor uses default refresh interval when not provided`() { + val result = + TrmnlDisplayInfo( + status = HTTP_200, + trmnlDeviceType = TrmnlDeviceType.TRMNL, + imageUrl = "https://test.com/image.png", + imageFileName = "test-image.png", + ) + + assertThat(result.refreshIntervalSeconds).isEqualTo(AppConfig.DEFAULT_REFRESH_INTERVAL_SEC) + } + + @Test + fun `constructor creates display info with error`() { + val result = + TrmnlDisplayInfo( + status = HTTP_500, + trmnlDeviceType = TrmnlDeviceType.BYOS, + imageUrl = "", + imageFileName = "", + error = "Device not found", + refreshIntervalSeconds = null, + ) + + assertThat(result.status).isEqualTo(HTTP_500) + assertThat(result.error).isEqualTo("Device not found") + assertThat(result.imageUrl).isEmpty() + assertThat(result.imageFileName).isEmpty() + assertThat(result.refreshIntervalSeconds).isNull() + } + + @Test + fun `data class equality works correctly`() { + val info1 = + TrmnlDisplayInfo( + status = HTTP_200, + trmnlDeviceType = TrmnlDeviceType.TRMNL, + imageUrl = "https://test.com/image.png", + imageFileName = "test.png", + error = null, + refreshIntervalSeconds = 600L, + ) + + val info2 = + TrmnlDisplayInfo( + status = HTTP_200, + trmnlDeviceType = TrmnlDeviceType.TRMNL, + imageUrl = "https://test.com/image.png", + imageFileName = "test.png", + error = null, + refreshIntervalSeconds = 600L, + ) + + assertThat(info1).isEqualTo(info2) + } + + @Test + fun `data class copy works correctly`() { + val original = + TrmnlDisplayInfo( + status = HTTP_200, + trmnlDeviceType = TrmnlDeviceType.TRMNL, + imageUrl = "https://test.com/image.png", + imageFileName = "test.png", + ) + + val copied = original.copy(status = HTTP_500, error = "Error occurred") + + assertThat(copied.status).isEqualTo(HTTP_500) + assertThat(copied.error).isEqualTo("Error occurred") + assertThat(copied.imageUrl).isEqualTo(original.imageUrl) + assertThat(copied.trmnlDeviceType).isEqualTo(original.trmnlDeviceType) + } + + @Test + fun `constructor creates display info for each device type`() { + val trmnlInfo = + TrmnlDisplayInfo( + status = HTTP_200, + trmnlDeviceType = TrmnlDeviceType.TRMNL, + imageUrl = "https://test.com/trmnl.png", + imageFileName = "trmnl.png", + ) + + val byosInfo = + TrmnlDisplayInfo( + status = HTTP_200, + trmnlDeviceType = TrmnlDeviceType.BYOS, + imageUrl = "https://test.com/byos.png", + imageFileName = "byos.png", + ) + + val byodInfo = + TrmnlDisplayInfo( + status = HTTP_200, + trmnlDeviceType = TrmnlDeviceType.BYOD, + imageUrl = "https://test.com/byod.png", + imageFileName = "byod.png", + ) + + assertThat(trmnlInfo.trmnlDeviceType).isEqualTo(TrmnlDeviceType.TRMNL) + assertThat(byosInfo.trmnlDeviceType).isEqualTo(TrmnlDeviceType.BYOS) + assertThat(byodInfo.trmnlDeviceType).isEqualTo(TrmnlDeviceType.BYOD) + } +} diff --git a/app/src/test/java/ink/trmnl/android/data/log/TrmnlRefreshLogTest.kt b/app/src/test/java/ink/trmnl/android/data/log/TrmnlRefreshLogTest.kt new file mode 100644 index 0000000..12fc796 --- /dev/null +++ b/app/src/test/java/ink/trmnl/android/data/log/TrmnlRefreshLogTest.kt @@ -0,0 +1,160 @@ +package ink.trmnl.android.data.log + +import com.google.common.truth.Truth.assertThat +import ink.trmnl.android.data.HttpResponseMetadata +import ink.trmnl.android.model.TrmnlDeviceType +import org.junit.Test + +/** + * Unit tests for TrmnlRefreshLog data class and its factory methods. + */ +class TrmnlRefreshLogTest { + @Test + fun `createSuccess creates log with correct values and success flag true`() { + val result = + TrmnlRefreshLog.createSuccess( + trmnlDeviceType = TrmnlDeviceType.TRMNL, + imageUrl = "https://test.com/image.png", + imageName = "test-image.png", + refreshIntervalSeconds = 600L, + imageRefreshWorkType = "PERIODIC", + ) + + assertThat(result.trmnlDeviceType).isEqualTo(TrmnlDeviceType.TRMNL) + assertThat(result.imageUrl).isEqualTo("https://test.com/image.png") + assertThat(result.imageName).isEqualTo("test-image.png") + assertThat(result.refreshIntervalSeconds).isEqualTo(600L) + assertThat(result.imageRefreshWorkType).isEqualTo("PERIODIC") + assertThat(result.success).isTrue() + assertThat(result.error).isNull() + assertThat(result.timestamp).isGreaterThan(0L) + } + + @Test + fun `createSuccess creates log with httpResponseMetadata when provided`() { + val httpMetadata = + HttpResponseMetadata( + url = "https://test.com/api/display", + protocol = "h2", + statusCode = 200, + message = "OK", + contentType = "application/json", + contentLength = 1234L, + serverName = "TestServer", + requestDuration = 500L, + etag = "W/\"abc-123\"", + requestId = "req-123", + ) + + val result = + TrmnlRefreshLog.createSuccess( + trmnlDeviceType = TrmnlDeviceType.BYOS, + imageUrl = "https://test.com/image.png", + imageName = "test-image.png", + refreshIntervalSeconds = 300L, + imageRefreshWorkType = "ONE_TIME", + httpResponseMetadata = httpMetadata, + ) + + assertThat(result.httpResponseMetadata).isEqualTo(httpMetadata) + assertThat(result.success).isTrue() + } + + @Test + fun `createSuccess creates log with null refreshIntervalSeconds`() { + val result = + TrmnlRefreshLog.createSuccess( + trmnlDeviceType = TrmnlDeviceType.BYOD, + imageUrl = "https://test.com/image.png", + imageName = "test-image.png", + refreshIntervalSeconds = null, + imageRefreshWorkType = null, + ) + + assertThat(result.refreshIntervalSeconds).isNull() + assertThat(result.imageRefreshWorkType).isNull() + assertThat(result.success).isTrue() + } + + @Test + fun `createFailure creates log with correct values and success flag false`() { + val result = + TrmnlRefreshLog.createFailure( + error = "Device not found", + ) + + assertThat(result.error).isEqualTo("Device not found") + assertThat(result.success).isFalse() + assertThat(result.imageUrl).isNull() + assertThat(result.imageName).isNull() + assertThat(result.refreshIntervalSeconds).isNull() + assertThat(result.imageRefreshWorkType).isNull() + assertThat(result.timestamp).isGreaterThan(0L) + // Device type is set to TRMNL as default for failures + assertThat(result.trmnlDeviceType).isEqualTo(TrmnlDeviceType.TRMNL) + } + + @Test + fun `createFailure creates log with httpResponseMetadata when provided`() { + val httpMetadata = + HttpResponseMetadata( + url = "https://test.com/api/display", + protocol = "h2", + statusCode = 429, + message = "Too Many Requests", + contentType = "text/html", + contentLength = -1L, + serverName = "cloudflare", + requestDuration = 50L, + etag = null, + requestId = "req-456", + ) + + val result = + TrmnlRefreshLog.createFailure( + error = "Rate limit exceeded", + httpResponseMetadata = httpMetadata, + ) + + assertThat(result.httpResponseMetadata).isEqualTo(httpMetadata) + assertThat(result.error).isEqualTo("Rate limit exceeded") + assertThat(result.success).isFalse() + } + + @Test + fun `createFailure creates log without httpResponseMetadata when not provided`() { + val result = + TrmnlRefreshLog.createFailure( + error = "Network error", + ) + + assertThat(result.httpResponseMetadata).isNull() + assertThat(result.error).isEqualTo("Network error") + } + + @Test + fun `timestamp is set to current time for both success and failure logs`() { + val beforeTimestamp = System.currentTimeMillis() + + val successLog = + TrmnlRefreshLog.createSuccess( + trmnlDeviceType = TrmnlDeviceType.TRMNL, + imageUrl = "https://test.com/image.png", + imageName = "test-image.png", + refreshIntervalSeconds = 600L, + imageRefreshWorkType = "PERIODIC", + ) + + val failureLog = + TrmnlRefreshLog.createFailure( + error = "Test error", + ) + + val afterTimestamp = System.currentTimeMillis() + + assertThat(successLog.timestamp).isAtLeast(beforeTimestamp) + assertThat(successLog.timestamp).isAtMost(afterTimestamp) + assertThat(failureLog.timestamp).isAtLeast(beforeTimestamp) + assertThat(failureLog.timestamp).isAtMost(afterTimestamp) + } +} diff --git a/app/src/test/java/ink/trmnl/android/util/DateTimeFormatterTest.kt b/app/src/test/java/ink/trmnl/android/util/DateTimeFormatterTest.kt new file mode 100644 index 0000000..50b9ac1 --- /dev/null +++ b/app/src/test/java/ink/trmnl/android/util/DateTimeFormatterTest.kt @@ -0,0 +1,122 @@ +package ink.trmnl.android.util + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +/** + * Unit tests for the getTimeElapsedString function in DateTimeFormatter.kt. + * + * Note: These tests use relative timestamps based on System.currentTimeMillis() to avoid + * static mocking issues. Each test creates a timestamp that is a fixed duration in the past. + */ +class DateTimeFormatterTest { + @Test + fun `getTimeElapsedString returns 'Just now' for timestamps less than a minute ago`() { + // Timestamp from 30 seconds ago + val timestamp = System.currentTimeMillis() - 30_000L + + val result = getTimeElapsedString(timestamp) + + assertThat(result).isEqualTo("Just now") + } + + @Test + fun `getTimeElapsedString returns correct singular minute format`() { + // Timestamp from 1 minute ago + val timestamp = System.currentTimeMillis() - 60_000L + + val result = getTimeElapsedString(timestamp) + + assertThat(result).isEqualTo("1 minute ago") + } + + @Test + fun `getTimeElapsedString returns correct plural minutes format`() { + // Timestamp from 9 minutes ago + val timestamp = System.currentTimeMillis() - (9 * 60_000L) + + val result = getTimeElapsedString(timestamp) + + assertThat(result).isEqualTo("9 minutes ago") + } + + @Test + fun `getTimeElapsedString returns correct singular hour format`() { + // Timestamp from 1 hour ago exactly + val timestamp = System.currentTimeMillis() - (60 * 60_000L) + + val result = getTimeElapsedString(timestamp) + + assertThat(result).isEqualTo("1 hour ago") + } + + @Test + fun `getTimeElapsedString returns correct format for hours and minutes`() { + // Timestamp from 3 hours and 23 minutes ago + val timestamp = System.currentTimeMillis() - (3 * 60 * 60_000L) - (23 * 60_000L) + + val result = getTimeElapsedString(timestamp) + + assertThat(result).isEqualTo("3 hours and 23 minutes ago") + } + + @Test + fun `getTimeElapsedString returns correct format for singular hour and singular minute`() { + // Timestamp from 1 hour and 1 minute ago + val timestamp = System.currentTimeMillis() - (1 * 60 * 60_000L) - (1 * 60_000L) + + val result = getTimeElapsedString(timestamp) + + assertThat(result).isEqualTo("1 hour and 1 minute ago") + } + + @Test + fun `getTimeElapsedString returns correct format for singular day`() { + // Timestamp from 1 day ago exactly + val timestamp = System.currentTimeMillis() - (24 * 60 * 60_000L) + + val result = getTimeElapsedString(timestamp) + + assertThat(result).isEqualTo("1 day ago") + } + + @Test + fun `getTimeElapsedString returns correct format for plural days`() { + // Timestamp from 2 days ago exactly + val timestamp = System.currentTimeMillis() - (2 * 24 * 60 * 60_000L) + + val result = getTimeElapsedString(timestamp) + + assertThat(result).isEqualTo("2 days ago") + } + + @Test + fun `getTimeElapsedString returns correct format for days and hours`() { + // Timestamp from 2 days and 5 hours ago (no minutes) + val timestamp = System.currentTimeMillis() - (2 * 24 * 60 * 60_000L) - (5 * 60 * 60_000L) + + val result = getTimeElapsedString(timestamp) + + assertThat(result).isEqualTo("2 days and 5 hours ago") + } + + @Test + fun `getTimeElapsedString returns correct format for day, hours, and minutes`() { + // Timestamp from 2 days, 12 hours, and 45 minutes ago + val timestamp = System.currentTimeMillis() - (2 * 24 * 60 * 60_000L) - (12 * 60 * 60_000L) - (45 * 60_000L) + + val result = getTimeElapsedString(timestamp) + + assertThat(result).isEqualTo("2 days 12 hours and 45 minutes ago") + } + + @Test + fun `getTimeElapsedString returns correct format for singular day, singular hour, and singular minute`() { + // Timestamp from 1 day, 1 hour, and 1 minute ago + val timestamp = System.currentTimeMillis() - (1 * 24 * 60 * 60_000L) - (1 * 60 * 60_000L) - (1 * 60_000L) + + val result = getTimeElapsedString(timestamp) + + assertThat(result).isEqualTo("1 day 1 hour and 1 minute ago") + } +} diff --git a/app/src/test/java/ink/trmnl/android/util/NetworkExtensionsTest.kt b/app/src/test/java/ink/trmnl/android/util/NetworkExtensionsTest.kt new file mode 100644 index 0000000..9a87924 --- /dev/null +++ b/app/src/test/java/ink/trmnl/android/util/NetworkExtensionsTest.kt @@ -0,0 +1,106 @@ +package ink.trmnl.android.util + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +/** + * Unit tests for NetworkExtensions helper functions. + */ +class NetworkExtensionsTest { + @Test + fun `isHttpOk returns true for HTTP_OK status code`() { + assertThat(HTTP_OK.isHttpOk()).isTrue() + } + + @Test + fun `isHttpOk returns true for HTTP_200 status code`() { + assertThat(HTTP_200.isHttpOk()).isTrue() + } + + @Test + fun `isHttpOk returns true for HTTP_NONE status code`() { + assertThat(HTTP_NONE.isHttpOk()).isTrue() + } + + @Test + fun `isHttpOk returns false for HTTP_500 status code`() { + assertThat(HTTP_500.isHttpOk()).isFalse() + } + + @Test + fun `isHttpOk returns false for HTTP_429 status code`() { + assertThat(HTTP_429.isHttpOk()).isFalse() + } + + @Test + fun `isHttpOk returns false for null status code`() { + val nullCode: Int? = null + assertThat(nullCode.isHttpOk()).isFalse() + } + + @Test + fun `isHttpError returns true for HTTP_500 status code`() { + assertThat(HTTP_500.isHttpError()).isTrue() + } + + @Test + fun `isHttpError returns true for null status code`() { + val nullCode: Int? = null + assertThat(nullCode.isHttpError()).isTrue() + } + + @Test + fun `isHttpError returns false for HTTP_OK status code`() { + assertThat(HTTP_OK.isHttpError()).isFalse() + } + + @Test + fun `isHttpError returns false for HTTP_200 status code`() { + assertThat(HTTP_200.isHttpError()).isFalse() + } + + @Test + fun `isHttpError returns false for HTTP_429 status code`() { + assertThat(HTTP_429.isHttpError()).isFalse() + } + + @Test + fun `isRateLimitError returns true for HTTP_429 status code`() { + assertThat(HTTP_429.isRateLimitError()).isTrue() + } + + @Test + fun `isRateLimitError returns false for HTTP_500 status code`() { + assertThat(HTTP_500.isRateLimitError()).isFalse() + } + + @Test + fun `isRateLimitError returns false for HTTP_200 status code`() { + assertThat(HTTP_200.isRateLimitError()).isFalse() + } + + @Test + fun `isRateLimitError returns false for HTTP_OK status code`() { + assertThat(HTTP_OK.isRateLimitError()).isFalse() + } + + @Test + fun `isRateLimitError returns false for null status code`() { + val nullCode: Int? = null + assertThat(nullCode.isRateLimitError()).isFalse() + } + + @Test + fun `HTTP constants have correct values`() { + assertThat(HTTP_500).isEqualTo(500) + assertThat(HTTP_200).isEqualTo(200) + assertThat(HTTP_429).isEqualTo(429) + assertThat(HTTP_OK).isEqualTo(0) + assertThat(HTTP_NONE).isEqualTo(-1) + } + + @Test + fun `ERROR_TYPE_DEVICE_SETUP_REQUIRED has correct value`() { + assertThat(ERROR_TYPE_DEVICE_SETUP_REQUIRED).isEqualTo("device_requires_setup") + } +}