Merge branch 'tests'
This commit is contained in:
commit
f09219b032
44
api/pom.xml
44
api/pom.xml
@ -129,6 +129,11 @@
|
||||
<artifactId>ktorm-support-mysql</artifactId>
|
||||
<version>${ktorm_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>me.liuwj.ktorm</groupId>
|
||||
<artifactId>ktorm-jackson</artifactId>
|
||||
<version>${ktorm_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.hekeki</groupId>
|
||||
<artifactId>huckleberry</artifactId>
|
||||
@ -161,6 +166,36 @@
|
||||
<version>1.0.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ktor</groupId>
|
||||
<artifactId>ktor-server-tests</artifactId>
|
||||
<version>${ktor_version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>mariadb</artifactId>
|
||||
<version>1.14.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.amshove.kluent</groupId>
|
||||
<artifactId>kluent</artifactId>
|
||||
<version>1.61</version>
|
||||
<scope>test</scope>
|
||||
</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>
|
||||
<build>
|
||||
<sourceDirectory>${project.basedir}/src</sourceDirectory>
|
||||
@ -174,12 +209,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>Test*</include>
|
||||
<include>*Test</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
<version>3.0.0-M4</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
@ -210,7 +240,7 @@
|
||||
<configuration>
|
||||
<experimentalCoroutines>enable</experimentalCoroutines>
|
||||
<sourceDirs>
|
||||
<sourceDir>${project.basedir}/src/test</sourceDir>
|
||||
<sourceDir>${project.basedir}/test</sourceDir>
|
||||
</sourceDirs>
|
||||
</configuration>
|
||||
</execution>
|
||||
|
||||
@ -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
|
||||
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);
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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`;
|
||||
@ -1,5 +0,0 @@
|
||||
ALTER TABLE `Notes`
|
||||
DROP COLUMN `last_viewed`;
|
||||
|
||||
ALTER TABLE `Notes`
|
||||
ADD COLUMN `updated_at` datetime;
|
||||
@ -1,2 +0,0 @@
|
||||
ALTER TABLE `Chapters`
|
||||
ADD COLUMN `title` varchar(50);
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -12,4 +12,6 @@
|
||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||
<logger name="io.netty" level="INFO"/>
|
||||
<logger name="org.flywaydb.core" level="INFO"/>
|
||||
<logger name="org.testcontainers" level="INFO"/>
|
||||
<logger name="com.github.dockerjava" level="INFO"/>
|
||||
</configuration>
|
||||
|
||||
21
api/src/Dependencies.kt
Normal file
21
api/src/Dependencies.kt
Normal file
@ -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<Logger>() with singleton { LoggerFactory.getLogger("Application") }
|
||||
bind<Migration>() with singleton { Migration(this.kodein) }
|
||||
bind<Database>() with singleton { Database.connect(this.instance<DataSource>()) }
|
||||
}
|
||||
@ -20,15 +20,13 @@ import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.sql.DataSource
|
||||
|
||||
val kodein = Kodein {
|
||||
import(serviceModule)
|
||||
import(configurationModule)
|
||||
bind<Logger>() with singleton { LoggerFactory.getLogger("Application") }
|
||||
bind<Migration>() with singleton { Migration(this.kodein) }
|
||||
bind<Database>() with singleton { Database.connect(this.instance<DataSource>()) }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
|
||||
val kodein = Kodein{
|
||||
import(mainModule)
|
||||
}
|
||||
|
||||
val config by kodein.instance<Config>()
|
||||
val logger by kodein.instance<Logger>()
|
||||
logger.info("Running application with configuration $config")
|
||||
@ -43,7 +41,7 @@ fun serve(kodein: Kodein) {
|
||||
val logger by kodein.instance<Logger>()
|
||||
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 {
|
||||
|
||||
@ -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<SimpleJWT>(tag = "auth")
|
||||
|
||||
@ -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}"}""")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,6 @@ fun Application.loadFeatures(kodein: Kodein) {
|
||||
corsFeature()
|
||||
}
|
||||
contentNegotiationFeature()
|
||||
authenticationModule()
|
||||
authenticationModule(kodein)
|
||||
handleErrors()
|
||||
}
|
||||
|
||||
@ -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<UserService>()
|
||||
|
||||
post("/user/test") {
|
||||
val user = call.receive<User>()
|
||||
call.respond(user)
|
||||
}
|
||||
|
||||
route("/user") {
|
||||
post {
|
||||
val user = call.receive<UserDto>()
|
||||
val user = call.receive<User>()
|
||||
|
||||
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<UserDto>()
|
||||
val user = call.receive<User>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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<NotesService>()
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<Database>() with singleton { db }
|
||||
bind<UserService>() with singleton { UserService(this.kodein) }
|
||||
bind<NotesService>() with singleton { NotesService(this.kodein) }
|
||||
}
|
||||
|
||||
val notesService by kodein.instance<NotesService>()
|
||||
|
||||
@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))
|
||||
}
|
||||
|
||||
}
|
||||
142
api/test/routing/UserControllerKtTest.kt
Normal file
142
api/test/routing/UserControllerKtTest.kt
Normal 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"}"""
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
69
api/test/services/UserServiceTest.kt
Normal file
69
api/test/services/UserServiceTest.kt
Normal file
@ -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<DataSource>(overrides = true) with singleton { testContainerDataSource(mariadb) }
|
||||
}
|
||||
|
||||
private val migration by kodein.instance<Migration>()
|
||||
|
||||
init {
|
||||
migration.migrate()
|
||||
}
|
||||
|
||||
private val userService by kodein.instance<UserService>()
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
19
api/test/utils/Assertions.kt
Normal file
19
api/test/utils/Assertions.kt
Normal 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)
|
||||
23
api/test/utils/JsonAssertExtensions.kt
Normal file
23
api/test/utils/JsonAssertExtensions.kt
Normal 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)
|
||||
}
|
||||
52
api/test/utils/KtorTestingExtensions.kt
Normal file
52
api/test/utils/KtorTestingExtensions.kt
Normal 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
|
||||
18
api/test/utils/TestContainers.kt
Normal file
18
api/test/utils/TestContainers.kt
Normal file
@ -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<KMariadbContainer>()
|
||||
|
||||
fun testContainerDataSource(container: KMariadbContainer): DataSource {
|
||||
val hikariConfig = HikariConfig().apply {
|
||||
jdbcUrl = container.jdbcUrl
|
||||
username = container.username
|
||||
password = container.password
|
||||
}
|
||||
|
||||
return HikariDataSource(hikariConfig)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user