diff --git a/build.gradle.kts b/build.gradle.kts index bb93a733..c2a65539 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ plugins { id("com.github.breadmoirai.github-release") id("org.sonarqube") id("io.github.gradle-nexus.publish-plugin") - + id("org.jetbrains.kotlinx.kover") kotlin("jvm") apply (false) id("org.cadixdev.licenser") apply (false) @@ -47,11 +47,11 @@ allprojects { configure(moduleNames.map { project(":sunday-$it") }) { apply(plugin = "java-library") - apply(plugin = "jacoco") apply(plugin = "maven-publish") apply(plugin = "signing") apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "org.jetbrains.kotlinx.kover") apply(plugin = "org.jetbrains.dokka") apply(plugin = "org.cadixdev.licenser") apply(plugin = "org.jmailen.kotlinter") @@ -105,10 +105,6 @@ configure(moduleNames.map { project(":sunday-$it") }) { // TEST // - configure { - toolVersion = "0.8.12" - } - tasks.named("test").configure { useJUnitPlatform() @@ -120,8 +116,6 @@ configure(moduleNames.map { project(":sunday-$it") }) { } reports.junitXml.required.set(true) - - finalizedBy("jacocoTestReport") } @@ -284,7 +278,7 @@ configure(moduleNames.map { project(":sunday-$it") }) { property("sonar.jacoco.reportPaths", "") property( "sonar.coverage.jacoco.xmlReportPaths", - "$rootDir/code-coverage/build/reports/jacoco/testCoverageReport/testCoverageReport.xml", + "$rootDir/code-coverage/build/reports/kover/report.xml", ) } } diff --git a/code-coverage/build.gradle.kts b/code-coverage/build.gradle.kts index caf38f3a..26567c61 100644 --- a/code-coverage/build.gradle.kts +++ b/code-coverage/build.gradle.kts @@ -1,7 +1,7 @@ plugins { base - id("jacoco-report-aggregation") + id("org.jetbrains.kotlinx.kover") } repositories { @@ -9,26 +9,15 @@ repositories { } dependencies { - jacocoAggregation(project(":sunday-core")) - jacocoAggregation(project(":sunday-jdk")) - jacocoAggregation(project(":sunday-okhttp")) -} - -reporting { - reports { - create("testCoverageReport") { - testType.set(TestSuiteType.UNIT_TEST) - reportTask { - reports.xml.required.set(true) - } - } - } + kover(project(":sunday-core")) + kover(project(":sunday-jdk")) + kover(project(":sunday-okhttp")) } tasks { check { - finalizedBy(named("testCoverageReport")) + finalizedBy(named("koverXmlReport"), named("koverHtmlReport")) } } diff --git a/core/src/main/kotlin/io/outfoxx/sunday/RequestFactory.kt b/core/src/main/kotlin/io/outfoxx/sunday/RequestFactory.kt index 3e60ea25..73e51767 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/RequestFactory.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/RequestFactory.kt @@ -599,7 +599,7 @@ abstract class RequestFactory : Closeable { response.contentType?.let { MediaType.from(it.toString()) } ?: throw SundayError( SundayError.Reason.InvalidContentType, - response.contentType?.value ?: "", + response.contentType?.value ?: "", ) val contentTypeDecoder = diff --git a/core/src/main/kotlin/io/outfoxx/sunday/SundayError.kt b/core/src/main/kotlin/io/outfoxx/sunday/SundayError.kt index 97763775..dfc26021 100644 --- a/core/src/main/kotlin/io/outfoxx/sunday/SundayError.kt +++ b/core/src/main/kotlin/io/outfoxx/sunday/SundayError.kt @@ -34,7 +34,7 @@ class SundayError( ResponseDecodingFailed("Response decoding failed"), EventDecodingFailed("Event decoding failed"), InvalidBaseUri("Base URL is invalid after expanding template"), - NoSupportedContentTypes("None of the provided Content-Types for the request has a registered decoder"), + NoSupportedContentTypes("None of the provided Content-Types for the request has a registered encoder"), NoSupportedAcceptTypes("None of the provided Accept types for the request has a registered decoder"), InvalidHeaderValue("The encoded header value contains one or more invalid characters"), } diff --git a/core/src/test/kotlin/HeaderExtensionsTest.kt b/core/src/test/kotlin/HeaderExtensionsTest.kt new file mode 100644 index 00000000..86399e61 --- /dev/null +++ b/core/src/test/kotlin/HeaderExtensionsTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2020 Outfox, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.outfoxx.sunday.MediaType +import io.outfoxx.sunday.http.HeaderParameters +import io.outfoxx.sunday.http.getAll +import io.outfoxx.sunday.http.getFirst +import io.outfoxx.sunday.http.toMultiMap +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.containsInAnyOrder +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.oneOf +import org.junit.jupiter.api.Test + +class HeaderExtensionsTest { + + @Test + fun `test getFirst`() { + val headers = HeaderParameters.encode(mapOf("test" to arrayOf(MediaType.JSON, MediaType.CBOR))) + + assertThat( + headers.getFirst("test"), + `is`(oneOf(MediaType.JSON.value, MediaType.CBOR.value)), + ) + } + + @Test + fun `test getAll`() { + val headers = HeaderParameters.encode(mapOf("test" to arrayOf(MediaType.JSON, MediaType.CBOR))) + + assertThat( + headers.getAll("test"), + containsInAnyOrder(MediaType.JSON.value, MediaType.CBOR.value), + ) + } + + @Test + fun `test toMultiMap`() { + val headers = HeaderParameters.encode(mapOf("test" to arrayOf(MediaType.JSON, MediaType.CBOR))) + + assertThat( + headers.toMultiMap(), + equalTo(mapOf("test" to listOf(MediaType.JSON.value, MediaType.CBOR.value))), + ) + } + +} diff --git a/core/src/test/kotlin/MediaTypeCodecTest.kt b/core/src/test/kotlin/MediaTypeCodecTest.kt index def0d3e1..c41744f9 100644 --- a/core/src/test/kotlin/MediaTypeCodecTest.kt +++ b/core/src/test/kotlin/MediaTypeCodecTest.kt @@ -14,8 +14,13 @@ * limitations under the License. */ +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper +import io.outfoxx.sunday.MediaType import io.outfoxx.sunday.mediatypes.codecs.BinaryDecoder import io.outfoxx.sunday.mediatypes.codecs.BinaryEncoder +import io.outfoxx.sunday.mediatypes.codecs.MediaTypeDecoders +import io.outfoxx.sunday.mediatypes.codecs.MediaTypeEncoders import io.outfoxx.sunday.mediatypes.codecs.TextDecoder import io.outfoxx.sunday.mediatypes.codecs.TextEncoder import io.outfoxx.sunday.mediatypes.codecs.decode @@ -27,6 +32,9 @@ import okio.Source import okio.buffer import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.not +import org.hamcrest.Matchers.nullValue import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -142,4 +150,29 @@ class MediaTypeCodecTest { } } + @Test + fun `test encoders builder registers specific codecs`() { + val encoders = + MediaTypeEncoders + .Builder() + .registerJSON(JsonMapper()) + .registerCBOR(CBORMapper()) + .build() + + assertThat(encoders.find(MediaType.JSON), `is`(not(nullValue()))) + assertThat(encoders.find(MediaType.CBOR), `is`(not(nullValue()))) + } + + @Test + fun `test decoders builder registers specific codecs`() { + val decoders = + MediaTypeDecoders + .Builder() + .registerJSON(JsonMapper()) + .registerCBOR(CBORMapper()) + .build() + + assertThat(decoders.find(MediaType.JSON), `is`(not(nullValue()))) + assertThat(decoders.find(MediaType.CBOR), `is`(not(nullValue()))) + } } diff --git a/core/src/test/kotlin/MediaTypeTest.kt b/core/src/test/kotlin/MediaTypeTest.kt index 3e9a4aa2..df01f776 100644 --- a/core/src/test/kotlin/MediaTypeTest.kt +++ b/core/src/test/kotlin/MediaTypeTest.kt @@ -407,4 +407,11 @@ class MediaTypeTest { assertFalse(htmlWithCharset.compatible(MediaType.JSONStructured)) assertTrue(htmlWithCharset.compatible(MediaType.Any)) } + + @Test + fun `test constructor`() { + val mediaType = MediaType(Application, Vendor, "test", Zip, "charset" to "utf-8") + + assertEquals(mediaType.value, "application/vnd.test+zip;charset=utf-8") + } } diff --git a/core/src/test/kotlin/ProblemsTest.kt b/core/src/test/kotlin/ProblemsTest.kt new file mode 100644 index 00000000..3c4b4ac3 --- /dev/null +++ b/core/src/test/kotlin/ProblemsTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2020 Outfox, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.outfoxx.sunday.http.Headers +import io.outfoxx.sunday.http.Request +import io.outfoxx.sunday.http.Response +import io.outfoxx.sunday.utils.Problems +import okio.BufferedSource +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.jupiter.api.Test +import org.zalando.problem.Status + +class ProblemsTest { + + @Test + fun `test forResponse`() { + val problem = Problems.forResponse(TestResponse(400, "Bad Request", listOf(), null)) + + assertThat(problem.status, equalTo(Status.BAD_REQUEST)) + } + + @Test + fun `test forStatus`() { + val problem = Problems.forStatus(400, "Bad Request") + + assertThat(problem.status, equalTo(Status.BAD_REQUEST)) + } + + @Test + fun `test forStatus supports non-standard values`() { + val problem = Problems.forStatus(195, "AI Thinking") + + assertThat(problem.status?.statusCode, equalTo(195)) + assertThat(problem.status?.reasonPhrase, equalTo("AI Thinking")) + } + + data class TestResponse( + override val statusCode: Int, + override val reasonPhrase: String?, + override val headers: Headers, + override val body: BufferedSource?, + ) : Response { + + override val trailers: Headers? + get() = null + override val request: Request + get() = TODO("Not yet implemented") + } + +} diff --git a/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/RequestFactoryTest.kt b/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/RequestFactoryTest.kt index 386d90da..2001783d 100644 --- a/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/RequestFactoryTest.kt +++ b/core/src/testFixtures/kotlin/io/outfoxx/sunday/test/RequestFactoryTest.kt @@ -23,6 +23,7 @@ import io.outfoxx.sunday.MediaType.Companion.CBOR import io.outfoxx.sunday.MediaType.Companion.EventStream import io.outfoxx.sunday.MediaType.Companion.HTML import io.outfoxx.sunday.MediaType.Companion.JSON +import io.outfoxx.sunday.MediaType.Companion.Plain import io.outfoxx.sunday.MediaType.Companion.Problem import io.outfoxx.sunday.MediaType.Companion.WWWFormUrlEncoded import io.outfoxx.sunday.RequestFactory @@ -355,6 +356,42 @@ abstract class RequestFactoryTest { } } + @Test + fun `fetches typed results with body`() { + data class Tester( + val name: String, + val count: Int, + ) + + val tester = Tester("Test", 10) + + val server = MockWebServer() + server.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader(ContentType, JSON) + .setBody(objectMapper.writeValueAsString(tester)), + ) + server.start() + server.use { + createRequestFactory(URITemplate(server.url("/").toString())) + .use { requestFactory -> + + val result = + runBlocking { + requestFactory.resultResponse( + Method.Get, + "", + body = null, + ) + } + + assertThat(result.headers, hasItem(ContentType to "application/json")) + assertThat(result.result, equalTo(tester)) + } + } + } + @Test fun `executes requests with empty responses`() { val server = MockWebServer() @@ -400,6 +437,30 @@ abstract class RequestFactoryTest { } } + @Test + fun `executes manual requests with body for responses`() { + val server = MockWebServer() + server.enqueue( + MockResponse() + .setResponseCode(200) + .setHeader(ContentType, JSON) + .setBody("[]"), + ) + server.start() + server.use { + createRequestFactory(URITemplate(server.url("/").toString())) + .use { requestFactory -> + + val response = + runBlocking { + requestFactory.response(Method.Get, "", body = null, contentTypes = listOf(Plain)) + } + + assertThat(response.body?.readByteArray(), equalTo("[]".encodeToByteArray())) + } + } + } + @Test fun `error responses with non standard status codes are handled`() { val server = MockWebServer() @@ -417,7 +478,7 @@ abstract class RequestFactoryTest { val problem = assertThrows { runBlocking { - requestFactory.result>(Method.Get, "") + requestFactory.result>(Method.Get, "", body = null) } } @@ -479,6 +540,32 @@ abstract class RequestFactoryTest { } } + @Test + fun `fails when response content-type is missing`() { + val server = MockWebServer() + server.enqueue( + MockResponse() + .setResponseCode(200) + .setBody("some stuff"), + ) + server.start() + server.use { + createRequestFactory(URITemplate(server.url("/").toString())) + .use { requestFactory -> + + val error = + assertThrows { + runBlocking { + requestFactory.result>(Method.Get, "") + } + } + + assertThat(error.reason, equalTo(SundayError.Reason.InvalidContentType)) + assertThat(error.message, containsString("")) + } + } + } + @Test fun `fails when response content-type is invalid`() { val server = MockWebServer() @@ -501,6 +588,7 @@ abstract class RequestFactoryTest { } assertThat(error.reason, equalTo(SundayError.Reason.InvalidContentType)) + assertThat(error.message, containsString("bad/x-unknown")) } } } @@ -795,6 +883,40 @@ abstract class RequestFactoryTest { } } + @Test + fun `builds event sources with explicit body`() { + val encodedEvent = "event: hello\nid: 12345\ndata: Hello World!\n\n" + + val server = MockWebServer() + server.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader(ContentType, EventStream) + .setBody(encodedEvent), + ) + server.start() + server.use { + createRequestFactory(URITemplate(server.url("/").toString())) + .use { requestFactory -> + + runBlocking { + withTimeout(5000) { + val eventSource = requestFactory.eventSource(Method.Get, "", body = null) + eventSource.use { + suspendCancellableCoroutine { continuation -> + eventSource.onMessage = { _ -> + continuation.resume(Unit) + } + eventSource.connect() + } + + } + } + } + } + } + } + @Test fun `builds event streams`() { val encodedEvent = "event: hello\nid: 12345\ndata: {\"target\":\"world\"}\n\n" @@ -838,6 +960,50 @@ abstract class RequestFactoryTest { } } + @Test + fun `builds event streams with explicit body`() { + val encodedEvent = "event: hello\nid: 12345\ndata: {\"target\":\"world\"}\n\n" + + val server = MockWebServer() + server.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader(ContentType, EventStream) + .setBody(encodedEvent), + ) + server.start() + server.use { + createRequestFactory(URITemplate(server.url("/").toString())) + .use { requestFactory -> + + val result = + runBlocking { + withTimeout(50000) { + val eventStream = + requestFactory.eventStream>( + Method.Get, + "", + body = null, + decoder = { decoder, event, _, data, logger -> + when (event) { + "hello" -> decoder.decode>(data, typeOf>()) + else -> { + logger.error("unsupported event type") + null + } + } + }, + ) + + eventStream.first() + } + } + + assertThat(result, hasEntry("target", "world")) + } + } + } + class TestProblem( @JsonProperty("extra") val extra: String, instance: URI? = null, diff --git a/gradle.properties b/gradle.properties index ca40a2b4..4c9c913a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,7 @@ kotlinVersion=1.9 javaVersion=11 kotlinPluginVersion=1.9.20 +koverPluginVersion=0.8.3 dokkaPluginVersion=1.9.20 licenserPluginVersion=0.6.1 kotlinterPluginVersion=4.4.1 diff --git a/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/ReasonPhrasesTest.kt b/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/ReasonPhrasesTest.kt new file mode 100644 index 00000000..8a0050e7 --- /dev/null +++ b/jdk/src/test/kotlin/io/outfoxx/sunday/jdk/ReasonPhrasesTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Outfox, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.outfoxx.sunday.jdk + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.nullValue +import org.junit.jupiter.api.Test + +class ReasonPhrasesTest { + + @Test + fun `test lookups`() { + assertThat(ReasonPhrases.lookup(99), `is`(nullValue())) + assertThat(ReasonPhrases.lookup(100), equalTo("Continue")) + assertThat(ReasonPhrases.lookup(200), equalTo("OK")) + assertThat(ReasonPhrases.lookup(300), equalTo("Multiple Choices")) + assertThat(ReasonPhrases.lookup(400), equalTo("Bad Request")) + assertThat(ReasonPhrases.lookup(500), equalTo("Server Error")) + assertThat(ReasonPhrases.lookup(600), `is`(nullValue())) + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 9de8a555..1e86c270 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,7 @@ pluginManagement { } val kotlinPluginVersion: String by settings + val koverPluginVersion: String by settings val dokkaPluginVersion: String by settings val licenserPluginVersion: String by settings val kotlinterPluginVersion: String by settings @@ -17,6 +18,7 @@ pluginManagement { plugins { kotlin("jvm") version kotlinPluginVersion + id("org.jetbrains.kotlinx.kover") version koverPluginVersion id("org.jetbrains.dokka") version dokkaPluginVersion id("org.cadixdev.licenser") version licenserPluginVersion id("org.jmailen.kotlinter") version kotlinterPluginVersion