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