From 07ec732c5562a97559ccfefd130e6496541be833 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sun, 14 Jun 2020 23:06:28 +0200 Subject: [PATCH] More tests ! --- api/pom.xml | 17 +++ api/src/entities/User.kt | 2 +- .../extensions/ApplicationCallExtensions.kt | 5 +- api/src/features/ContentNegotiationFeature.kt | 7 +- api/src/routing/UserController.kt | 22 ++- api/src/services/UserService.kt | 8 +- api/test/routing/UserControllerKtTest.kt | 142 ++++++++++++++++++ api/test/utils/Assertions.kt | 19 +++ api/test/utils/JsonAssertExtensions.kt | 23 +++ api/test/utils/KtorTestingExtensions.kt | 52 +++++++ 10 files changed, 281 insertions(+), 16 deletions(-) create mode 100644 api/test/routing/UserControllerKtTest.kt create mode 100644 api/test/utils/Assertions.kt create mode 100644 api/test/utils/JsonAssertExtensions.kt create mode 100644 api/test/utils/KtorTestingExtensions.kt diff --git a/api/pom.xml b/api/pom.xml index 4cc60f5..40e5d0b 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -129,6 +129,11 @@ ktorm-support-mysql ${ktorm_version} + + me.liuwj.ktorm + ktorm-jackson + ${ktorm_version} + com.github.hekeki huckleberry @@ -179,6 +184,18 @@ 1.61 test + + org.skyscreamer + jsonassert + 1.5.0 + test + + + io.mockk + mockk + 1.10.0 + test + ${project.basedir}/src diff --git a/api/src/entities/User.kt b/api/src/entities/User.kt index aacb825..0400eb2 100644 --- a/api/src/entities/User.kt +++ b/api/src/entities/User.kt @@ -12,4 +12,4 @@ interface User : Entity { var password: String var createdAt: LocalDateTime var lastLogin: LocalDateTime? -} \ No newline at end of file +} diff --git a/api/src/extensions/ApplicationCallExtensions.kt b/api/src/extensions/ApplicationCallExtensions.kt index 97abb62..a170e75 100644 --- a/api/src/extensions/ApplicationCallExtensions.kt +++ b/api/src/extensions/ApplicationCallExtensions.kt @@ -1,19 +1,16 @@ package be.vandewalleh.extensions import be.vandewalleh.auth.UserDbIdPrincipal -import be.vandewalleh.kodein import be.vandewalleh.services.FullNoteCreateDTO import be.vandewalleh.services.FullNotePatchDTO -import be.vandewalleh.services.UserService import io.ktor.application.* import io.ktor.auth.* import io.ktor.http.* import io.ktor.request.* import io.ktor.response.* -import org.kodein.di.generic.instance suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) { - respond(status, status.description) + respond(status, """{"msg": "${status.description}"}""") } /** diff --git a/api/src/features/ContentNegotiationFeature.kt b/api/src/features/ContentNegotiationFeature.kt index df99c0a..fe892c5 100644 --- a/api/src/features/ContentNegotiationFeature.kt +++ b/api/src/features/ContentNegotiationFeature.kt @@ -3,9 +3,12 @@ package be.vandewalleh.features import io.ktor.application.* import io.ktor.features.* import io.ktor.jackson.* +import me.liuwj.ktorm.jackson.* fun Application.contentNegotiationFeature() { install(ContentNegotiation) { - jackson {} + jackson { + registerModule(KtormModule()) + } } -} \ No newline at end of file +} diff --git a/api/src/routing/UserController.kt b/api/src/routing/UserController.kt index 05245ae..bbdbea9 100644 --- a/api/src/routing/UserController.kt +++ b/api/src/routing/UserController.kt @@ -1,5 +1,6 @@ package be.vandewalleh.routing +import be.vandewalleh.entities.User import be.vandewalleh.extensions.respondStatus import be.vandewalleh.extensions.userId import be.vandewalleh.services.UserService @@ -12,16 +13,22 @@ import io.ktor.routing.* import org.kodein.di.Kodein import org.kodein.di.generic.instance import org.mindrot.jbcrypt.BCrypt +import java.time.LocalDateTime fun Routing.user(kodein: Kodein) { val userService by kodein.instance() + post("/user/test") { + val user = call.receive() + call.respond(user) + } + route("/user") { post { - val user = call.receive() + val user = call.receive() if (userService.userExists(user.username, user.email)) - return@post call.respond(HttpStatusCode.Conflict) + return@post call.respondStatus(HttpStatusCode.Conflict) val hashedPassword = BCrypt.hashpw(user.password, BCrypt.gensalt()) @@ -32,7 +39,7 @@ fun Routing.user(kodein: Kodein) { authenticate { put { - val user = call.receive() + val user = call.receive() if (userService.userExists(user.username, user.email)) return@put call.respond(HttpStatusCode.Conflict) @@ -45,12 +52,13 @@ fun Routing.user(kodein: Kodein) { } delete { - userService.deleteUser(call.userId()) - call.respondStatus(HttpStatusCode.OK) + val status = if (userService.deleteUser(call.userId())) + HttpStatusCode.OK + else + HttpStatusCode.NotFound + call.respondStatus(status) } } } } - -private data class UserDto(val username: String, val email: String, val password: String) diff --git a/api/src/services/UserService.kt b/api/src/services/UserService.kt index 8a5f05a..fc93555 100644 --- a/api/src/services/UserService.kt +++ b/api/src/services/UserService.kt @@ -98,9 +98,13 @@ class UserService(override val kodein: Kodein) : KodeinAware { } } - fun deleteUser(userId: Int) { + fun deleteUser(userId: Int): Boolean { db.useTransaction { - db.delete(Users) { it.id eq userId } + return when (db.delete(Users) { it.id eq userId }) { + 1 -> true + 0 -> false + else -> error("??") + } } } } diff --git a/api/test/routing/UserControllerKtTest.kt b/api/test/routing/UserControllerKtTest.kt new file mode 100644 index 0000000..8e521fc --- /dev/null +++ b/api/test/routing/UserControllerKtTest.kt @@ -0,0 +1,142 @@ +package routing + +import be.vandewalleh.auth.SimpleJWT +import be.vandewalleh.entities.User +import be.vandewalleh.mainModule +import be.vandewalleh.module +import be.vandewalleh.services.UserService +import io.ktor.http.* +import io.ktor.server.testing.* +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.* +import org.junit.jupiter.api.* +import org.kodein.di.Kodein +import org.kodein.di.generic.bind +import org.kodein.di.generic.instance +import utils.* +import java.time.LocalDateTime + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class UserControllerKtTest { + + private val userService = mockk() + + init { + // new user + every { userService.userExists("new", "new@test.com") } returns false + every { userService.createUser("new", "new@test.com", any()) } returns User { + this.createdAt = LocalDateTime.now() + this.username = "new" + this.email = "new@test.com" + } + + // existing user + every { userService.userExists("existing", "existing@test.com") } returns true + every { userService.createUser("existing", "existing@test.com", any()) } returns null + every { userService.getUserId("existing@test.com") } returns 1 + every { userService.deleteUser(1) } returns true andThen false + + // modified user + every { userService.userExists("modified", "modified@test.com") } returns true + every { + userService.userExists( + and(not("modified"), not("existing")), + and(not("modified@test.com"), not("existing@test.com")) + ) + } returns false + every { userService.userExists(1) } returns true + every { userService.createUser("modified", "modified@test.com", any()) } returns null + every { userService.getUserId("modified@test.com") } returns 1 + every { userService.updateUser(1, "ThisIsMyNewName", "ThisIsMyNewName@mail.com", any()) } returns Unit + + } + + + private val kodein = Kodein { + import(mainModule, allowOverride = true) + bind(overrides = true) with instance(userService) + } + + private val testEngine = TestApplicationEngine().apply { + start() + application.module(kodein) + } + + @Nested + inner class CreateUser { + @Test + fun `create a new user`() { + val res = testEngine.post("/user") { + json { + it["username"] = "new" + it["password"] = "test" + it["email"] = "new@test.com" + } + } + res.status() `should be equal to` HttpStatusCode.Created + res.content `should be equal to json` """{msg:"Created"}""" + } + + @Test + fun `create an existing user`() { + val res = testEngine.post("/user") { + json { + it["username"] = "existing" + it["email"] = "existing@test.com" + it["password"] = "test" + } + } + res.status() `should be equal to` HttpStatusCode.Conflict + res.content `should be equal to json` """{msg:"Conflict"}""" + } + } + + + @Nested + inner class DeleteUser { + + @Test + fun `delete an existing user`() { + val authJwt by kodein.instance("auth") + val token = authJwt.sign(1) + + val res = testEngine.delete("/user") { + addHeader(HttpHeaders.Authorization, "Bearer $token") + } + res.status() `should be equal to` HttpStatusCode.OK + res.content `should be equal to json` """{msg:"OK"}""" + + // try again + val res2 = testEngine.delete("/user") { + setToken(token) + } + res2.status() `should be equal to` HttpStatusCode.NotFound + res2.content `should be equal to json` """{msg:"Not Found"}""" + } + } + + @Nested + inner class ModifyUser { + + @Test + fun `modify a user`() { + val authJwt by kodein.instance("auth") + val token = authJwt.sign(1) + + val res = testEngine.put("/user") { + setToken(token) + json { + it["username"] = "ThisIsMyNewName" + it["email"] = "ThisIsMyNewName@mail.com" + } + } + + res.status() `should be equal to` HttpStatusCode.OK + res.content `should be equal to json` """{msg:"OK"}""" + + } + } + + +} diff --git a/api/test/utils/Assertions.kt b/api/test/utils/Assertions.kt new file mode 100644 index 0000000..6b9bad8 --- /dev/null +++ b/api/test/utils/Assertions.kt @@ -0,0 +1,19 @@ +package utils + +import org.skyscreamer.jsonassert.JSONAssert + +infix fun String?.shouldBeEqualToJson(expected: String?) = JSONAssert.assertEquals(expected, this, false) + +infix fun String?.`should be equal to json`(expected: String?) = shouldBeEqualToJson(expected) + +infix fun String?.shouldStrictlyBeEqualToJson(expected: String?) = JSONAssert.assertEquals(expected, this, true) + +infix fun String?.`should strictly be equal to json`(expected: String?) = shouldStrictlyBeEqualToJson(expected) + +infix fun String?.shouldNotStrictlyBeEqualToJson(expected: String?) = JSONAssert.assertNotEquals(expected, this, true) + +infix fun String?.`should not strictly be equal to json`(expected: String?) = shouldNotStrictlyBeEqualToJson(expected) + +infix fun String?.shouldNotBeEqualToJson(expected: String?) = JSONAssert.assertNotEquals(expected, this, false) + +infix fun String?.`should not be equal to json`(expected: String?) = shouldNotBeEqualToJson(expected) diff --git a/api/test/utils/JsonAssertExtensions.kt b/api/test/utils/JsonAssertExtensions.kt new file mode 100644 index 0000000..1ee2569 --- /dev/null +++ b/api/test/utils/JsonAssertExtensions.kt @@ -0,0 +1,23 @@ +package utils + +import org.json.JSONObject + +operator fun JSONObject.set(name: String, value: String) { + this.put(name, value) +} + +operator fun JSONObject.set(name: String, value: Double) { + this.put(name, value) +} + +operator fun JSONObject.set(name: String, value: Long) { + this.put(name, value) +} + +operator fun JSONObject.set(name: String, value: Int) { + this.put(name, value) +} + +operator fun JSONObject.set(name: String, value: Boolean) { + this.put(name, value) +} diff --git a/api/test/utils/KtorTestingExtensions.kt b/api/test/utils/KtorTestingExtensions.kt new file mode 100644 index 0000000..7cad21a --- /dev/null +++ b/api/test/utils/KtorTestingExtensions.kt @@ -0,0 +1,52 @@ +package utils + +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.server.testing.* +import org.json.JSONObject + + +fun TestApplicationRequest.json(block: (JSONObject) -> Unit) { + addHeader(HttpHeaders.ContentType, "application/json") + setBody(JSONObject().apply(block).toString()) +} + +fun TestApplicationRequest.setToken(token: String) { + addHeader(HttpHeaders.Authorization, "Bearer $token") +} + +fun TestApplicationEngine.post( + uri: String, + setup: TestApplicationRequest.() -> Unit = {} +): TestApplicationResponse = handleRequest { + this.uri = uri + this.method = HttpMethod.Post + setup() +}.response + +fun TestApplicationEngine.get( + uri: String, + setup: TestApplicationRequest.() -> Unit = {} +): TestApplicationResponse = handleRequest { + this.uri = uri + this.method = HttpMethod.Get + setup() +}.response + +fun TestApplicationEngine.delete( + uri: String, + setup: TestApplicationRequest.() -> Unit = {} +): TestApplicationResponse = handleRequest { + this.uri = uri + this.method = HttpMethod.Delete + setup() +}.response + +fun TestApplicationEngine.put( + uri: String, + setup: TestApplicationRequest.() -> Unit = {} +): TestApplicationResponse = handleRequest { + this.uri = uri + this.method = HttpMethod.Put + setup() +}.response