More tests !

This commit is contained in:
Hubert Van De Walle 2020-06-14 23:06:28 +02:00
parent 913e3dfc93
commit 07ec732c55
10 changed files with 281 additions and 16 deletions

View File

@ -129,6 +129,11 @@
<artifactId>ktorm-support-mysql</artifactId> <artifactId>ktorm-support-mysql</artifactId>
<version>${ktorm_version}</version> <version>${ktorm_version}</version>
</dependency> </dependency>
<dependency>
<groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-jackson</artifactId>
<version>${ktorm_version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.github.hekeki</groupId> <groupId>com.github.hekeki</groupId>
<artifactId>huckleberry</artifactId> <artifactId>huckleberry</artifactId>
@ -179,6 +184,18 @@
<version>1.61</version> <version>1.61</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<version>1.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>1.10.0</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
<sourceDirectory>${project.basedir}/src</sourceDirectory> <sourceDirectory>${project.basedir}/src</sourceDirectory>

View File

@ -12,4 +12,4 @@ interface User : Entity<User> {
var password: String var password: String
var createdAt: LocalDateTime var createdAt: LocalDateTime
var lastLogin: LocalDateTime? var lastLogin: LocalDateTime?
} }

View File

@ -1,19 +1,16 @@
package be.vandewalleh.extensions package be.vandewalleh.extensions
import be.vandewalleh.auth.UserDbIdPrincipal import be.vandewalleh.auth.UserDbIdPrincipal
import be.vandewalleh.kodein
import be.vandewalleh.services.FullNoteCreateDTO import be.vandewalleh.services.FullNoteCreateDTO
import be.vandewalleh.services.FullNotePatchDTO import be.vandewalleh.services.FullNotePatchDTO
import be.vandewalleh.services.UserService
import io.ktor.application.* import io.ktor.application.*
import io.ktor.auth.* import io.ktor.auth.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.request.* import io.ktor.request.*
import io.ktor.response.* import io.ktor.response.*
import org.kodein.di.generic.instance
suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) { suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
respond(status, status.description) respond(status, """{"msg": "${status.description}"}""")
} }
/** /**

View File

@ -3,9 +3,12 @@ package be.vandewalleh.features
import io.ktor.application.* import io.ktor.application.*
import io.ktor.features.* import io.ktor.features.*
import io.ktor.jackson.* import io.ktor.jackson.*
import me.liuwj.ktorm.jackson.*
fun Application.contentNegotiationFeature() { fun Application.contentNegotiationFeature() {
install(ContentNegotiation) { install(ContentNegotiation) {
jackson {} jackson {
registerModule(KtormModule())
}
} }
} }

View File

@ -1,5 +1,6 @@
package be.vandewalleh.routing package be.vandewalleh.routing
import be.vandewalleh.entities.User
import be.vandewalleh.extensions.respondStatus import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.extensions.userId import be.vandewalleh.extensions.userId
import be.vandewalleh.services.UserService import be.vandewalleh.services.UserService
@ -12,16 +13,22 @@ import io.ktor.routing.*
import org.kodein.di.Kodein import org.kodein.di.Kodein
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
import org.mindrot.jbcrypt.BCrypt import org.mindrot.jbcrypt.BCrypt
import java.time.LocalDateTime
fun Routing.user(kodein: Kodein) { fun Routing.user(kodein: Kodein) {
val userService by kodein.instance<UserService>() val userService by kodein.instance<UserService>()
post("/user/test") {
val user = call.receive<User>()
call.respond(user)
}
route("/user") { route("/user") {
post { post {
val user = call.receive<UserDto>() val user = call.receive<User>()
if (userService.userExists(user.username, user.email)) 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()) val hashedPassword = BCrypt.hashpw(user.password, BCrypt.gensalt())
@ -32,7 +39,7 @@ fun Routing.user(kodein: Kodein) {
authenticate { authenticate {
put { put {
val user = call.receive<UserDto>() val user = call.receive<User>()
if (userService.userExists(user.username, user.email)) if (userService.userExists(user.username, user.email))
return@put call.respond(HttpStatusCode.Conflict) return@put call.respond(HttpStatusCode.Conflict)
@ -45,12 +52,13 @@ fun Routing.user(kodein: Kodein) {
} }
delete { delete {
userService.deleteUser(call.userId()) val status = if (userService.deleteUser(call.userId()))
call.respondStatus(HttpStatusCode.OK) HttpStatusCode.OK
else
HttpStatusCode.NotFound
call.respondStatus(status)
} }
} }
} }
} }
private data class UserDto(val username: String, val email: String, val password: String)

View File

@ -98,9 +98,13 @@ class UserService(override val kodein: Kodein) : KodeinAware {
} }
} }
fun deleteUser(userId: Int) { fun deleteUser(userId: Int): Boolean {
db.useTransaction { db.useTransaction {
db.delete(Users) { it.id eq userId } return when (db.delete(Users) { it.id eq userId }) {
1 -> true
0 -> false
else -> error("??")
}
} }
} }
} }

View File

@ -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<UserService>()
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<UserService>(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<SimpleJWT>("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<SimpleJWT>("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"}"""
}
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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