Merge pull request #220 from usetrmnl/copilot/increase-test-coverage

Add unit tests for untested utility functions and data classes
This commit is contained in:
Hossain Khan
2025-11-28 05:17:14 -05:00
committed by GitHub
5 changed files with 712 additions and 0 deletions
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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")
}
}
@@ -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")
}
}