diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index d4e7b48..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index e0844bc..62269df 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,5 +1,6 @@ + diff --git a/build.gradle.kts b/build.gradle.kts index c6bde77..1c78cad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,6 +38,14 @@ dependencies { implementation("net.pearx.kasechange:kasechange-jvm:1.1.0") implementation("fr.postgresjson:postgresjson:$postgresjson_version") testImplementation("io.ktor:ktor-server-tests:$ktor_version") + testImplementation("io.ktor:ktor-client-mock:$ktor_version") + testImplementation("io.ktor:ktor-client-mock-jvm:$ktor_version") + testImplementation("org.koin:koin-test:$koinVersion") + testImplementation("io.mockk:mockk:1.9") + testImplementation("org.junit.jupiter:junit-jupiter:5.5.0") + testImplementation("org.amshove.kluent:kluent:1.4") + testImplementation("io.cucumber:cucumber-java8:4.3.1") + testImplementation("io.cucumber:cucumber-junit:4.3.1") } kotlin.sourceSets["main"].kotlin.srcDirs("src") diff --git a/src/fr/dcproject/Module.kt b/src/fr/dcproject/Module.kt index fd93cf4..31627f9 100644 --- a/src/fr/dcproject/Module.kt +++ b/src/fr/dcproject/Module.kt @@ -6,9 +6,10 @@ import org.koin.dsl.module import java.io.File import fr.dcproject.repository.Article as ArticleRepository +val config = Config() + @KtorExperimentalAPI val Module = module { - val config = Config() single { config } diff --git a/src/fr/dcproject/repository/Article.kt b/src/fr/dcproject/repository/Article.kt index aa2188c..c9b38e0 100644 --- a/src/fr/dcproject/repository/Article.kt +++ b/src/fr/dcproject/repository/Article.kt @@ -21,10 +21,17 @@ class Article(override var requester: Requester) : RepositoryI { } } - fun find(page: Int = 1, limit: Int = 50, sort: String? = null, direction: Direction? = null, search: String? = null): Paginated { + fun find( + page: Int = 1, + limit: Int = 50, + sort: String? = null, + direction: Direction? = null, + search: String? = null + ): Paginated { return requester .getFunction("find_articles") - .select(page, limit, + .select( + page, limit, "sort" to sort?.toSnakeCase(), "direction" to direction, "search" to search diff --git a/test/RunCucumberTest.kt b/test/RunCucumberTest.kt new file mode 100644 index 0000000..de6c753 --- /dev/null +++ b/test/RunCucumberTest.kt @@ -0,0 +1,25 @@ +import cucumber.api.CucumberOptions +import cucumber.api.Scenario +import cucumber.api.java8.En +import cucumber.api.junit.Cucumber +import feature.Context +import io.ktor.server.testing.TestApplicationEngine +import io.ktor.server.testing.createTestEnvironment +import org.junit.runner.RunWith +import java.util.concurrent.TimeUnit +import feature.Context.Companion.current as contextCurrent + +@RunWith(Cucumber::class) +@CucumberOptions(plugin = ["pretty"]) +class RunCucumberTest: En { + init { + Before(-1) { scenario: Scenario -> +// config.database = "dc-projectg-test" + contextCurrent = Context(TestApplicationEngine(createTestEnvironment()) {}, scenario) + } + + After { scenario: Scenario -> + contextCurrent.engine.stop(0L, 0L, TimeUnit.MILLISECONDS) + } + } +} diff --git a/test/feature/Context.kt b/test/feature/Context.kt new file mode 100644 index 0000000..ab4555e --- /dev/null +++ b/test/feature/Context.kt @@ -0,0 +1,40 @@ +package feature + +import cucumber.api.Scenario +import fr.dcproject.module +import io.ktor.application.Application +import io.ktor.server.testing.TestApplicationCall +import io.ktor.server.testing.TestApplicationEngine +import io.ktor.server.testing.TestApplicationRequest + +class Context( + val engine: TestApplicationEngine, + val scenario: Scenario +) { + companion object { + lateinit var current: Context + } + + init { + engine.start() + val moduleFunction: Application.() -> Unit = { module() } + val test: TestApplicationEngine.() -> Unit = { + moduleFunction(application) + } + engine.test() + } + + var call: TestApplicationCall? = null + + private val requestContextConfigurations: MutableList Unit> = mutableListOf() + fun setupRequest(testApplicationRequest: TestApplicationRequest) { + requestContextConfigurations.forEach { + it(testApplicationRequest) + } + } + fun setupNextRequests(requestContextConfiguration: TestApplicationRequest.() -> Unit) = requestContextConfigurations.add(requestContextConfiguration) +} + +fun TestApplicationRequest.applyConfigurations() { + Context.current.setupRequest(this) +} \ No newline at end of file diff --git a/test/feature/Request.kt b/test/feature/Request.kt new file mode 100644 index 0000000..4c85e13 --- /dev/null +++ b/test/feature/Request.kt @@ -0,0 +1,92 @@ +package feature + +import com.google.gson.Gson +import cucumber.api.Scenario +import cucumber.api.java8.En +import io.cucumber.datatable.DataTable +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.server.testing.TestApplicationCall +import io.ktor.server.testing.TestApplicationEngine +import io.ktor.server.testing.setBody +import org.junit.jupiter.api.Assertions.assertEquals +import org.koin.test.KoinTest +import org.opentest4j.AssertionFailedError +import kotlin.test.asserter +import feature.Context.Companion.current as currentContext + +class Request: En, KoinTest { + init { + Before { scenario: Scenario -> + } + + After { scenario: Scenario -> + } + + When("I send a {string} request to {string} with body:") { method: String, uri: String, body: String -> + val test: TestApplicationEngine.() -> Unit = { + currentContext.call = handleRequest { + applyConfigurations() + addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + this.method = HttpMethod.parse(method) + this.uri = uri + setBody(body) + } + } + + currentContext.engine.test() + } + + When("I send a {string} request to {string}") { method: String, uri: String -> + val test: TestApplicationEngine.() -> Unit = { + currentContext.call = handleRequest { + applyConfigurations() + addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + this.method = HttpMethod.parse(method.toUpperCase()) + this.uri = uri + } + } + + currentContext.engine.test() + } + + Then("the response status code should be {int}") { statusCode: Int -> + val call: TestApplicationCall = currentContext.call ?: throw AssertionFailedError("No call", statusCode, null) + with(call) { + assertEquals(HttpStatusCode.fromValue(statusCode), response.status()) + } + } + + And("the response should contain:") { expected: DataTable -> + val call: TestApplicationCall = currentContext.call ?: throw AssertionFailedError("No call") + val p = call.response + val response = Gson().fromJson>>(p.content, List::class.java) + + expected.asMap(String::class.java, String::class.java).forEach { (key, value) -> + response.forEach { + if (it.containsKey(key)) { + assertEquals(it[key], value) + return@And + } + } + asserter.fail("The response not contain $key field") + } + } + + And("the response should contain object:") { expected: DataTable -> + val call: TestApplicationCall = currentContext.call ?: throw AssertionFailedError("No call") + val p = call.response + val response = Gson().fromJson>(p.content, Map::class.java) + + expected.asMap(String::class.java, String::class.java).forEach { (key, value) -> + if (response.containsKey(key)) { + assertEquals(value, response[key]) + return@And + } + asserter.fail("The response not contain $key field") + } + } + } +} \ No newline at end of file diff --git a/testresources/feature/articles.feature b/testresources/feature/articles.feature new file mode 100644 index 0000000..65a82d6 --- /dev/null +++ b/testresources/feature/articles.feature @@ -0,0 +1,36 @@ +Feature: articles routes + + Scenario: The route for get articles must response a 200 + When I send a "GET" request to "/articles" + Then the response status code should be 200 + + Scenario: The route for get article must response a 200 + When I send a "GET" request to "/articles/55a24426-139b-4ee7-b1e2-a3d016d66cc2" + Then the response status code should be 200 + + Scenario: The route for get article must response a 200 + When I send a "POST" request to "/articles" with body: + """ + { + "version_id": "09c418b6-63ba-448b-b38b-502b41cd500e", + "title": "title2", + "annonymous": false, + "content": "content2", + "description": "description2", + "tags": [ + "green" + ], + "created_by": { + "id": "64b7b379-2298-43ec-b428-ba134930cabd" + } + } + """ + Then the response status code should be 200 + And the response should contain object: + | version_id | 09c418b6-63ba-448b-b38b-502b41cd500e | + | title | title2 | + When I send a "GET" request to "/articles/99afd1b1-3555-43c1-80a7-63c56e93d250" + Then the response status code should be 200 + And the response should contain object: + | id | 99afd1b1-3555-43c1-80a7-63c56e93d250 | + | title | title2 | \ No newline at end of file