diff --git a/api/pom.xml b/api/pom.xml
index 098b0a5..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
@@ -161,6 +166,36 @@
1.0.2
test
+
+ io.ktor
+ ktor-server-tests
+ ${ktor_version}
+ test
+
+
+ org.testcontainers
+ mariadb
+ 1.14.3
+ test
+
+
+ org.amshove.kluent
+ kluent
+ 1.61
+ test
+
+
+ org.skyscreamer
+ jsonassert
+ 1.5.0
+ test
+
+
+ io.mockk
+ mockk
+ 1.10.0
+ test
+
${project.basedir}/src
@@ -174,12 +209,7 @@
org.apache.maven.plugins
maven-surefire-plugin
-
-
- Test*
- *Test
-
-
+ 3.0.0-M4
maven-compiler-plugin
@@ -210,7 +240,7 @@
enable
- ${project.basedir}/src/test
+ ${project.basedir}/test
diff --git a/api/resources/db/migration/V1__Create_user_table.sql b/api/resources/db/migration/V1__Create_user_table.sql
index 041f5c7..14a6c98 100644
--- a/api/resources/db/migration/V1__Create_user_table.sql
+++ b/api/resources/db/migration/V1__Create_user_table.sql
@@ -1,9 +1,46 @@
-CREATE TABLE `Users`
+create table Users
(
- `id` int PRIMARY KEY AUTO_INCREMENT,
- `username` varchar(50) UNIQUE NOT NULL,
- `email` varchar(255) UNIQUE NOT NULL,
- `password` varchar(255) NOT NULL,
- `created_at` datetime NOT NULL,
- `last_login` datetime
-);
\ No newline at end of file
+ id int auto_increment primary key,
+ username varchar(50) not null,
+ email varchar(255) not null,
+ password varchar(255) not null,
+ created_at datetime not null,
+ last_login datetime null,
+
+ constraint email unique (email),
+ constraint username unique (username)
+);
+
+create table Notes
+(
+ uuid binary(16) not null primary key,
+ title varchar(50) not null,
+ user_id int not null,
+ updated_at datetime null,
+
+ constraint Notes_fk_user foreign key (user_id) references Users (id) on delete cascade
+);
+
+create table Chapters
+(
+ id int auto_increment primary key,
+ number int not null,
+ title varchar(50) not null,
+ content text not null,
+ note_uuid binary(16) not null,
+ constraint Chapters_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade
+);
+
+create index note_uuid on Chapters (note_uuid);
+
+create index user_id on Notes (user_id);
+
+create table Tags
+(
+ id int auto_increment primary key,
+ name varchar(50) not null,
+ note_uuid binary(16) not null,
+ constraint Tags_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade
+);
+
+create index note_uuid on Tags (note_uuid);
diff --git a/api/resources/db/migration/V2__Create_note_table.sql b/api/resources/db/migration/V2__Create_note_table.sql
deleted file mode 100644
index 4904664..0000000
--- a/api/resources/db/migration/V2__Create_note_table.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-CREATE TABLE `Notes`
-(
- `id` int PRIMARY KEY AUTO_INCREMENT,
- `title` varchar(50) NOT NULL,
- `content` text NOT NULL,
- `user_id` int NOT NULL,
- `last_viewed` datetime
-);
-
-ALTER TABLE `Notes`
- ADD FOREIGN KEY (`user_id`) REFERENCES `Users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT;
\ No newline at end of file
diff --git a/api/resources/db/migration/V3__Create_tag_table.sql b/api/resources/db/migration/V3__Create_tag_table.sql
deleted file mode 100644
index 71f9b44..0000000
--- a/api/resources/db/migration/V3__Create_tag_table.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-CREATE TABLE `Tags`
-(
- `id` int PRIMARY KEY AUTO_INCREMENT,
- `name` varchar(50) NOT NULL,
- `note_id` int NOT NULL
-);
-
-ALTER TABLE `Tags`
- ADD FOREIGN KEY (`note_id`) REFERENCES `Notes` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT;
\ No newline at end of file
diff --git a/api/resources/db/migration/V4__Create_chapters_table.sql b/api/resources/db/migration/V4__Create_chapters_table.sql
deleted file mode 100644
index 2867403..0000000
--- a/api/resources/db/migration/V4__Create_chapters_table.sql
+++ /dev/null
@@ -1,13 +0,0 @@
-CREATE TABLE `Chapters`
-(
- `id` int PRIMARY KEY AUTO_INCREMENT,
- `number` int NOT NULL,
- `content` text NOT NULL,
- `note_id` int NOT NULL
-);
-
-ALTER TABLE `Chapters`
- ADD FOREIGN KEY (`note_id`) REFERENCES `Notes` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT;
-
-ALTER TABLE `Notes`
- DROP COLUMN `content`;
\ No newline at end of file
diff --git a/api/resources/db/migration/V5__Update_note_table.sql b/api/resources/db/migration/V5__Update_note_table.sql
deleted file mode 100644
index 980637b..0000000
--- a/api/resources/db/migration/V5__Update_note_table.sql
+++ /dev/null
@@ -1,5 +0,0 @@
-ALTER TABLE `Notes`
- DROP COLUMN `last_viewed`;
-
-ALTER TABLE `Notes`
- ADD COLUMN `updated_at` datetime;
diff --git a/api/resources/db/migration/V6__Update_chapter_table.sql b/api/resources/db/migration/V6__Update_chapter_table.sql
deleted file mode 100644
index 1478385..0000000
--- a/api/resources/db/migration/V6__Update_chapter_table.sql
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE `Chapters`
- ADD COLUMN `title` varchar(50);
diff --git a/api/resources/db/migration/V7__Update_constraints_cascade_delete.sql b/api/resources/db/migration/V7__Update_constraints_cascade_delete.sql
deleted file mode 100644
index 48bb1f5..0000000
--- a/api/resources/db/migration/V7__Update_constraints_cascade_delete.sql
+++ /dev/null
@@ -1,19 +0,0 @@
--- ON DELETE -> CASCADE
-
-ALTER TABLE `Notes`
- DROP CONSTRAINT `Notes_ibfk_1`;
-
-ALTER TABLE `Notes`
- ADD FOREIGN KEY (`user_id`) REFERENCES `Users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT;
-
-ALTER TABLE `Chapters`
- DROP CONSTRAINT `Chapters_ibfk_1`;
-
-ALTER TABLE `Chapters`
- ADD FOREIGN KEY (`note_id`) REFERENCES `Notes` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT;
-
-ALTER TABLE `Tags`
- DROP CONSTRAINT `Tags_ibfk_1`;
-
-ALTER TABLE `Tags`
- ADD FOREIGN KEY (`note_id`) REFERENCES `Notes` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT;
\ No newline at end of file
diff --git a/api/resources/db/migration/V8__Notes_uuid.sql b/api/resources/db/migration/V8__Notes_uuid.sql
deleted file mode 100644
index 301e6f4..0000000
--- a/api/resources/db/migration/V8__Notes_uuid.sql
+++ /dev/null
@@ -1,37 +0,0 @@
--- no need to migrate existing data yet
-drop table if exists Chapters;
-drop table if exists Tags;
-drop table if exists Notes;
-
-CREATE TABLE `Notes`
-(
- `uuid` binary(16) PRIMARY KEY,
- `title` varchar(50) NOT NULL,
- `user_id` int NOT NULL,
- `updated_at` datetime
-);
-
-ALTER TABLE `Notes`
- ADD FOREIGN KEY (`user_id`) REFERENCES `Users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT;
-
-CREATE TABLE `Tags`
-(
- `id` int PRIMARY KEY AUTO_INCREMENT,
- `name` varchar(50) NOT NULL,
- `note_uuid` binary(16) NOT NULL
-);
-
-ALTER TABLE `Tags`
- ADD FOREIGN KEY (`note_uuid`) REFERENCES `Notes` (`uuid`) ON DELETE CASCADE ON UPDATE RESTRICT;
-
-CREATE TABLE `Chapters`
-(
- `id` int PRIMARY KEY AUTO_INCREMENT,
- `number` int NOT NULL,
- `title` varchar(50) NOT NULL,
- `content` text NOT NULL,
- `note_uuid` binary(16) NOT NULL
-);
-
-ALTER TABLE `Chapters`
- ADD FOREIGN KEY (`note_uuid`) REFERENCES `Notes` (`uuid`) ON DELETE CASCADE ON UPDATE RESTRICT;
diff --git a/api/resources/logback.xml b/api/resources/logback.xml
index d38ae26..b9c2bba 100644
--- a/api/resources/logback.xml
+++ b/api/resources/logback.xml
@@ -12,4 +12,6 @@
+
+
diff --git a/api/src/Dependencies.kt b/api/src/Dependencies.kt
new file mode 100644
index 0000000..c77fcd7
--- /dev/null
+++ b/api/src/Dependencies.kt
@@ -0,0 +1,21 @@
+package be.vandewalleh
+
+import be.vandewalleh.features.configurationModule
+import be.vandewalleh.migrations.Migration
+import be.vandewalleh.services.serviceModule
+import me.liuwj.ktorm.database.*
+import org.kodein.di.Kodein
+import org.kodein.di.generic.bind
+import org.kodein.di.generic.instance
+import org.kodein.di.generic.singleton
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import javax.sql.DataSource
+
+val mainModule = Kodein.Module("main") {
+ import(serviceModule)
+ import(configurationModule)
+ bind() with singleton { LoggerFactory.getLogger("Application") }
+ bind() with singleton { Migration(this.kodein) }
+ bind() with singleton { Database.connect(this.instance()) }
+}
diff --git a/api/src/NotesApplication.kt b/api/src/NotesApplication.kt
index 7774f5f..e52d240 100644
--- a/api/src/NotesApplication.kt
+++ b/api/src/NotesApplication.kt
@@ -20,15 +20,13 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.sql.DataSource
-val kodein = Kodein {
- import(serviceModule)
- import(configurationModule)
- bind() with singleton { LoggerFactory.getLogger("Application") }
- bind() with singleton { Migration(this.kodein) }
- bind() with singleton { Database.connect(this.instance()) }
-}
fun main() {
+
+ val kodein = Kodein{
+ import(mainModule)
+ }
+
val config by kodein.instance()
val logger by kodein.instance()
logger.info("Running application with configuration $config")
@@ -43,7 +41,7 @@ fun serve(kodein: Kodein) {
val logger by kodein.instance()
val env = applicationEngineEnvironment {
module {
- module()
+ module(kodein)
}
log = logger
connector {
@@ -55,7 +53,7 @@ fun serve(kodein: Kodein) {
}
-fun Application.module() {
+fun Application.module(kodein: Kodein) {
loadFeatures(kodein)
routing {
diff --git a/api/src/auth/AuthenticationModule.kt b/api/src/auth/AuthenticationModule.kt
index cea8b90..33e3441 100644
--- a/api/src/auth/AuthenticationModule.kt
+++ b/api/src/auth/AuthenticationModule.kt
@@ -1,12 +1,12 @@
package be.vandewalleh.auth
-import be.vandewalleh.kodein
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.auth.jwt.*
+import org.kodein.di.Kodein
import org.kodein.di.generic.instance
-fun Application.authenticationModule() {
+fun Application.authenticationModule(kodein: Kodein) {
install(Authentication) {
jwt {
val simpleJwt by kodein.instance(tag = "auth")
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/features/Features.kt b/api/src/features/Features.kt
index 22be429..cea8bd3 100644
--- a/api/src/features/Features.kt
+++ b/api/src/features/Features.kt
@@ -11,6 +11,6 @@ fun Application.loadFeatures(kodein: Kodein) {
corsFeature()
}
contentNegotiationFeature()
- authenticationModule()
+ authenticationModule(kodein)
handleErrors()
}
diff --git a/api/src/routing/UserController.kt b/api/src/routing/UserController.kt
index f8f94e3..bbdbea9 100644
--- a/api/src/routing/UserController.kt
+++ b/api/src/routing/UserController.kt
@@ -1,8 +1,8 @@
package be.vandewalleh.routing
+import be.vandewalleh.entities.User
import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.extensions.userId
-import be.vandewalleh.services.UserDto
import be.vandewalleh.services.UserService
import io.ktor.application.*
import io.ktor.auth.*
@@ -13,49 +13,52 @@ 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())
- userService.createUser(
- UserDto(user.username, user.email, hashedPassword)
- )
+ userService.createUser(user.username, user.email, hashedPassword)
call.respondStatus(HttpStatusCode.Created)
}
authenticate {
put {
- val user = call.receive()
+ val user = call.receive()
if (userService.userExists(user.username, user.email))
return@put call.respond(HttpStatusCode.Conflict)
val hashedPassword = BCrypt.hashpw(user.password, BCrypt.gensalt())
- userService.updateUser(
- call.userId(),
- UserDto(user.username, user.email, hashedPassword)
- )
+ userService.updateUser(call.userId(), user.username, user.email, hashedPassword)
call.respondStatus(HttpStatusCode.OK)
}
delete {
- userService.deleteUser(call.userId())
- call.respondStatus(HttpStatusCode.OK)
+ val status = if (userService.deleteUser(call.userId()))
+ HttpStatusCode.OK
+ else
+ HttpStatusCode.NotFound
+ call.respondStatus(status)
}
}
}
-
-}
\ No newline at end of file
+}
diff --git a/api/src/services/UserService.kt b/api/src/services/UserService.kt
index 0ed10f9..fc93555 100644
--- a/api/src/services/UserService.kt
+++ b/api/src/services/UserService.kt
@@ -8,6 +8,7 @@ import me.liuwj.ktorm.entity.*
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.generic.instance
+import java.sql.SQLIntegrityConstraintViolationException
import java.time.LocalDateTime
/**
@@ -28,19 +29,14 @@ class UserService(override val kodein: Kodein) : KodeinAware {
}
/**
- * returns a user email and password from it's email if found or null
+ * returns a user email and password from it's username if found or null
*/
- fun getFromUsername(username: String): UserSchema? {
+ fun getFromUsername(username: String): User? {
return db.from(Users)
.select(Users.email, Users.password, Users.id)
.where { Users.username eq username }
.map { row ->
- UserSchema(
- row[Users.id]!!,
- username,
- row[Users.email]!!,
- row[Users.password]!!
- )
+ Users.createEntity(row)
}
.firstOrNull()
}
@@ -59,11 +55,11 @@ class UserService(override val kodein: Kodein) : KodeinAware {
.firstOrNull() != null
}
- fun getUserInfo(id: Int): UserInfoDto? {
+ fun getUserInfo(id: Int): User? {
return db.from(Users)
.select(Users.email, Users.username)
.where { Users.id eq id }
- .map { UserInfoDto(it[Users.username]!!, it[Users.email]!!) }
+ .map { Users.createEntity(it) }
.firstOrNull()
}
@@ -71,25 +67,30 @@ class UserService(override val kodein: Kodein) : KodeinAware {
* create a new user
* password should already be hashed
*/
- fun createUser(user: UserDto) {
- db.useTransaction {
- val newUser = User {
- this.username = user.username
- this.email = user.email
- this.password = user.password
- this.createdAt = LocalDateTime.now()
- }
+ fun createUser(username: String, email: String, hashedPassword: String): User? {
+ try {
+ db.useTransaction {
+ val newUser = User {
+ this.username = username
+ this.email = email
+ this.password = hashedPassword
+ this.createdAt = LocalDateTime.now()
+ }
- db.sequenceOf(Users).add(newUser)
+ db.sequenceOf(Users).add(newUser)
+ return newUser
+ }
+ } catch (e: SQLIntegrityConstraintViolationException) {
+ return null
}
}
- fun updateUser(userId: Int, user: UserDto) {
+ fun updateUser(userId: Int, username: String, email: String, hashedPassword: String) {
db.useTransaction {
db.update(Users) {
- it.username to user.username
- it.email to user.email
- it.password to user.password
+ it.username to username
+ it.email to email
+ it.password to hashedPassword
where {
it.id eq userId
}
@@ -97,13 +98,14 @@ 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("??")
+ }
}
}
}
-data class UserSchema(val id: Int, val username: String, val email: String, val password: String)
-data class UserDto(val username: String, val email: String, val password: String)
-data class UserInfoDto(val username: String, val email: String)
diff --git a/api/test/FakeDataTest.kt b/api/test/FakeDataTest.kt
deleted file mode 100644
index 3d8f6a4..0000000
--- a/api/test/FakeDataTest.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-import be.vandewalleh.services.ChapterDTO
-import be.vandewalleh.services.FullNoteCreateDTO
-import be.vandewalleh.services.NotesService
-import com.github.javafaker.Faker
-import org.junit.jupiter.api.Test
-import org.kodein.di.generic.instance
-
-class FakeDataTest {
- val notesService by kodein.instance()
-
- @Test
- fun addNotes() {
- val faker = Faker()
- val title = faker.hobbit().quote()
-
- val tags = listOf(
- faker.beer().name(),
- faker.beer().yeast()
- )
-
- val chapters = listOf(
- ChapterDTO(
- faker.animal().name(),
- faker.lorem().paragraph()
- ),
- ChapterDTO(
- faker.animal().name(),
- faker.lorem().paragraph()
- )
- )
-
- val note = FullNoteCreateDTO(title, tags, chapters)
-
- notesService.createNote(1, note)
- }
-
-}
diff --git a/api/test/NotesRetrievePerformanceTest.kt b/api/test/NotesRetrievePerformanceTest.kt
deleted file mode 100644
index 35fe610..0000000
--- a/api/test/NotesRetrievePerformanceTest.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-import be.vandewalleh.services.NotesService
-import be.vandewalleh.services.UserService
-import com.hekeki.huckleberry.Benchmark
-import com.hekeki.huckleberry.BenchmarkRunner
-import com.hekeki.huckleberry.BenchmarkTest
-import com.hekeki.huckleberry.TimeUnit
-import com.zaxxer.hikari.HikariConfig
-import com.zaxxer.hikari.HikariDataSource
-import me.liuwj.ktorm.database.*
-import org.junit.jupiter.api.Assertions.*
-import org.junit.jupiter.api.Test
-import org.kodein.di.Kodein
-import org.kodein.di.generic.bind
-import org.kodein.di.generic.instance
-import org.kodein.di.generic.singleton
-
-
-val hikariConfig = HikariConfig().apply {
- jdbcUrl = "jdbc:mariadb://localhost:3306/notes"
- username = "notes"
- password = "notes"
-}
-
-val dataSource = HikariDataSource(hikariConfig)
-
-val db = Database.Companion.connect(dataSource)
-
-val kodein = Kodein {
- bind() with singleton { db }
- bind() with singleton { UserService(this.kodein) }
- bind() with singleton { NotesService(this.kodein) }
-}
-
-val notesService by kodein.instance()
-
-@Benchmark(threads = 1, iterations = 30, warmup = true, warmupIterations = 1000, timeUnit = TimeUnit.MILLIS)
-class RetrieveNotesBenchmarkTest : BenchmarkTest {
-
- override fun execute() {
- notesService.getNotes(15)
- }
-
- @Test
- fun compute() {
- val benchmarkResult = BenchmarkRunner(RetrieveNotesBenchmarkTest::class.java).run()
- assertTrue(benchmarkResult.median(2, TimeUnit.MILLIS))
- assertTrue(benchmarkResult.maxTime(4, TimeUnit.MILLIS))
- }
-
-}
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/services/UserServiceTest.kt b/api/test/services/UserServiceTest.kt
new file mode 100644
index 0000000..d9a6c57
--- /dev/null
+++ b/api/test/services/UserServiceTest.kt
@@ -0,0 +1,69 @@
+package services
+
+import be.vandewalleh.mainModule
+import be.vandewalleh.migrations.Migration
+import be.vandewalleh.services.UserService
+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 org.kodein.di.generic.singleton
+import utils.KMariadbContainer
+import utils.testContainerDataSource
+import javax.sql.DataSource
+
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
+class UserServiceTest {
+
+ private val mariadb = KMariadbContainer().apply { start() }
+
+ private val kodein = Kodein {
+ import(mainModule, allowOverride = true)
+ bind(overrides = true) with singleton { testContainerDataSource(mariadb) }
+ }
+
+ private val migration by kodein.instance()
+
+ init {
+ migration.migrate()
+ }
+
+ private val userService by kodein.instance()
+
+ @Test
+ @Order(1)
+ fun `test create user`() {
+ val username = "hubert"
+ val email = "a@a"
+ val password = "password"
+ println(userService.createUser(username, email, password))
+
+ val id = userService.getUserId(email)
+ id `should not be` null
+
+ userService.getUserInfo(id!!)!!.let {
+ it.username `should be equal to` username
+ it.email `should be equal to` email
+ }
+ }
+
+ @Test
+ @Order(2)
+ fun `test create same user`() {
+ userService.createUser(username = "hubert", hashedPassword = "password", email = "a@a") `should be` null
+ }
+
+ @Test
+ @Order(3)
+ fun `test delete user`() {
+ val email = "a@a"
+ val id = userService.getUserId(email)!!
+ userService.deleteUser(id)
+
+ userService.getUserId(email) `should be` null
+ userService.getUserInfo(id) `should be` null
+ }
+}
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
diff --git a/api/test/utils/TestContainers.kt b/api/test/utils/TestContainers.kt
new file mode 100644
index 0000000..1254a68
--- /dev/null
+++ b/api/test/utils/TestContainers.kt
@@ -0,0 +1,18 @@
+package utils
+
+import com.zaxxer.hikari.HikariConfig
+import com.zaxxer.hikari.HikariDataSource
+import org.testcontainers.containers.MariaDBContainer
+import javax.sql.DataSource
+
+class KMariadbContainer : MariaDBContainer()
+
+fun testContainerDataSource(container: KMariadbContainer): DataSource {
+ val hikariConfig = HikariConfig().apply {
+ jdbcUrl = container.jdbcUrl
+ username = container.username
+ password = container.password
+ }
+
+ return HikariDataSource(hikariConfig)
+}