From 0eaedb352a9f7e2328005a4a85ffbbff0893cb27 Mon Sep 17 00:00:00 2001 From: Doris Lam Date: Mon, 6 Nov 2023 21:18:06 -0800 Subject: [PATCH] test: migrated get org to use ktor 2 test apparatus, tests currently use both 1 and 2 styles, replace functions as more gets migrated --- .circleci/config.yml | 1 + build.gradle.kts | 7 +- src/test/kotlin/org/openmbee/mms5/OrgAny.kt | 5 +- src/test/kotlin/org/openmbee/mms5/OrgRead.kt | 28 ++--- .../openmbee/mms5/util/BlazegraphBackend.kt | 106 ------------------ .../org/openmbee/mms5/util/FusekiBackend.kt | 66 ----------- .../kotlin/org/openmbee/mms5/util/Helper.kt | 13 ++- .../org/openmbee/mms5/util/RdfAssertions.kt | 18 +++ .../kotlin/org/openmbee/mms5/util/Requests.kt | 20 ++++ 9 files changed, 68 insertions(+), 196 deletions(-) delete mode 100644 src/test/kotlin/org/openmbee/mms5/util/BlazegraphBackend.kt delete mode 100644 src/test/kotlin/org/openmbee/mms5/util/FusekiBackend.kt diff --git a/.circleci/config.yml b/.circleci/config.yml index a9dc5bd2..f6c88e40 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,6 +41,7 @@ jobs: name: Build stack and run tests shell: /bin/bash command: | + cp src/main/resources/application.conf.test ./src/main/resources/application.conf docker-compose -f src/test/resources/docker-compose.yml up -d docker build --network=mms5-test-network -t mms5-test:latest -f Dockerfile-Test . docker run --network=mms5-test-network --name mms5-test-container mms5-test:latest diff --git a/build.gradle.kts b/build.gradle.kts index 8815a4f4..04cca8d0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") testImplementation("io.kotest:kotest-assertions-json-jvm:$kotestVersion") testImplementation("io.kotest:kotest-property:$kotestVersion") + testImplementation("io.kotest.extensions:kotest-assertions-ktor:2.0.0") val commonsCliVersion = "1.5.0" implementation("commons-cli:commons-cli:$commonsCliVersion") @@ -93,7 +94,6 @@ dependencies { tasks { test { useJUnitPlatform() - dependsOn("copy-test-fuseki-server") this.testLogging { this.showStandardStreams = true } @@ -104,11 +104,6 @@ tasks { if (System.getenv("MMS5_LOAD_SERVICE_URL") != null) environment("MMS5_LOAD_SERVICE_URL", System.getenv("MMS5_LOAD_SERVICE_URL")) } - register("copy-test-fuseki-server") { - // Copy fuseki-server jar to known location (build/test-fuseki-server) - from(testFuseki.resolvedConfiguration.files) - destinationDir = project.buildDir.resolve("test-fuseki-server") - } } tasks.test { finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run diff --git a/src/test/kotlin/org/openmbee/mms5/OrgAny.kt b/src/test/kotlin/org/openmbee/mms5/OrgAny.kt index a6fcaffe..3a0d038c 100644 --- a/src/test/kotlin/org/openmbee/mms5/OrgAny.kt +++ b/src/test/kotlin/org/openmbee/mms5/OrgAny.kt @@ -9,7 +9,6 @@ import org.openmbee.mms5.util.* import org.slf4j.LoggerFactory fun TriplesAsserter.validateOrgTriples( - createResponse: TestApplicationResponse, orgId: String, orgName: String, extraPatterns: List = listOf() @@ -35,7 +34,7 @@ fun TriplesAsserter.validateCreatedOrgTriples( orgName: String, extraPatterns: List = listOf() ) { - validateOrgTriples(createResponse, orgId, orgName, extraPatterns) + validateOrgTriples(orgId, orgName, extraPatterns) // auto policy matchOneSubjectTerseByPrefix("m-policy:AutoOrgOwner") { @@ -72,4 +71,4 @@ open class OrgAny: CommonSpec() { val validOrgBody = """ <> dct:title "$orgName"@en . """.trimIndent() -} \ No newline at end of file +} diff --git a/src/test/kotlin/org/openmbee/mms5/OrgRead.kt b/src/test/kotlin/org/openmbee/mms5/OrgRead.kt index a0c3f2fa..fb8bc657 100644 --- a/src/test/kotlin/org/openmbee/mms5/OrgRead.kt +++ b/src/test/kotlin/org/openmbee/mms5/OrgRead.kt @@ -1,6 +1,9 @@ package org.openmbee.mms5 +import io.kotest.assertions.ktor.client.shouldHaveETag +import io.kotest.assertions.ktor.client.shouldHaveStatus import io.ktor.http.* +import io.ktor.server.testing.* import org.openmbee.mms5.util.* import java.util.* @@ -34,16 +37,13 @@ class OrgRead : OrgAny() { } "get org" { - val create = createOrg(orgId, orgName) - - withTest { - httpGet(orgPath) {}.apply { - response shouldHaveStatus HttpStatusCode.OK - response.shouldHaveHeader(HttpHeaders.ETag, create.response.headers[HttpHeaders.ETag]!!) - - response includesTriples { - validateOrgTriples(response, orgId, orgName) - } + testApplication { + val created = createOrg(orgId, orgName) + val response = get(orgPath){} + response shouldHaveStatus HttpStatusCode.OK + response shouldHaveETag created.headers[HttpHeaders.ETag]!! + response includesTriples { + validateOrgTriples(orgId, orgName) } } } @@ -110,12 +110,12 @@ class OrgRead : OrgAny() { response.includesTriples { modelName = it - validateOrgTriples(createBase.response, orgId, orgName) - validateOrgTriples(createFoo.response, orgFooId, orgFooName) - validateOrgTriples(createBar.response, orgBarId, orgBarName) + validateOrgTriples(orgId, orgName) + validateOrgTriples(orgFooId, orgFooName) + validateOrgTriples(orgBarId, orgBarName) } } } } } -} \ No newline at end of file +} diff --git a/src/test/kotlin/org/openmbee/mms5/util/BlazegraphBackend.kt b/src/test/kotlin/org/openmbee/mms5/util/BlazegraphBackend.kt deleted file mode 100644 index ebdd2022..00000000 --- a/src/test/kotlin/org/openmbee/mms5/util/BlazegraphBackend.kt +++ /dev/null @@ -1,106 +0,0 @@ -package org.openmbee.mms5.util - -import com.sun.net.httpserver.HttpServer -import java.io.File -import java.net.InetSocketAddress -import java.net.URI -import java.net.http.HttpClient -import java.net.http.HttpRequest -import java.net.http.HttpResponse.BodyHandlers -import java.nio.charset.StandardCharsets -import java.util.* - -/** - * Runs a Blazegraph server in a separate process. Not used anymore and probably - * can be deleted, but kept here for reference. - */ -class BlazegraphBackend : SparqlBackend { - private val server = HttpServer.create() - private val id = UUID.randomUUID().toString() - private var started = false - - override fun start() { - if (started) { - throw IllegalStateException("Already started") - } - started = true - server.bind(InetSocketAddress(0), 16) - server.createContext("/") { exchange -> - exchange.responseBody.writer(StandardCharsets.UTF_8).use { writer -> - writer.write(id) - } - } - val cmd = listOf( - System.getProperty("java.home") + File.separator + "bin" + File.separator + "java", - "-cp", - System.getProperty("java.class.path"), - javaClass.name + "Kt", - id, - server.address.port.toString() - ) - val workingDir = File("build" + File.separator + "blazegraph") - if (!workingDir.exists()) { - workingDir.mkdirs() - } - File(workingDir, "blazegraph.jnl").delete() - File(workingDir, "rules.log").delete() - val p = ProcessBuilder(cmd) - .directory(workingDir) - .start() - val s = Scanner(p.inputStream) - println("Waiting for BlazeGraph...") - while (s.hasNext()) { - val line = s.nextLine() - println(line) - if (line.startsWith("Go to")) { - break - } - } - println("BlazeGraph Ready") - } - - override fun stop() { - server.stop(0) - } - - override fun getQueryUrl(): String { - return "http://localhost:9999/blazegraph/sparql" - } - - override fun getUpdateUrl(): String { - return getQueryUrl(); - } - - override fun getGspUrl(): String { - return getQueryUrl(); - } -} - -fun main(args: Array) { - val cmd = listOf( - System.getProperty("java.home") + File.separator + "bin" + File.separator + "java", - "-jar", - "/Users/gaspardo/Downloads/blazegraph.jar" - ) - println("[START BLAZEGRAPH]") - val p = ProcessBuilder(cmd) - .inheritIO() - .start() - try { - var client = HttpClient.newHttpClient() - while (true) { - val req = HttpRequest.newBuilder(URI("http://localhost:" + args[1])) - .GET() - .build() - var res = client.send(req, BodyHandlers.ofString()) - if (res.body().trim() != args[1]) { - throw IllegalStateException("Unexpected ID") - } - Thread.sleep(100) - } - } catch (e: Exception) { - println("[STOP BLAZEGRAPH BECAUSE ${e.message}]") - } finally { - p.destroy() - } -} \ No newline at end of file diff --git a/src/test/kotlin/org/openmbee/mms5/util/FusekiBackend.kt b/src/test/kotlin/org/openmbee/mms5/util/FusekiBackend.kt deleted file mode 100644 index d9115914..00000000 --- a/src/test/kotlin/org/openmbee/mms5/util/FusekiBackend.kt +++ /dev/null @@ -1,66 +0,0 @@ -package org.openmbee.mms5.util - -import java.io.File -import java.lang.reflect.Constructor -import java.lang.reflect.Method -import java.net.ServerSocket -import java.net.URLClassLoader - -/** - * Fuseki SPARQL backend. Runs fuseki-server JAR in an isolated ClassLoader using an in-memory dataset. - */ -class FusekiBackend : SparqlBackend { - - companion object { - private val datasetGraphInMemoryConstructor: Constructor<*> - private val makeFusekiMethod: Method - private val startFusekiMethod: Method - private val stopFusekiMethod: Method - - init { - val fusekiJarUrls = (File("build" + File.separator + "test-fuseki-server").listFiles() ?: arrayOf()) - .filter { it.extension.lowercase() == "jar" } - .map { it.toURI().toURL() } - .toTypedArray() - if (fusekiJarUrls.isEmpty()) { - throw IllegalStateException("No fuseki-server JAR Found; run gradle build to initialize") - } - val bootstrapClassLoader = ClassLoader.getSystemClassLoader().parent - val fusekiClassLoader = URLClassLoader(fusekiJarUrls, bootstrapClassLoader) - - val fusekiServerClass = fusekiClassLoader.loadClass("org.apache.jena.fuseki.main.FusekiServer") - makeFusekiMethod = fusekiServerClass.getMethod("make", Integer.TYPE, fusekiClassLoader.loadClass("java.lang.String"), fusekiClassLoader.loadClass("org.apache.jena.sparql.core.DatasetGraph")) - startFusekiMethod = fusekiServerClass.getMethod("start") - stopFusekiMethod = fusekiServerClass.getMethod("stop") - datasetGraphInMemoryConstructor = - fusekiClassLoader.loadClass("org.apache.jena.sparql.core.mem.DatasetGraphInMemory") - .getConstructor() - } - } - - /** Port to use for fuseki. Finds a random open port */ - private val fusekiPort = ServerSocket(0).use { it.localPort } - - /** FusekiServer object */ - private val fusekiServer = makeFusekiMethod.invoke(null, fusekiPort, "/ds", datasetGraphInMemoryConstructor.newInstance()); - - override fun start() { - startFusekiMethod.invoke(fusekiServer) - } - - override fun stop() { - stopFusekiMethod.invoke(fusekiServer) - } - - override fun getQueryUrl(): String { - return "http://localhost:${fusekiPort}/ds/query" - } - - override fun getUpdateUrl(): String { - return "http://localhost:${fusekiPort}/ds/update" - } - - override fun getGspUrl(): String { - return "http://localhost:${fusekiPort}/ds/data" - } -} \ No newline at end of file diff --git a/src/test/kotlin/org/openmbee/mms5/util/Helper.kt b/src/test/kotlin/org/openmbee/mms5/util/Helper.kt index 2dad53ed..560fc8ef 100644 --- a/src/test/kotlin/org/openmbee/mms5/util/Helper.kt +++ b/src/test/kotlin/org/openmbee/mms5/util/Helper.kt @@ -1,10 +1,21 @@ package org.openmbee.mms5.util +import io.kotest.assertions.ktor.client.shouldHaveStatus +import io.ktor.client.request.* +import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* import org.openmbee.mms5.* - +suspend fun ApplicationTestBuilder.createOrg(orgId: String, orgName: String): HttpResponse { + val response = put("/orgs/$orgId") { + setTurtleBody(""" + <> dct:title "$orgName"@en . + """.trimIndent()) + } + response shouldHaveStatus HttpStatusCode.OK + return response +} fun createOrg(orgId: String, orgName: String): TestApplicationCall { return withTest { httpPut("/orgs/$orgId") { diff --git a/src/test/kotlin/org/openmbee/mms5/util/RdfAssertions.kt b/src/test/kotlin/org/openmbee/mms5/util/RdfAssertions.kt index eb6e87ce..6fab3295 100644 --- a/src/test/kotlin/org/openmbee/mms5/util/RdfAssertions.kt +++ b/src/test/kotlin/org/openmbee/mms5/util/RdfAssertions.kt @@ -4,9 +4,11 @@ package org.openmbee.mms5.util import io.kotest.assertions.fail import io.kotest.assertions.json.shouldBeJsonObject import io.kotest.assertions.json.shouldEqualJson +import io.kotest.assertions.ktor.client.shouldHaveStatus import io.kotest.assertions.withClue import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldStartWith +import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.request.* import io.ktor.server.testing.* @@ -367,6 +369,22 @@ fun TestApplicationResponse.includesTriples(statusCode: HttpStatusCode, assertio infix fun TestApplicationResponse.includesTriples(assertions: TriplesAsserter.() -> Unit): TriplesAsserter { return includesTriples(HttpStatusCode.OK, assertions) } +suspend fun HttpResponse.includesTriples(statusCode: HttpStatusCode, assertions: TriplesAsserter.() -> Unit): TriplesAsserter { + this.shouldHaveStatus(statusCode) + + // assert content-type header (ignore charset if present) + this.headers[HttpHeaders.ContentType].shouldStartWith(RdfContentTypes.Turtle.contentType) + + // parse turtle into model + val model = ModelFactory.createDefaultModel() + parseTurtle(this.bodyAsText(), model, this.call.request.url.toString()) + + // make triple assertions and then assert the model is empty + return TriplesAsserter(model).apply { assertions() } +} +suspend infix fun HttpResponse.includesTriples(assertions: TriplesAsserter.() -> Unit): TriplesAsserter { + return includesTriples(HttpStatusCode.OK, assertions) +} fun TestApplicationResponse.exclusivelyHasTriples(statusCode: HttpStatusCode, assertions: TriplesAsserter.() -> Unit) { includesTriples(statusCode, assertions).assertEmpty() diff --git a/src/test/kotlin/org/openmbee/mms5/util/Requests.kt b/src/test/kotlin/org/openmbee/mms5/util/Requests.kt index 7d7a09e8..19d95d78 100644 --- a/src/test/kotlin/org/openmbee/mms5/util/Requests.kt +++ b/src/test/kotlin/org/openmbee/mms5/util/Requests.kt @@ -2,6 +2,8 @@ package org.openmbee.mms5.util import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm +import io.ktor.client.request.* +import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* import org.openmbee.mms5.ROOT_CONTEXT @@ -124,3 +126,21 @@ fun TestApplicationEngine.httpPatch(uri: String, setup: TestApplicationRequest.( fun TestApplicationEngine.httpDelete(uri: String, setup: TestApplicationRequest.() -> Unit): TestApplicationCall { return this.httpRequest(HttpMethod.Delete, uri, setup) } + +//ktor2 +fun HttpRequestBuilder.setTurtleBody(body: String) { + header("Content-Type", "text/turtle") + setBody(body) +} +suspend fun ApplicationTestBuilder.put(uri: String, setup: HttpRequestBuilder.() -> Unit): HttpResponse { + return client.put(uri) { + header("Authorization", authorization(rootAuth)) + setup() + } +} +suspend fun ApplicationTestBuilder.get(uri: String, setup: HttpRequestBuilder.() -> Unit): HttpResponse { + return client.get(uri) { + header("Authorization", authorization(rootAuth)) + setup() + } +}