diff --git a/README.md b/README.md index b23dc37e..346e716b 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,12 @@ val user = some() ## Installation -Some is published as three artifacts: +Some is published as four artifacts: - `some-core` for Java and Kotlin/JVM projects - `some-android` for Android projects. It re-exports the core API, so you do not need to add `some-core` separately. - `some-kotest` for Kotest `Arb` integration. Add it alongside either `some-core` or `some-android`. +- `some-retrofit` for Retrofit response fixtures. Add it alongside `some-core` and Retrofit. ```kotlin dependencies { @@ -56,6 +57,10 @@ dependencies { // Optional: Kotest property testing integration. // Add this alongside either some-core or some-android. testImplementation("dev.appoutlet:some-kotest:{version}") + + // Optional: Retrofit response integration. + // Retrofit must be declared directly by the consuming project. + testImplementation("dev.appoutlet:some-retrofit:{version}") } ``` diff --git a/build.gradle.kts b/build.gradle.kts index 663fb117..a3c78e5b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { dokka(projects.android) dokka(projects.core) dokka(projects.kotest) + dokka(projects.retrofit) } tasks.named("prepareKotlinBuildScriptModel") { diff --git a/docs/getting-started.md b/docs/getting-started.md index 61642cbb..49735d60 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,7 +5,7 @@ icon: lucide/graduation-cap ## Installation -Some is published as three artifacts. Pick one base artifact for your platform. Add `some-kotest` only if you use Kotest property testing. +Some is published as four artifacts. Pick one base artifact for your platform. Add `some-kotest` only if you use Kotest property testing and `some-retrofit` only if you use Retrofit. !!! info "Upgrading from an older version?" @@ -24,6 +24,10 @@ dependencies { // Optional: Kotest property testing integration. // Add this alongside either some-core or some-android. testImplementation("dev.appoutlet:some-kotest:{version}") + + // Optional: Retrofit response integration. + // Retrofit must be declared directly by the consuming project. + testImplementation("dev.appoutlet:some-retrofit:{version}") } ``` @@ -105,6 +109,8 @@ val users = some> { For more configuration use cases and details, see the [Configuration overview](configuration.md), [Strategies](strategies/index.md), and [Supported Types](supported-types.md). +For Retrofit response fixtures, see the [Retrofit2 Integration](retrofit.md) guide. + ## Requirements - **Kotlin 2.3.0+** — required for `typeOf()` and other type introspection APIs diff --git a/docs/index.md b/docs/index.md index 18615ce4..80d6c681 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,7 +10,7 @@ icon: lucide/package-open ![Kotlin Android](https://img.shields.io/badge/android-kotlin?style=for-the-badge&logo=android&label=kotlin&color=%231FA95C) ![Maven Central Version](https://img.shields.io/maven-central/v/dev.appoutlet/some-core?style=for-the-badge) -A Kotlin test data generation library for JVM and Android tests, with optional Kotest `Arb` integration. +A Kotlin test data generation library for JVM and Android tests, with optional Kotest `Arb` and Retrofit integrations. ## The Problem @@ -70,11 +70,12 @@ Writing tests means creating test data — lots of it. Constructing data classes ## Modules -Some is published as three user-facing artifacts: +Some is published as four user-facing artifacts: - `some-core` for Java and Kotlin/JVM projects. - `some-android` for Android projects. It re-exports the shared core API, so you do not need to add `some-core` separately. - `some-kotest` for Kotest `Arb` integration. Add it alongside either `some-core` or `some-android`. +- `some-retrofit` for Retrofit response fixtures. Add it alongside `some-core` and Retrofit. Use [Getting Started](getting-started.md) to install Some and learn the shared API. diff --git a/docs/retrofit.md b/docs/retrofit.md new file mode 100644 index 00000000..430fef02 --- /dev/null +++ b/docs/retrofit.md @@ -0,0 +1,69 @@ +--- +icon: lucide/network +--- +# Retrofit2 integration + +Use `dev.appoutlet:some-retrofit` to generate Retrofit `Response` fixtures for JVM tests. +The integration supports both successful and error responses and is used alongside a direct Retrofit dependency. + +## Installation + +```kotlin +dependencies { + testImplementation("com.squareup.retrofit2:retrofit:3.0.0") + testImplementation("dev.appoutlet:some-core:{version}") + testImplementation("dev.appoutlet:some-retrofit:{version}") +} +``` + +`some-retrofit` compiles against Retrofit but does not provide Retrofit transitively. Declare Retrofit directly in the consuming project. + +## Generating successful responses + +The Retrofit resolver is discovered automatically when `some-retrofit` is on the classpath: + +```kotlin +data class User(val id: Int, val name: String) + +val response: Response = some() + +check(response.isSuccessful) +check(response.code() == 200) +check(response.body() != null) +``` + +Use `someResponse()` when response metadata matters: + +```kotlin +val response: Response = someResponse( + code = 201, + message = "Created", + headers = Headers.Builder().add("X-Test", "value").build(), + request = Request.Builder().url("https://example.com/users").build(), +) +``` + +The second overload accepts an existing successful `okhttp3.Response`: + +```kotlin +val response: Response = someResponse(rawResponse) +``` + +Successful response codes must be in the `200..299` range, matching Retrofit's `Response.success` contract. + +## Generating error responses + +Use `someErrorResponse()` for HTTP error handling tests: + +```kotlin +val response: Response = someErrorResponse( + code = 404, + message = "Not Found", +) + +check(response.body() == null) +check(response.errorBody() != null) +``` + +The raw-response overload accepts an existing `okhttp3.Response`. Error response codes must be `400` or higher. +The generated error body is empty; add application-specific error-body handling in the test when needed. diff --git a/docs/supported-types.md b/docs/supported-types.md index 63ec434e..a58312ba 100644 --- a/docs/supported-types.md +++ b/docs/supported-types.md @@ -9,6 +9,8 @@ These supported types are part of the shared API consumed through both `some-cor For Android-specific support, see [Android](android/index.md). +For Retrofit response support, see [Retrofit2 Integration](retrofit.md). + For types not listed here, register a [custom factory](custom-factories.md) or ship a [custom resolver](custom-resolvers.md). ## Reference @@ -47,6 +49,8 @@ For types not listed here, register a [custom factory](custom-factories.md) or s | `Map` | `some>()` | | | `MutableMap` | `some>()` | | | `Array` | `some>()` | | +| **Retrofit** | | | +| `retrofit2.Response` | `some>()` | Requires `some-retrofit` and Retrofit | | **Android & Compose** | | | | Android framework | `some()`, `some()`, ... | See [Android](android/index.md) | | Jetpack Compose | `some()`, `some()`, ... | See [Compose](android/compose.md) | diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a0aee253..1a2e2e50 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ kotlin = "2.4.10" kotest = "6.2.4" mavenPublish = "0.37.0" robolectric = "4.16.1" +retrofit = "3.0.0" [libraries] androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androidxCompose" } @@ -21,6 +22,7 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } [plugins] android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/retrofit/build.gradle.kts b/retrofit/build.gradle.kts new file mode 100644 index 00000000..d9c7a219 --- /dev/null +++ b/retrofit/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.mavenPublish) +} + +group = rootProject.group +version = rootProject.version + +repositories { + mavenCentral() +} + +dependencies { + implementation(projects.core) + compileOnly(libs.retrofit) + + testImplementation(libs.retrofit) + testImplementation(libs.kotlin.test) + + detektPlugins(libs.detekt.formatting) +} + +kotlin { + jvmToolchain(17) +} + +tasks.test { + useJUnitPlatform() +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + signAllPublications() + + coordinates(artifactId = "some-retrofit") + + pom { + name.set("Some Retrofit") + description.set("Retrofit response integration for Some, a Kotlin test data generation library.") + inceptionYear.set("2026") + url.set("https://github.com/MessiasLima/Some") + + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("repo") + } + } + + developers { + developer { + id.set("MessiasLima") + name.set("Messias Lima") + url.set("https://github.com/MessiasLima") + } + } + + scm { + url.set("https://github.com/MessiasLima/Some") + connection.set("scm:git:git://github.com/MessiasLima/Some.git") + developerConnection.set("scm:git:ssh://git@github.com/MessiasLima/Some.git") + } + } +} + +detekt { autoCorrect = true } diff --git a/retrofit/src/main/kotlin/dev/appoutlet/some/retrofit/RetrofitResolverProvider.kt b/retrofit/src/main/kotlin/dev/appoutlet/some/retrofit/RetrofitResolverProvider.kt new file mode 100644 index 00000000..0c7e8eb6 --- /dev/null +++ b/retrofit/src/main/kotlin/dev/appoutlet/some/retrofit/RetrofitResolverProvider.kt @@ -0,0 +1,17 @@ +package dev.appoutlet.some.retrofit + +import dev.appoutlet.some.core.Resolver +import dev.appoutlet.some.core.ResolverProvider +import dev.appoutlet.some.core.StrategyProvider +import dev.appoutlet.some.retrofit.resolver.ResponseResolver +import kotlin.random.Random + +/** + * Contributes Retrofit-specific resolvers discovered through [java.util.ServiceLoader]. + */ +class RetrofitResolverProvider : ResolverProvider { + override fun createResolvers( + strategyProvider: StrategyProvider, + random: Random, + ): List = listOf(ResponseResolver()) +} diff --git a/retrofit/src/main/kotlin/dev/appoutlet/some/retrofit/SomeErrorResponse.kt b/retrofit/src/main/kotlin/dev/appoutlet/some/retrofit/SomeErrorResponse.kt new file mode 100644 index 00000000..b8d9f061 --- /dev/null +++ b/retrofit/src/main/kotlin/dev/appoutlet/some/retrofit/SomeErrorResponse.kt @@ -0,0 +1,52 @@ +package dev.appoutlet.some.retrofit + +import dev.appoutlet.some.some +import okhttp3.Headers +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.ResponseBody.Companion.toResponseBody +import retrofit2.Response + +/** + * Generates an error Retrofit response with an empty error body. + * + * @param T Expected response body type for the endpoint under test. + * @param code Error HTTP status code. + * @param message HTTP status message. + * @param protocol HTTP protocol used by the raw response. + * @param headers HTTP headers used by the raw response. + * @param request Request associated with the raw response. + * @return An error Retrofit response with a `null` body and empty error body. + * @throws IllegalArgumentException If the generated response is successful. + */ +@JvmOverloads +inline fun someErrorResponse( + code: Int = 400, + message: String = "Bad Request", + protocol: Protocol = Protocol.HTTP_1_1, + headers: Headers = Headers.Builder().build(), + request: Request = defaultRequest(), +): Response { + val rawResponse = okhttp3.Response.Builder() + .code(code) + .message(message) + .protocol(protocol) + .headers(headers) + .request(request) + .build() + + return someErrorResponse(rawResponse) +} + +/** + * Generates an error Retrofit response with an empty error body and supplied raw response metadata. + * + * @param T Expected response body type for the endpoint under test. + * @param rawResponse Non-successful OkHttp response to wrap. + * @return An error Retrofit response with a `null` body and empty error body. + * @throws IllegalArgumentException If [rawResponse] is successful. + */ +inline fun someErrorResponse(rawResponse: okhttp3.Response): Response { + require(rawResponse.isSuccessful.not()) { "rawResponse must be an error response" } + return Response.error(some().toString().toResponseBody(), rawResponse) +} diff --git a/retrofit/src/main/kotlin/dev/appoutlet/some/retrofit/SomeResponse.kt b/retrofit/src/main/kotlin/dev/appoutlet/some/retrofit/SomeResponse.kt new file mode 100644 index 00000000..d6862f67 --- /dev/null +++ b/retrofit/src/main/kotlin/dev/appoutlet/some/retrofit/SomeResponse.kt @@ -0,0 +1,54 @@ +package dev.appoutlet.some.retrofit + +import dev.appoutlet.some.some +import okhttp3.Headers +import okhttp3.Protocol +import okhttp3.Request +import retrofit2.Response + +private const val DEFAULT_URL = "http://localhost/" + +/** + * Generates a successful Retrofit response containing a generated [T] body. + * + * @param T Type of the generated response body. + * @param code Successful HTTP status code. + * @param message HTTP status message. + * @param protocol HTTP protocol used by the raw response. + * @param headers HTTP headers used by the raw response. + * @param request Request associated with the raw response. + * @return A successful Retrofit response containing a generated body. + * @throws IllegalArgumentException If [code] is outside the successful HTTP status range. + */ +@JvmOverloads +inline fun someResponse( + code: Int = 200, + message: String = "OK", + protocol: Protocol = Protocol.HTTP_1_1, + headers: Headers = Headers.Builder().build(), + request: Request = defaultRequest(), +): Response { + val rawResponse = okhttp3.Response.Builder() + .code(code) + .message(message) + .protocol(protocol) + .headers(headers) + .request(request) + .build() + + return Response.success(some(), rawResponse) +} + +/** + * Generates a successful Retrofit response containing a generated [T] body and the supplied raw response metadata. + * + * @param T Type of the generated response body. + * @param rawResponse Successful OkHttp response to wrap. + * @return A successful Retrofit response containing a generated body. + * @throws IllegalArgumentException If [rawResponse] is not successful. + */ +inline fun someResponse(rawResponse: okhttp3.Response): Response = + Response.success(some(), rawResponse) + +@PublishedApi +internal fun defaultRequest(): Request = Request.Builder().url(DEFAULT_URL).build() diff --git a/retrofit/src/main/kotlin/dev/appoutlet/some/retrofit/resolver/ResponseResolver.kt b/retrofit/src/main/kotlin/dev/appoutlet/some/retrofit/resolver/ResponseResolver.kt new file mode 100644 index 00000000..bc608e7c --- /dev/null +++ b/retrofit/src/main/kotlin/dev/appoutlet/some/retrofit/resolver/ResponseResolver.kt @@ -0,0 +1,21 @@ +package dev.appoutlet.some.retrofit.resolver + +import dev.appoutlet.some.core.Resolver +import dev.appoutlet.some.core.ResolverChain +import retrofit2.Response +import kotlin.reflect.KType + +/** + * Resolves [Response] values by delegating their body type to the active resolver chain. + */ +class ResponseResolver : Resolver { + override fun canResolve(type: KType): Boolean = type.classifier == Response::class + + override fun resolve(type: KType, chain: ResolverChain): Any { + val bodyType = requireNotNull(type.arguments.firstOrNull()?.type) { + "Star projection not supported in Response" + } + + return Response.success(chain.resolve(bodyType)) + } +} diff --git a/retrofit/src/main/resources/META-INF/services/dev.appoutlet.some.core.ResolverProvider b/retrofit/src/main/resources/META-INF/services/dev.appoutlet.some.core.ResolverProvider new file mode 100644 index 00000000..3a3ccddc --- /dev/null +++ b/retrofit/src/main/resources/META-INF/services/dev.appoutlet.some.core.ResolverProvider @@ -0,0 +1 @@ +dev.appoutlet.some.retrofit.RetrofitResolverProvider diff --git a/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/RetrofitIntegrationTest.kt b/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/RetrofitIntegrationTest.kt new file mode 100644 index 00000000..18b1f374 --- /dev/null +++ b/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/RetrofitIntegrationTest.kt @@ -0,0 +1,19 @@ +package dev.appoutlet.some.retrofit + +import dev.appoutlet.some.some +import retrofit2.Response +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class RetrofitIntegrationTest { + data class User(val name: String, val age: Int) + + @Test + fun `top-level some discovers Retrofit Response resolver`() { + val result: Response = some() + + assertEquals(200, result.code()) + assertNotNull(result.body()) + } +} diff --git a/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/RetrofitResolverProviderTest.kt b/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/RetrofitResolverProviderTest.kt new file mode 100644 index 00000000..6630c3db --- /dev/null +++ b/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/RetrofitResolverProviderTest.kt @@ -0,0 +1,16 @@ +package dev.appoutlet.some.retrofit + +import dev.appoutlet.some.config.DefaultStrategyProvider +import dev.appoutlet.some.retrofit.resolver.ResponseResolver +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals + +class RetrofitResolverProviderTest { + @Test + fun `createResolvers returns the Retrofit resolver list`() { + val resolvers = RetrofitResolverProvider().createResolvers(DefaultStrategyProvider(), Random.Default) + + assertEquals(listOf(ResponseResolver::class), resolvers.map { it::class }) + } +} diff --git a/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/SomeErrorResponseTest.kt b/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/SomeErrorResponseTest.kt new file mode 100644 index 00000000..8614a775 --- /dev/null +++ b/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/SomeErrorResponseTest.kt @@ -0,0 +1,83 @@ +package dev.appoutlet.some.retrofit + +import okhttp3.Headers +import okhttp3.Protocol +import okhttp3.Request +import retrofit2.Response +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class SomeErrorResponseTest { + data class User(val name: String, val age: Int) + + @Test + fun `someErrorResponse generates an error response with empty error body`() { + val result: Response = someErrorResponse() + + assertEquals(400, result.code()) + assertEquals("Bad Request", result.message()) + assertNull(result.body()) + assertNotNull(result.errorBody()) + assertEquals("http://localhost/", result.raw().request.url.toString()) + } + + @Test + fun `someErrorResponse preserves custom response metadata`() { + val headers = Headers.Builder().add("X-Test", "value").build() + val request = Request.Builder().url("https://example.com/users").build() + + val result: Response = someErrorResponse( + code = 500, + message = "Server Error", + protocol = Protocol.HTTP_2, + headers = headers, + request = request, + ) + + assertEquals(500, result.code()) + assertEquals("Server Error", result.message()) + assertEquals(Protocol.HTTP_2, result.raw().protocol) + assertEquals(headers, result.headers()) + assertEquals(request, result.raw().request) + assertNull(result.body()) + assertNotNull(result.errorBody()) + } + + @Test + fun `someErrorResponse wraps a supplied raw response`() { + val rawResponse = okhttp3.Response.Builder() + .code(404) + .message("Not Found") + .protocol(Protocol.HTTP_1_1) + .request(Request.Builder().url("https://example.com/missing").build()) + .build() + + val result: Response = someErrorResponse(rawResponse) + + assertEquals(rawResponse, result.raw()) + assertEquals(404, result.code()) + assertNull(result.body()) + assertNotNull(result.errorBody()) + } + + @Test + fun `someErrorResponse rejects successful responses`() { + assertFailsWith { + someErrorResponse(code = 200) + } + + val successfulRawResponse = okhttp3.Response.Builder() + .code(200) + .message("OK") + .protocol(Protocol.HTTP_1_1) + .request(Request.Builder().url("http://localhost/").build()) + .build() + + assertFailsWith { + someErrorResponse(successfulRawResponse) + } + } +} diff --git a/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/SomeResponseTest.kt b/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/SomeResponseTest.kt new file mode 100644 index 00000000..3451514a --- /dev/null +++ b/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/SomeResponseTest.kt @@ -0,0 +1,89 @@ +package dev.appoutlet.some.retrofit + +import okhttp3.Headers +import okhttp3.Protocol +import okhttp3.Request +import retrofit2.Response +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class SomeResponseTest { + data class User(val name: String, val age: Int) + + @Test + fun `someResponse generates a successful response with generated body and defaults`() { + val result: Response = someResponse() + + assertTrue(result.isSuccessful) + assertEquals(200, result.code()) + assertEquals("OK", result.message()) + assertEquals(Protocol.HTTP_1_1, result.raw().protocol) + assertEquals(0, result.headers().size) + assertEquals("http://localhost/", result.raw().request.url.toString()) + assertNotNull(result.body()) + } + + @Test + fun `someResponse preserves custom response metadata`() { + val headers = Headers.Builder().add("X-Test", "value").build() + val request = Request.Builder().url("https://example.com/users").build() + + val result: Response = someResponse( + code = 201, + message = "Created", + protocol = Protocol.HTTP_2, + headers = headers, + request = request, + ) + + assertEquals(201, result.code()) + assertEquals("Created", result.message()) + assertEquals(Protocol.HTTP_2, result.raw().protocol) + assertEquals(headers, result.headers()) + assertEquals(request, result.raw().request) + assertNotNull(result.body()) + } + + @Test + fun `someResponse wraps a supplied raw response`() { + val rawResponse = okhttp3.Response.Builder() + .code(202) + .message("Accepted") + .protocol(Protocol.HTTP_1_1) + .headers(Headers.Builder().add("X-Test", "value").build()) + .request(Request.Builder().url("https://example.com").build()) + .build() + + val result: Response = someResponse(rawResponse) + + assertEquals(rawResponse, result.raw()) + assertEquals(202, result.code()) + assertNotNull(result.body()) + } + + @Test + fun `someResponse rejects a supplied unsuccessful raw response`() { + val rawResponse = okhttp3.Response.Builder() + .code(404) + .message("Not Found") + .protocol(Protocol.HTTP_1_1) + .request(Request.Builder().url("https://example.com/missing").build()) + .build() + + val exception = assertFailsWith { + someResponse(rawResponse) + } + + assertEquals("rawResponse must be successful response", exception.message) + } + + @Test + fun `someResponse rejects non-success response codes`() { + assertFailsWith { + someResponse(code = 404) + } + } +} diff --git a/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/resolver/ResponseResolverTest.kt b/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/resolver/ResponseResolverTest.kt new file mode 100644 index 00000000..a834c1ee --- /dev/null +++ b/retrofit/src/test/kotlin/dev/appoutlet/some/retrofit/resolver/ResponseResolverTest.kt @@ -0,0 +1,73 @@ +package dev.appoutlet.some.retrofit.resolver + +import dev.appoutlet.some.config.NullableStrategy +import dev.appoutlet.some.core.Resolver +import dev.appoutlet.some.core.ResolverChain +import retrofit2.Response +import kotlin.reflect.KType +import kotlin.reflect.typeOf +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertTrue + +class ResponseResolverTest { + private val resolver = ResponseResolver() + + @Test + fun `ResponseResolver generates Response by delegating body type to chain`() { + val resolvedTypes = mutableListOf() + val chain = ResolverChain( + listOf(TrackingResolver(typeOf(), "body", resolvedTypes)), + NullableStrategy.NullOnCircularReference, + ) + + val result = resolver.resolve(typeOf>(), chain) + + assertIs>(result) + assertEquals("body", result.body()) + assertEquals(listOf(typeOf()), resolvedTypes) + assertTrue(result.isSuccessful) + assertEquals(200, result.code()) + } + + @Test + fun `ResponseResolver canResolve detects Retrofit Response types`() { + assertTrue(resolver.canResolve(typeOf>())) + } + + @Test + fun `ResponseResolver rejects non-Response types`() { + assertFalse(resolver.canResolve(typeOf())) + assertFalse(resolver.canResolve(typeOf())) + assertFalse(resolver.canResolve(typeOf())) + } + + @Test + fun `ResponseResolver throws error on star projection`() { + assertFailsWith { + resolver.resolve( + typeOf>(), + ResolverChain( + emptyList(), + NullableStrategy.NullOnCircularReference, + ), + ) + } + } + + private class TrackingResolver( + private val supportedType: KType, + private val value: Any, + private val resolvedTypes: MutableList, + ) : Resolver { + override fun canResolve(type: KType): Boolean = type == supportedType + + override fun resolve(type: KType, chain: ResolverChain): Any { + resolvedTypes += type + return value + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 977d9726..3906e1a5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,7 @@ rootProject.name = "Some" include(":core") include(":android") include(":kotest") +include(":retrofit") kover { enableCoverage() diff --git a/zensical.toml b/zensical.toml index 93ae2b65..b342784c 100644 --- a/zensical.toml +++ b/zensical.toml @@ -31,6 +31,7 @@ nav = [ "android/color.md" ] }, "kotest.md", + "retrofit.md", "custom-factories.md", "custom-resolvers.md", { "API Reference" = "https://some.appoutlet.dev/reference/latest/" }