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