diff --git a/app/src/main/kotlin/org/jdc/template/util/ext/KtorExt.kt b/app/src/main/kotlin/org/jdc/template/util/ext/KtorExt.kt index 3a7d690d..fde32e12 100644 --- a/app/src/main/kotlin/org/jdc/template/util/ext/KtorExt.kt +++ b/app/src/main/kotlin/org/jdc/template/util/ext/KtorExt.kt @@ -75,8 +75,7 @@ suspend fun HttpClient.executeSafelyCached( val response = apiCall() if (response.status == HttpStatusCode.NotModified) { CacheApiResponse.Success(null, response.etag(), response.headers[HttpHeaders.LastModified]) - } - if (response.status.isSuccess()) { + } else if (response.status.isSuccess()) { CacheApiResponse.Success(mapSuccess(response), response.etag(), response.headers[HttpHeaders.LastModified]) } else { val message = "Error executing service call: ${response.request.method.value} ${response.request.url} (${response.status})" diff --git a/app/src/test/kotlin/org/jdc/template/util/network/CacheApiResponseTest.kt b/app/src/test/kotlin/org/jdc/template/util/network/CacheApiResponseTest.kt index bd9c51ad..42901214 100644 --- a/app/src/test/kotlin/org/jdc/template/util/network/CacheApiResponseTest.kt +++ b/app/src/test/kotlin/org/jdc/template/util/network/CacheApiResponseTest.kt @@ -14,28 +14,66 @@ import io.ktor.client.plugins.resources.get import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.headersOf +import io.ktor.resources.Resource import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable import org.jdc.template.util.ext.executeSafelyCached import org.junit.jupiter.api.Test class CacheApiResponseTest { @Test - fun `test CacheApiResponse Success`() = runTest { + fun `test CacheApiResponse Success Etag Data`() = runTest { val mockEngine = MockEngine { + assertThat(it.headers[HttpHeaders.IfNoneMatch]).isEqualTo("abc") respond( content = """{"status":"success"}""", - headers = headersOf(HttpHeaders.ContentType, "application/json"), + headers = headersOf(HttpHeaders.ContentType to listOf("application/json"), HttpHeaders.ETag to listOf("def")), status = HttpStatusCode.OK, ) } val client = TestHttpClientProvider.getTestClient(mockEngine) - val response: CacheApiResponse = client.executeSafelyCached({ get(TestResource) }) { it.body() } + val response: CacheApiResponse = client.executeSafelyCached({ get(CacheTestResource) { cacheHeaders("abc", null) } }) { it.body() } + + assertThat(response).isInstanceOf(CacheApiResponse.Success::class) + assertThat(response.getOrNull()).isEqualTo(DtoCacheTest("success")) + assertThat(response.getOrThrow()).isEqualTo(DtoCacheTest("success")) + assertThat(response.messageOrNull).isNull() + assertThat(response.isSuccess).isTrue() + assertThat(response.isFailure).isFalse() + assertThat(response.isError).isFalse() + assertThat(response.isException).isFalse() + + response.onSuccess { + assertThat(data).isEqualTo(DtoCacheTest("success")) + assertThat(etag).isEqualTo("def") + }.onFailure { + fail("Should not be called") + }.onError { + fail("Should not be called") + }.onException { + fail("Should not be called") + } + } + + @Test + fun `test CacheApiResponse Success Etag No Data`() = runTest { + val mockEngine = MockEngine { + assertThat(it.headers[HttpHeaders.IfNoneMatch]).isEqualTo("def") + respond( + content = "", + headers = headersOf(HttpHeaders.ETag to listOf("def")), + status = HttpStatusCode.NotModified, + ) + } + val client = TestHttpClientProvider.getTestClient(mockEngine) + + val response: CacheApiResponse = client.executeSafelyCached({ get(CacheTestResource) { cacheHeaders("def", null) } }) { it.body() } assertThat(response).isInstanceOf(CacheApiResponse.Success::class) - assertThat(response.getOrNull()).isEqualTo(DtoTest("success")) - assertThat(response.getOrThrow()).isEqualTo(DtoTest("success")) + assertThat(response.getOrNull()).isNull() + assertThat(response.getOrThrow()).isNull() assertThat(response.messageOrNull).isNull() assertThat(response.isSuccess).isTrue() assertThat(response.isFailure).isFalse() @@ -43,7 +81,79 @@ class CacheApiResponseTest { assertThat(response.isException).isFalse() response.onSuccess { - assertThat(data).isEqualTo(DtoTest("success")) + assertThat(data).isNull() + assertThat(etag).isEqualTo("def") + }.onFailure { + fail("Should not be called") + }.onError { + fail("Should not be called") + }.onException { + fail("Should not be called") + } + } + + @Test + fun `test CacheApiResponse Success LastModified Data`() = runTest { + val mockEngine = MockEngine { + assertThat(it.headers[HttpHeaders.IfModifiedSince]).isEqualTo("abc") + + respond( + content = """{"status":"success"}""", + headers = headersOf(HttpHeaders.ContentType to listOf("application/json"), HttpHeaders.LastModified to listOf("def")), + status = HttpStatusCode.OK, + ) + } + val client = TestHttpClientProvider.getTestClient(mockEngine) + + val response: CacheApiResponse = client.executeSafelyCached({ get(CacheTestResource) { cacheHeaders(null, "abc") } }) { it.body() } + + assertThat(response).isInstanceOf(CacheApiResponse.Success::class) + assertThat(response.getOrNull()).isEqualTo(DtoCacheTest("success")) + assertThat(response.getOrThrow()).isEqualTo(DtoCacheTest("success")) + assertThat(response.messageOrNull).isNull() + assertThat(response.isSuccess).isTrue() + assertThat(response.isFailure).isFalse() + assertThat(response.isError).isFalse() + assertThat(response.isException).isFalse() + + response.onSuccess { + assertThat(data).isEqualTo(DtoCacheTest("success")) + assertThat(lastModified).isEqualTo("def") + }.onFailure { + fail("Should not be called") + }.onError { + fail("Should not be called") + }.onException { + fail("Should not be called") + } + } + + @Test + fun `test CacheApiResponse Success LastModified No Data`() = runTest { + val mockEngine = MockEngine { + assertThat(it.headers[HttpHeaders.IfModifiedSince]).isEqualTo("def") + respond( + content = "", + headers = headersOf(HttpHeaders.LastModified to listOf("def")), + status = HttpStatusCode.NotModified, + ) + } + val client = TestHttpClientProvider.getTestClient(mockEngine) + + val response: CacheApiResponse = client.executeSafelyCached({ get(CacheTestResource) { cacheHeaders(null, "def") } }) { it.body() } + + assertThat(response).isInstanceOf(CacheApiResponse.Success::class) + assertThat(response.getOrNull()).isNull() + assertThat(response.getOrThrow()).isNull() + assertThat(response.messageOrNull).isNull() + assertThat(response.isSuccess).isTrue() + assertThat(response.isFailure).isFalse() + assertThat(response.isError).isFalse() + assertThat(response.isException).isFalse() + + response.onSuccess { + assertThat(data).isNull() + assertThat(lastModified).isEqualTo("def") }.onFailure { fail("Should not be called") }.onError { @@ -333,3 +443,8 @@ class CacheApiResponseTest { } } + +@Serializable +private data class DtoCacheTest( val status: String) +@Resource("test") +private object CacheTestResource