From 78b84dc62a206777693a8ef42d9db5a66456f7f8 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Wed, 28 Oct 2020 04:57:49 +0100 Subject: [PATCH 1/2] Add more persistance tests --- simplenotes-persistance/build.gradle.kts | 12 + .../persistance/DbHealthCheckImplTest.kt | 38 +++ ...lTest.kt => BaseNoteRepositoryImplTest.kt} | 222 +++++++++--------- .../notes/H2NoteRepositoryImplTests.kt | 29 +++ ...lTest.kt => BaseUserRepositoryImplTest.kt} | 68 +----- .../users/UserRepositoryImplTests.kt | 29 +++ .../src/test/resources/logback.xml | 15 ++ .../be/simplenotes/persistance/DataSources.kt | 24 ++ .../be/simplenotes/persistance/DbTest.kt | 40 ++++ .../persistance/notes/NotesFixtures.kt | 37 +++ .../persistance/users/UserFixtures.kt | 14 ++ 11 files changed, 358 insertions(+), 170 deletions(-) create mode 100644 simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DbHealthCheckImplTest.kt rename simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/{NoteRepositoryImplTest.kt => BaseNoteRepositoryImplTest.kt} (59%) create mode 100644 simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/H2NoteRepositoryImplTests.kt rename simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/{UserRepositoryImplTest.kt => BaseUserRepositoryImplTest.kt} (51%) create mode 100644 simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/UserRepositoryImplTests.kt create mode 100644 simplenotes-persistance/src/test/resources/logback.xml create mode 100644 simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DataSources.kt create mode 100644 simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DbTest.kt create mode 100644 simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/notes/NotesFixtures.kt create mode 100644 simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/users/UserFixtures.kt diff --git a/simplenotes-persistance/build.gradle.kts b/simplenotes-persistance/build.gradle.kts index 89fe6de..12a9d1d 100644 --- a/simplenotes-persistance/build.gradle.kts +++ b/simplenotes-persistance/build.gradle.kts @@ -3,6 +3,7 @@ import be.simplenotes.Libs plugins { id("be.simplenotes.base") kotlin("kapt") + `java-test-fixtures` } dependencies { @@ -22,4 +23,15 @@ dependencies { testImplementation(Libs.junit) testImplementation(Libs.assertJ) + testImplementation(Libs.logbackClassic) + testImplementation("org.testcontainers:mariadb:1.15.0-rc2") + + testFixturesImplementation(project(":simplenotes-types")) + testFixturesImplementation(project(":simplenotes-config")) + testFixturesImplementation("com.github.javafaker:javafaker:1.0.2") + testFixturesImplementation("org.testcontainers:mariadb:1.15.0-rc2") + testFixturesImplementation(Libs.koinCore) + testFixturesImplementation(Libs.flywayCore) + testFixturesImplementation(Libs.junit) + } diff --git a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DbHealthCheckImplTest.kt b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DbHealthCheckImplTest.kt new file mode 100644 index 0000000..a1d29aa --- /dev/null +++ b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DbHealthCheckImplTest.kt @@ -0,0 +1,38 @@ +package be.simplenotes.persistance + +import be.simplenotes.config.DataSourceConfig +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.parallel.ResourceLock + +@ResourceLock("h2") +class H2DbHealthCheckImplTest : DbTest() { + private val healthCheck = koin.get() + override fun dataSourceConfig() = h2dataSourceConfig() + + @Test + fun healthCheck() { + assertThat(healthCheck.isOk()).isTrue + } + +} + +@ResourceLock("mariadb") +class MariaDbHealthCheckImplTest : DbTest() { + private val healthCheck = koin.get() + lateinit var mariaDB: KMariadbContainer + + override fun dataSourceConfig(): DataSourceConfig { + mariaDB = KMariadbContainer() + mariaDB.start() + return mariadbDataSourceConfig(mariaDB.jdbcUrl) + } + + @Test + fun healthCheck() { + assertThat(healthCheck.isOk()).isTrue + mariaDB.stop() + assertThat(healthCheck.isOk()).isFalse + } + +} diff --git a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/NoteRepositoryImplTest.kt b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/BaseNoteRepositoryImplTest.kt similarity index 59% rename from simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/NoteRepositoryImplTest.kt rename to simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/BaseNoteRepositoryImplTest.kt index acb178a..051d3c6 100644 --- a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/NoteRepositoryImplTest.kt +++ b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/BaseNoteRepositoryImplTest.kt @@ -1,13 +1,12 @@ package be.simplenotes.persistance.notes -import be.simplenotes.config.DataSourceConfig -import be.simplenotes.persistance.DbMigrations +import be.simplenotes.persistance.DbTest import be.simplenotes.persistance.converters.NoteConverter -import be.simplenotes.persistance.migrationModule -import be.simplenotes.persistance.persistanceModule import be.simplenotes.persistance.repositories.NoteRepository import be.simplenotes.persistance.repositories.UserRepository -import be.simplenotes.types.* +import be.simplenotes.persistance.users.createFakeUser +import be.simplenotes.types.ExportedNote +import be.simplenotes.types.PersistedUser import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.dsl.eq import me.liuwj.ktorm.entity.filter @@ -16,42 +15,12 @@ import me.liuwj.ktorm.entity.mapColumns import me.liuwj.ktorm.entity.toList import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy -import org.flywaydb.core.Flyway import org.junit.jupiter.api.* -import org.junit.jupiter.api.parallel.ResourceLock -import org.koin.dsl.koinApplication -import org.koin.dsl.module import org.mapstruct.factory.Mappers import java.sql.SQLIntegrityConstraintViolationException -import java.util.* -import javax.sql.DataSource -@ResourceLock("h2") -internal class NoteRepositoryImplTest { - private val testModule = module { - single { dataSourceConfig() } - } +internal abstract class BaseNoteRepositoryImplTest : DbTest() { - private val koinApp = koinApplication { - modules(persistanceModule, migrationModule, testModule) - } - - private fun dataSourceConfig() = DataSourceConfig( - jdbcUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;", - driverClassName = "org.h2.Driver", - username = "h2", - password = "", - maximumPoolSize = 2, - connectionTimeout = 3000 - ) - - private val koin = koinApp.koin - - @AfterAll - fun afterAll() = koinApp.close() - - private val migration = koin.get() - private val dataSource = koin.get() private val noteRepo = koin.get() private val userRepo = koin.get() private val db = koin.get() @@ -60,35 +29,18 @@ internal class NoteRepositoryImplTest { private lateinit var user2: PersistedUser @BeforeEach - fun beforeEach() { - Flyway.configure() - .dataSource(dataSource) - .load() - .clean() - - migration.migrate() - - user1 = userRepo.create(User("1", "1"))!! - user2 = userRepo.create(User("2", "2"))!! + fun insertUsers() { + user1 = userRepo.createFakeUser()!! + user2 = userRepo.createFakeUser()!! } - private fun createNote( - userId: Int, - title: String, - tags: List = emptyList(), - md: String = "md", - html: String = "html", - ): PersistedNote = noteRepo.create(userId, Note(NoteMetadata(title, tags), md, html)) - - private fun PersistedNote.toPersistedMeta() = PersistedNoteMetadata(meta.title, meta.tags, updatedAt, uuid) - @Nested @DisplayName("create()") inner class Create { @Test fun `create note for non existing user`() { - val note = Note(NoteMetadata("title", emptyList()), "md", "html") + val note = fakeNote() assertThatThrownBy { noteRepo.create(1000, note) @@ -97,7 +49,7 @@ internal class NoteRepositoryImplTest { @Test fun `create note for existing user`() { - val note = Note(NoteMetadata("title", emptyList()), "md", "html") + val note = fakeNote() assertThat(noteRepo.create(user1.id, note)) .isEqualToIgnoringGivenFields(note, "uuid", "updatedAt", "public") @@ -116,15 +68,8 @@ internal class NoteRepositoryImplTest { @Test fun `find all notes`() { - val notes1 = listOf( - createNote(user1.id, "1", listOf("a", "b")), - createNote(user1.id, "2"), - createNote(user1.id, "3", listOf("c")) - ) - - val notes2 = listOf( - createNote(user2.id, "4") - ) + val notes1 = noteRepo.insertFakeNotes(user1, count = 3) + val notes2 = listOf(noteRepo.insertFakeNote(user2)) assertThat(noteRepo.findAll(user1.id)) .hasSize(3) @@ -143,13 +88,15 @@ internal class NoteRepositoryImplTest { assertThat(noteRepo.findAll(1000)).isEmpty() } + // TODO: datetime -> timestamp migration + @Disabled("Not working with mariadb, inserts are too fast so the updated_at is the same") @Test fun pagination() { - (50 downTo 1).forEach { - createNote(user1.id, "$it") + (50 downTo 1).forEach { i -> + noteRepo.insertFakeNote(user1, "$i") } - assertThat(noteRepo.findAll(user1.id, limit = 20, offset = 0)) + assertThat(noteRepo.findAll(user1.id, limit = 20, offset = 0).onEach { println(it) }) .hasSize(20) .allMatch { it.title.toInt() in 1..20 } @@ -164,12 +111,13 @@ internal class NoteRepositoryImplTest { @Test fun `find all notes with tag`() { - createNote(user1.id, "1", listOf("a", "b")) - createNote(user1.id, "2") - createNote(user1.id, "3", listOf("c")) - createNote(user1.id, "4", listOf("c")) - createNote(user2.id, "5", listOf("c")) - + with(noteRepo) { + insertFakeNote(user1, "1", listOf("a", "b")) + insertFakeNote(user1, "2", tags = emptyList()) + insertFakeNote(user1, "3", listOf("c")) + insertFakeNote(user1, "4", listOf("c")) + insertFakeNote(user2, "5", listOf("c")) + } assertThat(noteRepo.findAll(user1.id, tag = "a")) .hasSize(1) .first() @@ -189,34 +137,31 @@ internal class NoteRepositoryImplTest { @Test @Suppress("UNCHECKED_CAST") fun `find an existing note`() { - createNote(user1.id, "1", listOf("a", "b")) + val fakeNote = noteRepo.insertFakeNote(user1) val converter = Mappers.getMapper(NoteConverter::class.java) - val note = db.notes.find { it.title eq "1" }!! + val note = db.notes.find { it.title eq fakeNote.meta.title }!! .let { entity -> val tags = db.tags.filter { it.noteUuid eq entity.uuid }.mapColumns { it.name } as List converter.toPersistedNote(entity, tags) } - assertThat(noteRepo.find(user1.id, note.uuid)) - .isEqualTo(note) - - assertThat(noteRepo.exists(user1.id, note.uuid)) - .isTrue + assertThat(noteRepo.find(user1.id, note.uuid)).isEqualTo(note) + assertThat(noteRepo.exists(user1.id, note.uuid)).isTrue } @Test fun `find an existing note from the wrong user`() { - val note = createNote(user1.id, "1", listOf("a", "b")) + val note = noteRepo.insertFakeNote(user1) assertThat(noteRepo.find(user2.id, note.uuid)).isNull() assertThat(noteRepo.exists(user2.id, note.uuid)).isFalse } @Test fun `find a non existing note`() { - createNote(user1.id, "1", listOf("a", "b")) - val uuid = UUID.randomUUID() + noteRepo.insertFakeNote(user1) + val uuid = fakeUuid() assertThat(noteRepo.find(user1.id, uuid)).isNull() assertThat(noteRepo.exists(user2.id, uuid)).isFalse } @@ -228,16 +173,14 @@ internal class NoteRepositoryImplTest { @Test fun `delete an existing note for a user should succeed and then fail`() { - val note = createNote(user1.id, "1", listOf("a", "b")) - assertThat(noteRepo.delete(user1.id, note.uuid)) - .isTrue + val note = noteRepo.insertFakeNote(user1) + assertThat(noteRepo.delete(user1.id, note.uuid)).isTrue } @Test fun `delete an existing note for the wrong user`() { - val note = createNote(user1.id, "1", listOf("a", "b")) - assertThat(noteRepo.delete(1000, note.uuid)) - .isFalse + val note = noteRepo.insertFakeNote(user1) + assertThat(noteRepo.delete(1000, note.uuid)).isFalse } } @@ -247,15 +190,8 @@ internal class NoteRepositoryImplTest { @Test fun getTags() { - val notes1 = listOf( - createNote(user1.id, "1", listOf("a", "b")), - createNote(user1.id, "2"), - createNote(user1.id, "3", listOf("c", "a")) - ) - - val notes2 = listOf( - createNote(user2.id, "4", listOf("a")) - ) + val notes1 = noteRepo.insertFakeNotes(user1, count = 3) + val notes2 = noteRepo.insertFakeNotes(user2, count = 1) val user1Tags = notes1.flatMap { it.meta.tags }.toSet() assertThat(noteRepo.getTags(user1.id)) @@ -276,16 +212,18 @@ internal class NoteRepositoryImplTest { @Test fun getTags() { - val note1 = createNote(user1.id, "1", listOf("a", "b")) - val newNote1 = Note(meta = note1.meta, markdown = "new", "new") - assertThat(noteRepo.update(user1.id, note1.uuid, newNote1)) - .isNotNull + val note1 = noteRepo.insertFakeNote(user1) + val newNote1 = fakeNote() + + assertThat(noteRepo.update(user1.id, note1.uuid, newNote1)).isNotNull assertThat(noteRepo.find(user1.id, note1.uuid)) .isEqualToComparingOnlyGivenFields(newNote1, "meta", "markdown", "html") - val note2 = createNote(user1.id, "2") - val newNote2 = Note(meta = note1.meta.copy(tags = listOf("a")), markdown = "new", "new") + val note2 = noteRepo.insertFakeNote(user1) + val newNote2 = fakeNote().let { + it.copy(meta = it.meta.copy(tags = tagGenerator().take(3).toList())) + } assertThat(noteRepo.update(user1.id, note2.uuid, newNote2)) .isNotNull @@ -299,7 +237,7 @@ internal class NoteRepositoryImplTest { @Test fun `trashed noted should be restored`() { - val note1 = createNote(user1.id, "1", listOf("a", "b")) + val note1 = noteRepo.insertFakeNote(user1, "1", listOf("a", "b")) assertThat(noteRepo.delete(user1.id, note1.uuid, permanent = false)) .isTrue @@ -321,11 +259,69 @@ internal class NoteRepositoryImplTest { @Test fun `permanent delete`() { - val note1 = createNote(user1.id, "1", listOf("a", "b")) - assertThat(noteRepo.delete(user1.id, note1.uuid, permanent = true)) - .isTrue - - assertThat(noteRepo.restore(user1.id, note1.uuid)).isFalse + val note = noteRepo.insertFakeNote(user1) + assertThat(noteRepo.delete(user1.id, note.uuid, permanent = true)).isTrue + assertThat(noteRepo.restore(user1.id, note.uuid)).isFalse } } + + @Test + fun count() { + assertThat(noteRepo.count(user1.id)).isEqualTo(0) + + noteRepo.insertFakeNotes(user1, count = 10) + assertThat(noteRepo.count(user1.id)).isEqualTo(10) + } + + @Test + fun countWithTag() { + noteRepo.insertFakeNote(user1, tags = listOf("a", "b")) + noteRepo.insertFakeNote(user1, tags = emptyList()) + noteRepo.insertFakeNote(user1, tags = listOf("a")) + noteRepo.insertFakeNote(user1, tags = emptyList()) + + assertThat(noteRepo.count(user1.id, tag = "a")).isEqualTo(2) + } + + @Test + fun export() { + val notes = noteRepo.insertFakeNotes(user1, count = 4) + noteRepo.delete(user1.id, notes.first().uuid, permanent = false) + + val export = noteRepo.export(user1.id) + + val expected = notes.mapIndexed { i, n -> + ExportedNote( + title = n.meta.title, + tags = n.meta.tags, + markdown = n.markdown, + html = n.html, + updatedAt = n.updatedAt, + trash = i == 0, + ) + } + assertThat(export) + .usingElementComparatorIgnoringFields("updatedAt") + .containsExactlyInAnyOrderElementsOf(expected) + } + + @Test + fun findAllDetails() { + val notes = noteRepo.insertFakeNotes(user1, count = 10) + val res = noteRepo.findAllDetails(user1.id) + assertThat(res) + .usingElementComparatorIgnoringFields("updatedAt") + .containsExactlyInAnyOrderElementsOf(notes) + } + + @Test + fun access() { + val n = noteRepo.insertFakeNote(user1) + noteRepo.makePublic(user1.id, n.uuid) + assertThat(noteRepo.findPublic(n.uuid)) + .isEqualToIgnoringGivenFields(n.copy(public = true), "updatedAt") + + noteRepo.makePrivate(user1.id, n.uuid) + assertThat(noteRepo.findPublic(n.uuid)).isNull() + } } diff --git a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/H2NoteRepositoryImplTests.kt b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/H2NoteRepositoryImplTests.kt new file mode 100644 index 0000000..9b311f1 --- /dev/null +++ b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/H2NoteRepositoryImplTests.kt @@ -0,0 +1,29 @@ +package be.simplenotes.persistance.notes + +import be.simplenotes.config.DataSourceConfig +import be.simplenotes.persistance.KMariadbContainer +import be.simplenotes.persistance.h2dataSourceConfig +import be.simplenotes.persistance.mariadbDataSourceConfig +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.parallel.ResourceLock + +@ResourceLock("h2") +internal class H2NoteRepositoryImplTests : BaseNoteRepositoryImplTest() { + override fun dataSourceConfig() = h2dataSourceConfig() +} + +@ResourceLock("mariadb") +internal class MariaDbNoteRepositoryImplTests : BaseNoteRepositoryImplTest() { + lateinit var mariaDB: KMariadbContainer + + @AfterAll + fun stopMariaDB() { + mariaDB.stop() + } + + override fun dataSourceConfig(): DataSourceConfig { + mariaDB = KMariadbContainer() + mariaDB.start() + return mariadbDataSourceConfig(mariaDB.jdbcUrl) + } +} diff --git a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/UserRepositoryImplTest.kt b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/BaseUserRepositoryImplTest.kt similarity index 51% rename from simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/UserRepositoryImplTest.kt rename to simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/BaseUserRepositoryImplTest.kt index f35e169..0103c6f 100644 --- a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/UserRepositoryImplTest.kt +++ b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/BaseUserRepositoryImplTest.kt @@ -1,73 +1,27 @@ package be.simplenotes.persistance.users -import be.simplenotes.config.DataSourceConfig -import be.simplenotes.persistance.DbMigrations -import be.simplenotes.persistance.migrationModule -import be.simplenotes.persistance.persistanceModule +import be.simplenotes.persistance.DbTest import be.simplenotes.persistance.repositories.UserRepository -import be.simplenotes.types.User -import me.liuwj.ktorm.database.* -import me.liuwj.ktorm.dsl.* -import me.liuwj.ktorm.entity.* +import me.liuwj.ktorm.database.Database +import me.liuwj.ktorm.dsl.eq +import me.liuwj.ktorm.entity.find +import me.liuwj.ktorm.entity.toList import org.assertj.core.api.Assertions.assertThat -import org.flywaydb.core.Flyway -import org.junit.jupiter.api.AfterAll -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import org.junit.jupiter.api.parallel.ResourceLock -import org.koin.dsl.koinApplication -import org.koin.dsl.module -import javax.sql.DataSource -@ResourceLock("h2") -internal class UserRepositoryImplTest { +internal abstract class BaseUserRepositoryImplTest : DbTest() { - // region setup - private val testModule = module { - single { dataSourceConfig() } - } - - private val koinApp = koinApplication { - modules(persistanceModule, migrationModule, testModule) - } - - private fun dataSourceConfig() = DataSourceConfig( - jdbcUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;", - driverClassName = "org.h2.Driver", - username = "h2", - password = "", - maximumPoolSize = 2, - connectionTimeout = 3000 - ) - - private val koin = koinApp.koin - - @AfterAll - fun afterAll() = koinApp.close() - - private val migration = koin.get() - private val dataSource = koin.get() private val userRepo = koin.get() private val db = koin.get() - @BeforeEach - fun beforeEach() { - Flyway.configure() - .dataSource(dataSource) - .load() - .clean() - migration.migrate() - } - // endregion setup - @Test fun `insert user`() { - val user = User("username", "test") + val user = fakeUser() assertThat(userRepo.create(user)) .isNotNull - .hasFieldOrPropertyWithValue("username", user.username) - .hasFieldOrPropertyWithValue("password", user.password) + .extracting("username", "password") + .contains(user.username, user.password) assertThat(db.users.find { it.username eq user.username }).isNotNull assertThat(db.users.toList()).hasSize(1) @@ -79,7 +33,7 @@ internal class UserRepositoryImplTest { @Test fun `query existing user`() { - val user = User("username", "test") + val user = fakeUser() userRepo.create(user) val foundUserMaybe = userRepo.find(user.username) @@ -105,7 +59,7 @@ internal class UserRepositoryImplTest { @Test fun `delete existing user`() { - val user = User("username", "test") + val user = fakeUser() userRepo.create(user) val foundUser = userRepo.find(user.username)!! diff --git a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/UserRepositoryImplTests.kt b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/UserRepositoryImplTests.kt new file mode 100644 index 0000000..38dcf71 --- /dev/null +++ b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/UserRepositoryImplTests.kt @@ -0,0 +1,29 @@ +package be.simplenotes.persistance.users + +import be.simplenotes.config.DataSourceConfig +import be.simplenotes.persistance.KMariadbContainer +import be.simplenotes.persistance.h2dataSourceConfig +import be.simplenotes.persistance.mariadbDataSourceConfig +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.parallel.ResourceLock + +@ResourceLock("h2") +internal class UserRepositoryImplTest : BaseUserRepositoryImplTest() { + override fun dataSourceConfig() = h2dataSourceConfig() +} + +@ResourceLock("mariadb") +internal class MariaDbUserRepositoryImplTest : BaseUserRepositoryImplTest() { + lateinit var mariaDB: KMariadbContainer + + @AfterAll + fun stopMariaDB() { + mariaDB.stop() + } + + override fun dataSourceConfig(): DataSourceConfig { + mariaDB = KMariadbContainer() + mariaDB.start() + return mariadbDataSourceConfig(mariaDB.jdbcUrl) + } +} diff --git a/simplenotes-persistance/src/test/resources/logback.xml b/simplenotes-persistance/src/test/resources/logback.xml new file mode 100644 index 0000000..380db36 --- /dev/null +++ b/simplenotes-persistance/src/test/resources/logback.xml @@ -0,0 +1,15 @@ + + + true + + %cyan(%d{YYYY-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %green(%logger{36}) - %msg%n + + + + + + + + + + diff --git a/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DataSources.kt b/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DataSources.kt new file mode 100644 index 0000000..a7fe074 --- /dev/null +++ b/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DataSources.kt @@ -0,0 +1,24 @@ +package be.simplenotes.persistance + +import be.simplenotes.config.DataSourceConfig +import org.testcontainers.containers.MariaDBContainer + +class KMariadbContainer : MariaDBContainer("mariadb:10.5.5") + +fun h2dataSourceConfig() = DataSourceConfig( + jdbcUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;", + driverClassName = "org.h2.Driver", + username = "h2", + password = "", + maximumPoolSize = 2, + connectionTimeout = 3000 +) + +fun mariadbDataSourceConfig(jdbcUrl: String) = DataSourceConfig( + jdbcUrl = jdbcUrl, + driverClassName = "org.mariadb.jdbc.Driver", + username = "test", + password = "test", + maximumPoolSize = 2, + connectionTimeout = 3000 +) diff --git a/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DbTest.kt b/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DbTest.kt new file mode 100644 index 0000000..6ba9d22 --- /dev/null +++ b/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DbTest.kt @@ -0,0 +1,40 @@ +package be.simplenotes.persistance + +import be.simplenotes.config.DataSourceConfig +import org.flywaydb.core.Flyway.* +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeEach +import org.koin.dsl.koinApplication +import org.koin.dsl.module +import javax.sql.DataSource + +abstract class DbTest { + + abstract fun dataSourceConfig(): DataSourceConfig + + private val testModule = module { + single { dataSourceConfig() } + } + + private val koinApp = koinApplication { + modules(persistanceModule, migrationModule, testModule) + } + + val koin = koinApp.koin + + @AfterAll + fun afterAll() = koinApp.close() + + @BeforeEach + fun beforeEach() { + val migration = koin.get() + val dataSource = koin.get() + configure() + .dataSource(dataSource) + .load() + .clean() + + migration.migrate() + } + +} diff --git a/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/notes/NotesFixtures.kt b/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/notes/NotesFixtures.kt new file mode 100644 index 0000000..a546dd3 --- /dev/null +++ b/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/notes/NotesFixtures.kt @@ -0,0 +1,37 @@ +package be.simplenotes.persistance.notes + +import be.simplenotes.persistance.repositories.NoteRepository +import be.simplenotes.types.* +import com.github.javafaker.Faker +import java.util.* + +private val faker = Faker() + +fun fakeUuid() = UUID.randomUUID()!! +fun fakeTitle() = faker.lorem().characters(3, 50)!! +fun tagGenerator() = generateSequence { faker.lorem().word() } +fun fakeTags() = tagGenerator().take(faker.random().nextInt(0, 3)).toList() +fun fakeContent() = faker.lorem().paragraph(faker.random().nextInt(0, 3))!! +fun fakeNote() = Note(NoteMetadata(fakeTitle(), fakeTags()), fakeContent(), fakeContent()) + +fun PersistedNote.toPersistedMeta() = + PersistedNoteMetadata(meta.title, meta.tags, updatedAt, uuid) + +fun NoteRepository.insertFakeNote( + userId: Int, + title: String, + tags: List = emptyList(), + md: String = "md", + html: String = "html", +): PersistedNote = create(userId, Note(NoteMetadata(title, tags), md, html)) + +fun NoteRepository.insertFakeNote( + user: PersistedUser, + title: String = fakeTitle(), + tags: List = fakeTags(), + md: String = fakeContent(), + html: String = fakeContent(), +): PersistedNote = insertFakeNote(user.id, title, tags, md, html) + +fun NoteRepository.insertFakeNotes(user: PersistedUser, count: Int): List = + generateSequence { insertFakeNote(user) }.take(count).toList() diff --git a/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/users/UserFixtures.kt b/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/users/UserFixtures.kt new file mode 100644 index 0000000..179eb56 --- /dev/null +++ b/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/users/UserFixtures.kt @@ -0,0 +1,14 @@ +package be.simplenotes.persistance.users + +import be.simplenotes.persistance.repositories.UserRepository +import be.simplenotes.types.PersistedUser +import be.simplenotes.types.User +import com.github.javafaker.Faker + +private val faker = Faker() + +fun fakeUser() = User(faker.name().username(), faker.internet().password()) + +fun UserRepository.createFakeUser(): PersistedUser? { + return create(fakeUser()) +} From 6a43acfd46199907bd915fa627d079cb08de46e8 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Fri, 30 Oct 2020 02:27:22 +0100 Subject: [PATCH 2/2] Switch from koin to micronaut-inject --- .../src/main/kotlin/be/simplenotes/Libs.kt | 5 +- simplenotes-app/build.gradle.kts | 8 +- .../main/kotlin/be/simplenotes/app/Server.kt | 6 + .../kotlin/be/simplenotes/app/SimpleNotes.kt | 33 ++---- .../simplenotes/app/api/ApiNoteController.kt | 7 +- .../simplenotes/app/api/ApiUserController.kt | 7 +- .../app/controllers/BaseController.kt | 2 + .../app/controllers/HealthCheckController.kt | 2 + .../app/controllers/NoteController.kt | 2 + .../app/controllers/SettingsController.kt | 2 + .../app/controllers/UserController.kt | 2 + .../app/extensions/KoinExtensions.kt | 12 -- .../be/simplenotes/app/filters/AuthFilter.kt | 58 ---------- .../be/simplenotes/app/filters/ErrorFilter.kt | 2 + .../app/filters/TransactionFilter.kt | 2 + .../app/filters/auth/AuthFilter.kt | 24 ++++ .../app/filters/auth/OptionalAuthFilter.kt | 22 ++++ .../app/filters/auth/RequiredAuthFilter.kt | 30 +++++ .../be/simplenotes/app/modules/ApiModule.kt | 24 ---- .../be/simplenotes/app/modules/AuthModule.kt | 46 ++++++++ .../be/simplenotes/app/modules/CoreModules.kt | 12 -- .../be/simplenotes/app/modules/JsonModule.kt | 19 ++-- .../simplenotes/app/modules/ServerModule.kt | 64 ++++------- .../be/simplenotes/app/routes/ApiRoutes.kt | 54 +++++++++ .../be/simplenotes/app/routes/BasicRoutes.kt | 67 +++++++++++ .../be/simplenotes/app/routes/NoteRoutes.kt | 53 +++++++++ .../be/simplenotes/app/routes/RouteUtils.kt | 8 ++ .../be/simplenotes/app/routes/Router.kt | 106 +++--------------- .../simplenotes/app/routes/SettingsRoutes.kt | 44 ++++++++ .../app/utils/StaticFilesResolver.kt | 2 + .../src/main/resources/logback.xml | 1 + ...ilterTest.kt => RequiredAuthFilterTest.kt} | 42 +++++-- simplenotes-config/build.gradle.kts | 4 +- .../be/simplenotes/config/ConfigLoader.kt | 2 + .../be/simplenotes/config/ConfigModule.kt | 19 +++- simplenotes-domain/build.gradle.kts | 5 +- .../be/simplenotes/domain/DomainModule.kt | 37 ------ .../domain/security/JwtPayloadExtractor.kt | 2 + .../domain/security/PasswordHash.kt | 8 +- .../simplenotes/domain/security/SimpleJwt.kt | 2 + .../domain/usecases/NoteService.kt | 2 + .../domain/usecases/UserService.kt | 2 + .../usecases/export/ExportUseCaseImpl.kt | 9 +- .../usecases/markdown/MarkdownConverter.kt | 2 + .../users/delete/DeleteUseCaseImpl.kt | 4 + .../usecases/users/login/LoginUseCaseImpl.kt | 4 + .../users/register/RegisterUseCaseImpl.kt | 4 + simplenotes-persistance/build.gradle.kts | 29 +++-- .../be/simplenotes/persistance/HealthCheck.kt | 2 + .../be/simplenotes/persistance/Migrations.kt | 2 + .../persistance/PersistanceModule.kt | 62 +++++----- .../persistance/notes/NoteRepositoryImpl.kt | 7 +- .../persistance/users/UserRepositoryImpl.kt | 7 +- .../be/simplenotes/persistance/DataSources.kt | 0 .../persistance/DbHealthCheckImplTest.kt | 7 +- .../be/simplenotes/persistance/DbTest.kt | 44 ++++++++ .../notes/BaseNoteRepositoryImplTest.kt | 9 +- .../users/BaseUserRepositoryImplTest.kt | 11 +- .../be/simplenotes/persistance/DbTest.kt | 33 +++--- simplenotes-search/build.gradle.kts | 5 +- .../be/simplenotes/search/NoteSearcherImpl.kt | 11 +- .../be/simplenotes/search/SeachModule.kt | 8 -- .../be/simplenotes/search/SearchModule.kt | 14 +++ simplenotes-views/build.gradle.kts | 5 +- .../kotlin/be/simplenotes/views/BaseView.kt | 5 +- .../kotlin/be/simplenotes/views/ErrorView.kt | 5 +- .../kotlin/be/simplenotes/views/NoteView.kt | 5 +- .../be/simplenotes/views/SettingView.kt | 5 +- .../kotlin/be/simplenotes/views/UserView.kt | 5 +- .../kotlin/be/simplenotes/views/ViewModule.kt | 12 -- 70 files changed, 728 insertions(+), 439 deletions(-) delete mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/extensions/KoinExtensions.kt delete mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/AuthFilter.kt create mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/auth/AuthFilter.kt create mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/auth/OptionalAuthFilter.kt create mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/auth/RequiredAuthFilter.kt delete mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ApiModule.kt create mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/AuthModule.kt delete mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/CoreModules.kt create mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/ApiRoutes.kt create mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/BasicRoutes.kt create mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/NoteRoutes.kt create mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/RouteUtils.kt create mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/SettingsRoutes.kt rename simplenotes-app/src/test/kotlin/be/simplenotes/app/filters/{AuthFilterTest.kt => RequiredAuthFilterTest.kt} (65%) delete mode 100644 simplenotes-domain/src/main/kotlin/be/simplenotes/domain/DomainModule.kt rename simplenotes-persistance/src/{testFixtures => test}/kotlin/be/simplenotes/persistance/DataSources.kt (100%) create mode 100644 simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DbTest.kt delete mode 100644 simplenotes-search/src/main/kotlin/be/simplenotes/search/SeachModule.kt create mode 100644 simplenotes-search/src/main/kotlin/be/simplenotes/search/SearchModule.kt delete mode 100644 simplenotes-views/src/main/kotlin/be/simplenotes/views/ViewModule.kt diff --git a/buildSrc/src/main/kotlin/be/simplenotes/Libs.kt b/buildSrc/src/main/kotlin/be/simplenotes/Libs.kt index 97bef55..5bfad3c 100644 --- a/buildSrc/src/main/kotlin/be/simplenotes/Libs.kt +++ b/buildSrc/src/main/kotlin/be/simplenotes/Libs.kt @@ -16,7 +16,6 @@ object Libs { const val jbcrypt = "org.mindrot:jbcrypt:0.4" const val jettyServer = "org.eclipse.jetty:jetty-server:9.4.32.v20200930" const val jettyServlet = "org.eclipse.jetty:jetty-servlet:9.4.32.v20200930" - const val koinCore = "org.koin:koin-core:2.1.6" const val konform = "io.konform:konform-jvm:0.2.0" const val kotlinxHtml = "org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.1" const val kotlinxSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.0.0" @@ -28,6 +27,8 @@ object Libs { const val luceneQueryParser = "org.apache.lucene:lucene-queryparser:8.6.1" const val mapstruct = "org.mapstruct:mapstruct:1.4.1.Final" const val mapstructProcessor = "org.mapstruct:mapstruct-processor:1.4.1.Final" + const val micronaut = "io.micronaut:micronaut-inject:2.1.2" + const val micronautProcessor = "io.micronaut:micronaut-inject-java:2.1.2" const val mariadbClient = "org.mariadb.jdbc:mariadb-java-client:2.6.2" const val owaspHtmlSanitizer = "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20200713.1" const val prettytime ="org.ocpsoft.prettytime:prettytime:4.0.5.Final" @@ -39,4 +40,6 @@ object Libs { const val http4kTestingHamkrest = "org.http4k:http4k-testing-hamkrest:3.268.0" const val junit = "org.junit.jupiter:junit-jupiter:5.6.2" const val mockk = "io.mockk:mockk:1.10.0" + const val faker = "com.github.javafaker:javafaker:1.0.2" + const val mariaTestContainer = "org.testcontainers:mariadb:1.15.0-rc2" } diff --git a/simplenotes-app/build.gradle.kts b/simplenotes-app/build.gradle.kts index c5cd419..9519be1 100644 --- a/simplenotes-app/build.gradle.kts +++ b/simplenotes-app/build.gradle.kts @@ -6,6 +6,7 @@ plugins { id("be.simplenotes.app-shadow") id("be.simplenotes.app-css") id("be.simplenotes.app-docker") + kotlin("kapt") } dependencies { @@ -16,7 +17,6 @@ dependencies { implementation(project(":simplenotes-config")) implementation(project(":simplenotes-views")) - implementation(Libs.koinCore) implementation(Libs.arrowCoreData) implementation(Libs.konform) implementation(Libs.http4kCore) @@ -27,6 +27,12 @@ dependencies { implementation(Libs.logbackClassic) implementation(Libs.ktormCore) + implementation(Libs.micronaut) + kapt(Libs.micronautProcessor) + + testImplementation(Libs.micronaut) + kaptTest(Libs.micronautProcessor) + testImplementation(Libs.junit) testImplementation(Libs.assertJ) testImplementation(Libs.http4kTestingHamkrest) diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/Server.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/Server.kt index 5b5a01a..54e6105 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/Server.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/Server.kt @@ -1,21 +1,27 @@ package be.simplenotes.app +import io.micronaut.context.annotation.Context import org.http4k.server.Http4kServer import org.slf4j.LoggerFactory +import javax.annotation.PostConstruct +import javax.annotation.PreDestroy import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig +@Context class Server( private val config: SimpleNotesServerConfig, private val http4kServer: Http4kServer, ) { private val logger = LoggerFactory.getLogger(javaClass) + @PostConstruct fun start(): Server { http4kServer.start() logger.info("Listening on http://${config.host}:${config.port}") return this } + @PreDestroy fun stop() { logger.info("Stopping server") http4kServer.close() diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/SimpleNotes.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/SimpleNotes.kt index 4179789..8b5927a 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/SimpleNotes.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/SimpleNotes.kt @@ -1,31 +1,12 @@ package be.simplenotes.app -import be.simplenotes.app.extensions.addShutdownHook -import be.simplenotes.app.modules.* -import be.simplenotes.config.configModule -import be.simplenotes.domain.domainModule -import be.simplenotes.persistance.migrationModule -import be.simplenotes.persistance.persistanceModule -import be.simplenotes.search.searchModule -import be.simplenotes.views.viewModule -import org.koin.core.context.startKoin -import org.koin.core.context.unloadKoinModules +import io.micronaut.context.ApplicationContext fun main() { - startKoin { - modules( - serverModule, - persistanceModule, - migrationModule, - configModule, - viewModule, - controllerModule, - domainModule, - searchModule, - apiModule, - jsonModule - ) - }.addShutdownHook() - - unloadKoinModules(listOf(migrationModule, configModule)) + val ctx = ApplicationContext.run().start() + Runtime.getRuntime().addShutdownHook( + Thread { + ctx.stop() + } + ) } diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/api/ApiNoteController.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/api/ApiNoteController.kt index 08220b8..7ba07b0 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/api/ApiNoteController.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/api/ApiNoteController.kt @@ -17,8 +17,13 @@ import org.http4k.core.Status.Companion.OK import org.http4k.lens.Path import org.http4k.lens.uuid import java.util.* +import javax.inject.Singleton -class ApiNoteController(private val noteService: NoteService, private val json: Json) { +@Singleton +class ApiNoteController( + json: Json, + private val noteService: NoteService, +) { fun createNote(request: Request, loggedInUser: LoggedInUser): Response { val content = noteContentLens(request) diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/api/ApiUserController.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/api/ApiUserController.kt index da9f1ac..b963f2d 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/api/ApiUserController.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/api/ApiUserController.kt @@ -9,8 +9,13 @@ import org.http4k.core.Request import org.http4k.core.Response import org.http4k.core.Status.Companion.BAD_REQUEST import org.http4k.core.Status.Companion.OK +import javax.inject.Singleton -class ApiUserController(private val userService: UserService, private val json: Json) { +@Singleton +class ApiUserController( + json: Json, + private val userService: UserService, +) { private val tokenLens = json.auto().toLens() private val loginFormLens = json.auto().toLens() diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/BaseController.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/BaseController.kt index a63cd09..13dbb84 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/BaseController.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/BaseController.kt @@ -6,7 +6,9 @@ import be.simplenotes.views.BaseView import org.http4k.core.Request import org.http4k.core.Response import org.http4k.core.Status.Companion.OK +import javax.inject.Singleton +@Singleton class BaseController(private val view: BaseView) { fun index(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser?) = Response(OK).html(view.renderHome(loggedInUser)) diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/HealthCheckController.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/HealthCheckController.kt index 90cebac..c2c821a 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/HealthCheckController.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/HealthCheckController.kt @@ -5,7 +5,9 @@ import org.http4k.core.Request import org.http4k.core.Response import org.http4k.core.Status.Companion.OK import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE +import javax.inject.Singleton +@Singleton class HealthCheckController(private val dbHealthCheck: DbHealthCheck) { fun healthCheck(@Suppress("UNUSED_PARAMETER") request: Request) = if (dbHealthCheck.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE) diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/NoteController.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/NoteController.kt index 1b3de56..15d5b3b 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/NoteController.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/NoteController.kt @@ -18,8 +18,10 @@ import org.http4k.core.Status.Companion.OK import org.http4k.core.body.form import org.http4k.routing.path import java.util.* +import javax.inject.Singleton import kotlin.math.abs +@Singleton class NoteController( private val view: NoteView, private val noteService: NoteService, diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/SettingsController.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/SettingsController.kt index fedfde9..ef32d6c 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/SettingsController.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/SettingsController.kt @@ -10,7 +10,9 @@ import be.simplenotes.views.SettingView import org.http4k.core.* import org.http4k.core.body.form import org.http4k.core.cookie.invalidateCookie +import javax.inject.Singleton +@Singleton class SettingsController( private val userService: UserService, private val settingView: SettingView, diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/UserController.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/UserController.kt index 6e9d32a..48526f8 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/UserController.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/UserController.kt @@ -21,7 +21,9 @@ import org.http4k.core.cookie.SameSite import org.http4k.core.cookie.cookie import org.http4k.core.cookie.invalidateCookie import java.util.concurrent.TimeUnit +import javax.inject.Singleton +@Singleton class UserController( private val userService: UserService, private val userView: UserView, diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/extensions/KoinExtensions.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/extensions/KoinExtensions.kt deleted file mode 100644 index acfb908..0000000 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/extensions/KoinExtensions.kt +++ /dev/null @@ -1,12 +0,0 @@ -package be.simplenotes.app.extensions - -import org.koin.core.KoinApplication -import kotlin.concurrent.thread - -fun KoinApplication.addShutdownHook() { - Runtime.getRuntime().addShutdownHook( - thread(start = false) { - close() - } - ) -} diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/AuthFilter.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/AuthFilter.kt deleted file mode 100644 index 7f1c118..0000000 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/AuthFilter.kt +++ /dev/null @@ -1,58 +0,0 @@ -package be.simplenotes.app.filters - -import be.simplenotes.app.extensions.redirect -import be.simplenotes.domain.security.JwtPayloadExtractor -import be.simplenotes.types.LoggedInUser -import org.http4k.core.* -import org.http4k.core.Status.Companion.UNAUTHORIZED -import org.http4k.core.cookie.cookie - -enum class AuthType { - Optional, Required -} - -private const val authKey = "auth" - -class AuthFilter( - private val extractor: JwtPayloadExtractor, - private val authType: AuthType, - private val ctx: RequestContexts, - private val source: JwtSource = JwtSource.Cookie, - private val redirect: Boolean = true, -) : Filter { - override fun invoke(next: HttpHandler): HttpHandler = { - val token = when (source) { - JwtSource.Header -> it.bearerTokenHeader() - JwtSource.Cookie -> it.bearerTokenCookie() - } - val jwtPayload = token?.let { extractor(token) } - when { - jwtPayload != null -> { - ctx[it][authKey] = jwtPayload - next(it) - } - authType == AuthType.Required -> { - if (redirect) Response.redirect("/login") - else Response(UNAUTHORIZED) - } - else -> next(it) - } - } -} - -fun Request.jwtPayload(ctx: RequestContexts): LoggedInUser? = ctx[this][authKey] - -enum class JwtSource { - Header, Cookie -} - -private fun Request.bearerTokenCookie(): String? = cookie("Bearer") - ?.value - ?.trim() - -private fun Request.bearerTokenHeader(): String? = - header("Authorization") - ?.trim() - ?.takeIf { it.startsWith("Bearer") } - ?.substringAfter("Bearer") - ?.trim() diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/ErrorFilter.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/ErrorFilter.kt index a80eb1d..174b9cf 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/ErrorFilter.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/ErrorFilter.kt @@ -10,7 +10,9 @@ import org.http4k.core.Status.Companion.NOT_IMPLEMENTED import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE import org.slf4j.LoggerFactory import java.sql.SQLTransientException +import javax.inject.Singleton +@Singleton class ErrorFilter(private val errorView: ErrorView) : Filter { private val logger = LoggerFactory.getLogger(javaClass) diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/TransactionFilter.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/TransactionFilter.kt index 952aa84..d99a7c8 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/TransactionFilter.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/TransactionFilter.kt @@ -3,7 +3,9 @@ package be.simplenotes.app.filters import me.liuwj.ktorm.database.Database import org.http4k.core.Filter import org.http4k.core.HttpHandler +import javax.inject.Singleton +@Singleton class TransactionFilter(private val db: Database) : Filter { override fun invoke(next: HttpHandler): HttpHandler = { request -> db.useTransaction { diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/auth/AuthFilter.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/auth/AuthFilter.kt new file mode 100644 index 0000000..1137146 --- /dev/null +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/auth/AuthFilter.kt @@ -0,0 +1,24 @@ +package be.simplenotes.app.filters.auth + +import be.simplenotes.types.LoggedInUser +import org.http4k.core.Request +import org.http4k.core.cookie.cookie +import org.http4k.lens.BiDiLens + +typealias OptionalAuthLens = BiDiLens<@JvmSuppressWildcards Request, @JvmSuppressWildcards LoggedInUser?> +typealias RequiredAuthLens = BiDiLens<@JvmSuppressWildcards Request, @JvmSuppressWildcards LoggedInUser> + +enum class JwtSource { + Header, Cookie +} + +fun Request.bearerTokenCookie(): String? = cookie("Bearer") + ?.value + ?.trim() + +fun Request.bearerTokenHeader(): String? = + header("Authorization") + ?.trim() + ?.takeIf { it.startsWith("Bearer") } + ?.substringAfter("Bearer") + ?.trim() diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/auth/OptionalAuthFilter.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/auth/OptionalAuthFilter.kt new file mode 100644 index 0000000..c6c2ced --- /dev/null +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/auth/OptionalAuthFilter.kt @@ -0,0 +1,22 @@ +package be.simplenotes.app.filters.auth + +import be.simplenotes.app.filters.auth.JwtSource.Cookie +import be.simplenotes.domain.security.JwtPayloadExtractor +import org.http4k.core.Filter +import org.http4k.core.HttpHandler +import org.http4k.core.with + +class OptionalAuthFilter( + private val extractor: JwtPayloadExtractor, + private val lens: OptionalAuthLens, + private val source: JwtSource = Cookie, +) : Filter { + override fun invoke(next: HttpHandler): HttpHandler = { + val token = when (source) { + JwtSource.Header -> it.bearerTokenHeader() + Cookie -> it.bearerTokenCookie() + } + + next(it.with(lens of token?.let { extractor(it) })) + } +} diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/auth/RequiredAuthFilter.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/auth/RequiredAuthFilter.kt new file mode 100644 index 0000000..d70c7c6 --- /dev/null +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/auth/RequiredAuthFilter.kt @@ -0,0 +1,30 @@ +package be.simplenotes.app.filters.auth + +import be.simplenotes.app.extensions.redirect +import be.simplenotes.domain.security.JwtPayloadExtractor +import org.http4k.core.Filter +import org.http4k.core.HttpHandler +import org.http4k.core.Response +import org.http4k.core.Status.Companion.UNAUTHORIZED +import org.http4k.core.with + +class RequiredAuthFilter( + private val extractor: JwtPayloadExtractor, + private val lens: RequiredAuthLens, + private val source: JwtSource = JwtSource.Cookie, + private val redirect: Boolean = true, +) : Filter { + override fun invoke(next: HttpHandler): HttpHandler = { + val token = when (source) { + JwtSource.Header -> it.bearerTokenHeader() + JwtSource.Cookie -> it.bearerTokenCookie() + } + val jwtPayload = token?.let { extractor(token) } + + if (jwtPayload != null) next(it.with(lens of jwtPayload)) + else { + if (redirect) Response.redirect("/login") + else Response(UNAUTHORIZED) + } + } +} diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ApiModule.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ApiModule.kt deleted file mode 100644 index 1fbf74a..0000000 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ApiModule.kt +++ /dev/null @@ -1,24 +0,0 @@ -package be.simplenotes.app.modules - -import be.simplenotes.app.api.ApiNoteController -import be.simplenotes.app.api.ApiUserController -import be.simplenotes.app.filters.AuthFilter -import be.simplenotes.app.filters.AuthType -import be.simplenotes.app.filters.JwtSource -import org.http4k.core.Filter -import org.koin.core.qualifier.named -import org.koin.dsl.module - -val apiModule = module { - single { ApiUserController(get(), get()) } - single { ApiNoteController(get(), get()) } - single(named("apiAuthFilter")) { - AuthFilter( - extractor = get(), - authType = AuthType.Required, - ctx = get(), - source = JwtSource.Header, - redirect = false - ) - } -} diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/AuthModule.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/AuthModule.kt new file mode 100644 index 0000000..42c856d --- /dev/null +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/AuthModule.kt @@ -0,0 +1,46 @@ +package be.simplenotes.app.modules + +import be.simplenotes.app.filters.auth.* +import be.simplenotes.domain.security.JwtPayloadExtractor +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Primary +import org.http4k.core.RequestContexts +import org.http4k.lens.RequestContextKey +import javax.inject.Named +import javax.inject.Singleton + +@Factory +class AuthModule { + + @Singleton + @Named("optional") + fun optionalAuthLens(ctx: RequestContexts): OptionalAuthLens = RequestContextKey.optional(ctx) + + @Singleton + @Named("required") + fun requiredAuthLens(ctx: RequestContexts): RequiredAuthLens = RequestContextKey.required(ctx) + + @Singleton + fun optionalAuth(extractor: JwtPayloadExtractor, @Named("optional") lens: OptionalAuthLens) = + OptionalAuthFilter(extractor, lens) + + @Primary + @Singleton + fun requiredAuth(extractor: JwtPayloadExtractor, @Named("required") lens: RequiredAuthLens) = + RequiredAuthFilter(extractor, lens) + + @Singleton + @Named("api") + internal fun apiAuthFilter( + jwtPayloadExtractor: JwtPayloadExtractor, + @Named("required") lens: RequiredAuthLens, + ) = RequiredAuthFilter( + extractor = jwtPayloadExtractor, + lens = lens, + source = JwtSource.Header, + redirect = false + ) + + @Singleton + fun requestContexts() = RequestContexts() +} diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/CoreModules.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/CoreModules.kt deleted file mode 100644 index 82f2f6c..0000000 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/CoreModules.kt +++ /dev/null @@ -1,12 +0,0 @@ -package be.simplenotes.app.modules - -import be.simplenotes.app.controllers.* -import org.koin.dsl.module - -val controllerModule = module { - single { UserController(get(), get(), get()) } - single { HealthCheckController(get()) } - single { BaseController(get()) } - single { NoteController(get(), get()) } - single { SettingsController(get(), get()) } -} diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/JsonModule.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/JsonModule.kt index e349a14..f140148 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/JsonModule.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/JsonModule.kt @@ -2,21 +2,20 @@ package be.simplenotes.app.modules import be.simplenotes.app.serialization.LocalDateTimeSerializer import be.simplenotes.app.serialization.UuidSerializer +import io.micronaut.context.annotation.Factory import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule -import org.koin.dsl.module import java.time.LocalDateTime import java.util.* +import javax.inject.Singleton -val jsonModule = module { - single { - Json { - prettyPrint = true - serializersModule = get() - } - } - single { - SerializersModule { +@Factory +class JsonModule { + + @Singleton + fun json() = Json { + prettyPrint = true + serializersModule = SerializersModule { contextual(LocalDateTime::class, LocalDateTimeSerializer()) contextual(UUID::class, UuidSerializer()) } diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ServerModule.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ServerModule.kt index cc474ab..c1d59a3 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ServerModule.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ServerModule.kt @@ -1,62 +1,38 @@ package be.simplenotes.app.modules -import be.simplenotes.app.Server -import be.simplenotes.app.filters.AuthFilter -import be.simplenotes.app.filters.AuthType -import be.simplenotes.app.filters.ErrorFilter -import be.simplenotes.app.filters.TransactionFilter import be.simplenotes.app.jetty.ConnectorBuilder import be.simplenotes.app.jetty.Jetty import be.simplenotes.app.routes.Router import be.simplenotes.app.utils.StaticFileResolver -import be.simplenotes.app.utils.StaticFileResolverImpl import be.simplenotes.config.ServerConfig +import io.micronaut.context.annotation.Factory import org.eclipse.jetty.server.ServerConnector -import org.http4k.core.Filter -import org.http4k.core.RequestContexts -import org.http4k.routing.RoutingHttpHandler +import org.http4k.server.Http4kServer import org.http4k.server.asServer -import org.koin.core.qualifier.named -import org.koin.core.qualifier.qualifier -import org.koin.dsl.module -import org.koin.dsl.onClose +import javax.inject.Named +import javax.inject.Singleton +import org.eclipse.jetty.server.Server as JettyServer import org.http4k.server.ServerConfig as Http4kServerConfig -val serverModule = module { - single(createdAtStart = true) { Server(get(), get()).start() } onClose { it?.stop() } - single { get().asServer(get()) } - single { - val config = get() - val builder: ConnectorBuilder = { server: org.eclipse.jetty.server.Server -> +@Factory +class ServerModule { + + @Singleton + @Named("styles") + fun styles(resolver: StaticFileResolver) = resolver.resolve("styles.css")!! + + @Singleton + fun http4kServer(router: Router, serverConfig: Http4kServerConfig): Http4kServer = + router().asServer(serverConfig) + + @Singleton + fun http4kServerConfig(config: ServerConfig): Http4kServerConfig { + val builder: ConnectorBuilder = { server: JettyServer -> ServerConnector(server).apply { port = config.port host = config.host } } - Jetty(config.port, builder) + return Jetty(config.port, builder) } - single { StaticFileResolverImpl(get()) } - single { - Router( - get(), - get(), - get(), - get(), - get(), - get(), - get(), - requiredAuth = get(AuthType.Required.qualifier), - optionalAuth = get(AuthType.Optional.qualifier), - apiAuth = get(named("apiAuthFilter")), - get(), - get(), - get(), - )() - } - single { RequestContexts() } - single(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get()) } - single(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get()) } - single { ErrorFilter(get()) } - single { TransactionFilter(get()) } - single(named("styles")) { get().resolve("styles.css") } } diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/ApiRoutes.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/ApiRoutes.kt new file mode 100644 index 0000000..5485b67 --- /dev/null +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/ApiRoutes.kt @@ -0,0 +1,54 @@ +package be.simplenotes.app.routes + +import be.simplenotes.app.api.ApiNoteController +import be.simplenotes.app.api.ApiUserController +import be.simplenotes.app.filters.TransactionFilter +import be.simplenotes.app.filters.auth.RequiredAuthFilter +import be.simplenotes.app.filters.auth.RequiredAuthLens +import org.http4k.core.Filter +import org.http4k.core.Method.* +import org.http4k.core.Request +import org.http4k.core.then +import org.http4k.routing.PathMethod +import org.http4k.routing.RoutingHttpHandler +import org.http4k.routing.bind +import org.http4k.routing.routes +import java.util.function.Supplier +import javax.inject.Named +import javax.inject.Singleton + +@Singleton +class ApiRoutes( + private val apiUserController: ApiUserController, + private val apiNoteController: ApiNoteController, + private val transaction: TransactionFilter, + @Named("api") private val auth: RequiredAuthFilter, + @Named("required") private val authLens: RequiredAuthLens, +) : Supplier { + override fun get(): RoutingHttpHandler { + + fun Filter.then(next: ProtectedHandler) = then { req: Request -> + next(req, authLens(req)) + } + + infix fun PathMethod.to(action: ProtectedHandler) = + this to { req: Request -> action(req, authLens(req)) } + + return routes( + "/login" bind POST to apiUserController::login, + + with(apiNoteController) { + auth.then( + routes( + "/" bind GET to ::notes, + "/" bind POST to transaction.then(::createNote), + "/search" bind POST to ::search, + "/{uuid}" bind GET to ::note, + "/{uuid}" bind PUT to transaction.then(::note), + ) + ).withBasePath("/notes") + } + + ).withBasePath("/api") + } +} diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/BasicRoutes.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/BasicRoutes.kt new file mode 100644 index 0000000..228a2ae --- /dev/null +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/BasicRoutes.kt @@ -0,0 +1,67 @@ +package be.simplenotes.app.routes + +import be.simplenotes.app.controllers.BaseController +import be.simplenotes.app.controllers.HealthCheckController +import be.simplenotes.app.controllers.NoteController +import be.simplenotes.app.controllers.UserController +import be.simplenotes.app.filters.ImmutableFilter +import be.simplenotes.app.filters.TransactionFilter +import be.simplenotes.app.filters.auth.OptionalAuthFilter +import be.simplenotes.app.filters.auth.OptionalAuthLens +import org.http4k.core.ContentType +import org.http4k.core.Filter +import org.http4k.core.Method.GET +import org.http4k.core.Method.POST +import org.http4k.core.Request +import org.http4k.core.then +import org.http4k.routing.* +import java.util.function.Supplier +import javax.inject.Named +import javax.inject.Singleton + +@Singleton +class BasicRoutes( + private val healthCheckController: HealthCheckController, + private val baseCtrl: BaseController, + private val userCtrl: UserController, + private val noteCtrl: NoteController, + @Named("optional") private val authLens: OptionalAuthLens, + private val auth: OptionalAuthFilter, + private val transactionFilter: TransactionFilter, +) : Supplier { + + override fun get(): RoutingHttpHandler { + + fun Filter.then(next: PublicHandler) = then { req: Request -> + next(req, authLens(req)) + } + + infix fun PathMethod.to(action: PublicHandler) = + this to { req: Request -> action(req, authLens(req)) } + + val staticHandler = ImmutableFilter.then( + static( + ResourceLoader.Classpath("/static"), + "woff2" to ContentType("font/woff2"), + "webmanifest" to ContentType("application/manifest+json") + ) + ) + + return routes( + auth.then( + routes( + "/" bind GET to baseCtrl::index, + "/register" bind GET to userCtrl::register, + "/register" bind POST to transactionFilter.then(userCtrl::register), + "/login" bind GET to userCtrl::login, + "/login" bind POST to userCtrl::login, + "/logout" bind POST to userCtrl::logout, + "/notes/public/{uuid}" bind GET to noteCtrl::public, + ) + ), + + "/health" bind GET to healthCheckController::healthCheck, + staticHandler + ) + } +} diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/NoteRoutes.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/NoteRoutes.kt new file mode 100644 index 0000000..28c3e91 --- /dev/null +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/NoteRoutes.kt @@ -0,0 +1,53 @@ +package be.simplenotes.app.routes + +import be.simplenotes.app.controllers.NoteController +import be.simplenotes.app.filters.TransactionFilter +import be.simplenotes.app.filters.auth.RequiredAuthFilter +import be.simplenotes.app.filters.auth.RequiredAuthLens +import org.http4k.core.Filter +import org.http4k.core.Method.GET +import org.http4k.core.Method.POST +import org.http4k.core.Request +import org.http4k.core.then +import org.http4k.routing.PathMethod +import org.http4k.routing.RoutingHttpHandler +import org.http4k.routing.bind +import org.http4k.routing.routes +import java.util.function.Supplier +import javax.inject.Named +import javax.inject.Singleton + +@Singleton +class NoteRoutes( + private val noteCtrl: NoteController, + private val transaction: TransactionFilter, + private val auth: RequiredAuthFilter, + @Named("required") private val authLens: RequiredAuthLens, +) : Supplier { + override fun get(): RoutingHttpHandler { + + fun Filter.then(next: ProtectedHandler) = then { req: Request -> + next(req, authLens(req)) + } + + infix fun PathMethod.to(action: ProtectedHandler) = + this to { req: Request -> action(req, authLens(req)) } + + return auth.then( + with(noteCtrl) { + routes( + "/" bind GET to ::list, + "/" bind POST to ::search, + "/new" bind GET to ::new, + "/new" bind POST to transaction.then(::new), + "/trash" bind GET to ::trash, + "/{uuid}" bind GET to ::note, + "/{uuid}" bind POST to transaction.then(::note), + "/{uuid}/edit" bind GET to ::edit, + "/{uuid}/edit" bind POST to transaction.then(::edit), + "/deleted/{uuid}" bind POST to transaction.then(::deleted), + ).withBasePath("/notes") + } + ) + } +} diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/RouteUtils.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/RouteUtils.kt new file mode 100644 index 0000000..ffedc1d --- /dev/null +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/RouteUtils.kt @@ -0,0 +1,8 @@ +package be.simplenotes.app.routes + +import be.simplenotes.types.LoggedInUser +import org.http4k.core.Request +import org.http4k.core.Response + +internal typealias PublicHandler = (Request, LoggedInUser?) -> Response +internal typealias ProtectedHandler = (Request, LoggedInUser) -> Response diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/Router.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/Router.kt index 70d3b6c..4f6731c 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/Router.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/Router.kt @@ -1,112 +1,32 @@ package be.simplenotes.app.routes -import be.simplenotes.app.api.ApiNoteController -import be.simplenotes.app.api.ApiUserController -import be.simplenotes.app.controllers.* -import be.simplenotes.app.filters.* -import be.simplenotes.types.LoggedInUser -import org.http4k.core.* -import org.http4k.core.Method.* +import be.simplenotes.app.filters.ErrorFilter +import be.simplenotes.app.filters.SecurityFilter +import org.http4k.core.RequestContexts +import org.http4k.core.then import org.http4k.filter.ResponseFilters.GZip import org.http4k.filter.ServerFilters.InitialiseRequestContext -import org.http4k.routing.* -import org.http4k.routing.ResourceLoader.Companion.Classpath +import org.http4k.routing.RoutingHttpHandler +import org.http4k.routing.routes +import java.util.function.Supplier +import javax.inject.Singleton +@Singleton class Router( - private val baseController: BaseController, - private val userController: UserController, - private val noteController: NoteController, - private val settingsController: SettingsController, - private val apiUserController: ApiUserController, - private val apiNoteController: ApiNoteController, - private val healthCheckController: HealthCheckController, - private val requiredAuth: Filter, - private val optionalAuth: Filter, - private val apiAuth: Filter, private val errorFilter: ErrorFilter, - private val transactionFilter: TransactionFilter, private val contexts: RequestContexts, + private val subRouters: List>, ) { operator fun invoke(): RoutingHttpHandler { - val basicRoutes = - routes( - "/health" bind GET to healthCheckController::healthCheck, - ImmutableFilter.then( - static( - Classpath("/static"), - "woff2" to ContentType("font/woff2"), - "webmanifest" to ContentType("application/manifest+json") - ) - ) - ) - - val publicRoutes = routes( - "/" bind GET public baseController::index, - "/register" bind GET public userController::register, - "/register" bind POST `public transactional` userController::register, - "/login" bind GET public userController::login, - "/login" bind POST public userController::login, - "/logout" bind POST to userController::logout, - "/notes/public/{uuid}" bind GET public noteController::public, - ) - - val protectedRoutes = routes( - "/settings" bind GET protected settingsController::settings, - "/settings" bind POST transactional settingsController::settings, - "/export" bind POST protected settingsController::export, - "/notes" bind GET protected noteController::list, - "/notes" bind POST protected noteController::search, - "/notes/new" bind GET protected noteController::new, - "/notes/new" bind POST transactional noteController::new, - "/notes/trash" bind GET protected noteController::trash, - "/notes/{uuid}" bind GET protected noteController::note, - "/notes/{uuid}" bind POST transactional noteController::note, - "/notes/{uuid}/edit" bind GET protected noteController::edit, - "/notes/{uuid}/edit" bind POST transactional noteController::edit, - "/notes/deleted/{uuid}" bind POST transactional noteController::deleted, - ) - - val apiRoutes = routes( - "/api/login" bind POST to apiUserController::login, - ) - - val protectedApiRoutes = routes( - "/api/notes" bind GET protected apiNoteController::notes, - "/api/notes" bind POST transactional apiNoteController::createNote, - "/api/notes/search" bind POST transactional apiNoteController::search, - "/api/notes/{uuid}" bind GET protected apiNoteController::note, - "/api/notes/{uuid}" bind PUT transactional apiNoteController::update, - ) - val routes = routes( - basicRoutes, - optionalAuth.then(publicRoutes), - requiredAuth.then(protectedRoutes), - apiAuth.then(protectedApiRoutes), - apiRoutes, + *subRouters.map { it.get() }.toTypedArray() ) - val globalFilters = errorFilter + return errorFilter .then(InitialiseRequestContext(contexts)) .then(SecurityFilter) .then(GZip()) - - return globalFilters.then(routes) + .then(routes) } - - private inline infix fun PathMethod.public(crossinline handler: PublicHandler) = - this to { handler(it, it.jwtPayload(contexts)) } - - private inline infix fun PathMethod.protected(crossinline handler: ProtectedHandler) = - this to { handler(it, it.jwtPayload(contexts)!!) } - - private inline infix fun PathMethod.transactional(crossinline handler: ProtectedHandler) = - this to transactionFilter.then { handler(it, it.jwtPayload(contexts)!!) } - - private inline infix fun PathMethod.`public transactional`(crossinline handler: PublicHandler) = - this to transactionFilter.then { handler(it, it.jwtPayload(contexts)) } } - -private typealias PublicHandler = (Request, LoggedInUser?) -> Response -private typealias ProtectedHandler = (Request, LoggedInUser) -> Response diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/SettingsRoutes.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/SettingsRoutes.kt new file mode 100644 index 0000000..ddbb9ae --- /dev/null +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/SettingsRoutes.kt @@ -0,0 +1,44 @@ +package be.simplenotes.app.routes + +import be.simplenotes.app.controllers.SettingsController +import be.simplenotes.app.filters.TransactionFilter +import be.simplenotes.app.filters.auth.RequiredAuthFilter +import be.simplenotes.app.filters.auth.RequiredAuthLens +import org.http4k.core.Filter +import org.http4k.core.Method.GET +import org.http4k.core.Method.POST +import org.http4k.core.Request +import org.http4k.core.then +import org.http4k.routing.PathMethod +import org.http4k.routing.RoutingHttpHandler +import org.http4k.routing.bind +import org.http4k.routing.routes +import java.util.function.Supplier +import javax.inject.Named +import javax.inject.Singleton + +@Singleton +class SettingsRoutes( + private val settingsController: SettingsController, + private val transaction: TransactionFilter, + private val auth: RequiredAuthFilter, + @Named("required") private val authLens: RequiredAuthLens, +) : Supplier { + override fun get(): RoutingHttpHandler { + + fun Filter.then(next: ProtectedHandler) = then { req: Request -> + next(req, authLens(req)) + } + + infix fun PathMethod.to(action: ProtectedHandler) = + this to { req: Request -> action(req, authLens(req)) } + + return auth.then( + routes( + "/settings" bind GET to settingsController::settings, + "/settings" bind POST to transaction.then(settingsController::settings), + "/export" bind POST to settingsController::export, + ) + ) + } +} diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/utils/StaticFilesResolver.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/utils/StaticFilesResolver.kt index 4e0dee5..1d0d8a2 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/utils/StaticFilesResolver.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/utils/StaticFilesResolver.kt @@ -3,11 +3,13 @@ package be.simplenotes.app.utils import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import javax.inject.Singleton interface StaticFileResolver { fun resolve(name: String): String? } +@Singleton class StaticFileResolverImpl(json: Json) : StaticFileResolver { private val mappings: Map diff --git a/simplenotes-app/src/main/resources/logback.xml b/simplenotes-app/src/main/resources/logback.xml index d94bc53..a2bdf9f 100644 --- a/simplenotes-app/src/main/resources/logback.xml +++ b/simplenotes-app/src/main/resources/logback.xml @@ -13,4 +13,5 @@ + diff --git a/simplenotes-app/src/test/kotlin/be/simplenotes/app/filters/AuthFilterTest.kt b/simplenotes-app/src/test/kotlin/be/simplenotes/app/filters/RequiredAuthFilterTest.kt similarity index 65% rename from simplenotes-app/src/test/kotlin/be/simplenotes/app/filters/AuthFilterTest.kt rename to simplenotes-app/src/test/kotlin/be/simplenotes/app/filters/RequiredAuthFilterTest.kt index 8461c94..93331fb 100644 --- a/simplenotes-app/src/test/kotlin/be/simplenotes/app/filters/AuthFilterTest.kt +++ b/simplenotes-app/src/test/kotlin/be/simplenotes/app/filters/RequiredAuthFilterTest.kt @@ -1,15 +1,23 @@ package be.simplenotes.app.filters +import be.simplenotes.app.filters.auth.OptionalAuthFilter +import be.simplenotes.app.filters.auth.OptionalAuthLens +import be.simplenotes.app.filters.auth.RequiredAuthFilter +import be.simplenotes.app.filters.auth.RequiredAuthLens import be.simplenotes.config.JwtConfig -import be.simplenotes.domain.security.JwtPayloadExtractor import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.types.LoggedInUser import com.natpryce.hamkrest.assertion.assertThat -import org.http4k.core.* +import io.micronaut.context.BeanContext +import io.micronaut.inject.qualifiers.Qualifiers import org.http4k.core.Method.GET +import org.http4k.core.Request +import org.http4k.core.RequestContexts +import org.http4k.core.Response import org.http4k.core.Status.Companion.FOUND import org.http4k.core.Status.Companion.OK import org.http4k.core.cookie.cookie +import org.http4k.core.then import org.http4k.filter.ServerFilters import org.http4k.hamkrest.hasBody import org.http4k.hamkrest.hasHeader @@ -20,22 +28,36 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import java.util.concurrent.TimeUnit -internal class AuthFilterTest { +internal class RequiredAuthFilterTest { // region setup private val jwtConfig = JwtConfig("secret", 1, TimeUnit.HOURS) private val simpleJwt = SimpleJwt(jwtConfig) - private val extractor = JwtPayloadExtractor(simpleJwt) - private val ctx = RequestContexts() - private val requiredAuth = AuthFilter(extractor, AuthType.Required, ctx) - private val optionalAuth = AuthFilter(extractor, AuthType.Optional, ctx) - private val echoJwtPayloadHandler = { request: Request -> Response(OK).body(request.jwtPayload(ctx).toString()) } + private val beanCtx = BeanContext.build() + .registerSingleton(jwtConfig) + .start() + + private inline fun BeanContext.getBean(): T = getBean(T::class.java) + private inline fun BeanContext.getBean(name: String): T = + getBean(T::class.java, Qualifiers.byName(name)) + + private val requiredAuth = beanCtx.getBean() + private val requiredLens = beanCtx.getBean("required") + + private val optionalAuth = beanCtx.getBean() + private val optionalLens = beanCtx.getBean("optional") + + private val ctx = beanCtx.getBean() private val app = ServerFilters.InitialiseRequestContext(ctx).then( routes( - "/optional" bind GET to optionalAuth.then(echoJwtPayloadHandler), - "/protected" bind GET to requiredAuth.then(echoJwtPayloadHandler) + "/optional" bind GET to optionalAuth.then { request: Request -> + Response(OK).body(optionalLens(request).toString()) + }, + "/protected" bind GET to requiredAuth.then { request: Request -> + Response(OK).body(requiredLens(request).toString()) + } ) ) // endregion diff --git a/simplenotes-config/build.gradle.kts b/simplenotes-config/build.gradle.kts index 17e2f5f..f2970d0 100644 --- a/simplenotes-config/build.gradle.kts +++ b/simplenotes-config/build.gradle.kts @@ -2,8 +2,10 @@ import be.simplenotes.Libs plugins { id("be.simplenotes.base") + kotlin("kapt") } dependencies { - implementation(Libs.koinCore) + implementation(Libs.micronaut) + kapt(Libs.micronautProcessor) } diff --git a/simplenotes-config/src/main/kotlin/be/simplenotes/config/ConfigLoader.kt b/simplenotes-config/src/main/kotlin/be/simplenotes/config/ConfigLoader.kt index 1e9c156..c8bf4b2 100644 --- a/simplenotes-config/src/main/kotlin/be/simplenotes/config/ConfigLoader.kt +++ b/simplenotes-config/src/main/kotlin/be/simplenotes/config/ConfigLoader.kt @@ -2,7 +2,9 @@ package be.simplenotes.config import java.util.* import java.util.concurrent.TimeUnit +import javax.inject.Singleton +@Singleton class ConfigLoader { //region Config loading private val properties: Properties = javaClass diff --git a/simplenotes-config/src/main/kotlin/be/simplenotes/config/ConfigModule.kt b/simplenotes-config/src/main/kotlin/be/simplenotes/config/ConfigModule.kt index 3c3ad59..64e6010 100644 --- a/simplenotes-config/src/main/kotlin/be/simplenotes/config/ConfigModule.kt +++ b/simplenotes-config/src/main/kotlin/be/simplenotes/config/ConfigModule.kt @@ -1,10 +1,17 @@ package be.simplenotes.config -import org.koin.dsl.module +import io.micronaut.context.annotation.Factory +import javax.inject.Singleton -val configModule = module { - single { ConfigLoader() } - single { get().dataSourceConfig } - single { get().jwtConfig } - single { get().serverConfig } +@Factory +class ConfigModule { + + @Singleton + internal fun dataSourceConfig(configLoader: ConfigLoader) = configLoader.dataSourceConfig + + @Singleton + internal fun jwtConfig(configLoader: ConfigLoader) = configLoader.jwtConfig + + @Singleton + internal fun serverConfig(configLoader: ConfigLoader) = configLoader.serverConfig } diff --git a/simplenotes-domain/build.gradle.kts b/simplenotes-domain/build.gradle.kts index c1e564a..89b2cc5 100644 --- a/simplenotes-domain/build.gradle.kts +++ b/simplenotes-domain/build.gradle.kts @@ -3,6 +3,7 @@ import be.simplenotes.Libs plugins { id("be.simplenotes.base") id("be.simplenotes.kotlinx-serialization") + kotlin("kapt") } dependencies { @@ -11,8 +12,10 @@ dependencies { implementation(project(":simplenotes-persistance")) implementation(project(":simplenotes-search")) + implementation(Libs.micronaut) + kapt(Libs.micronautProcessor) + implementation(Libs.kotlinxSerializationJson) - implementation(Libs.koinCore) implementation(Libs.arrowCoreData) implementation(Libs.konform) implementation(Libs.jbcrypt) diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/DomainModule.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/DomainModule.kt deleted file mode 100644 index eedf3aa..0000000 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/DomainModule.kt +++ /dev/null @@ -1,37 +0,0 @@ -package be.simplenotes.domain - -import be.simplenotes.domain.security.BcryptPasswordHash -import be.simplenotes.domain.security.JwtPayloadExtractor -import be.simplenotes.domain.security.PasswordHash -import be.simplenotes.domain.security.SimpleJwt -import be.simplenotes.domain.usecases.NoteService -import be.simplenotes.domain.usecases.UserService -import be.simplenotes.domain.usecases.export.ExportUseCase -import be.simplenotes.domain.usecases.export.ExportUseCaseImpl -import be.simplenotes.domain.usecases.markdown.MarkdownConverter -import be.simplenotes.domain.usecases.markdown.MarkdownConverterImpl -import be.simplenotes.domain.usecases.users.delete.DeleteUseCase -import be.simplenotes.domain.usecases.users.delete.DeleteUseCaseImpl -import be.simplenotes.domain.usecases.users.login.LoginUseCase -import be.simplenotes.domain.usecases.users.login.LoginUseCaseImpl -import be.simplenotes.domain.usecases.users.register.RegisterUseCase -import be.simplenotes.domain.usecases.users.register.RegisterUseCaseImpl -import org.koin.dsl.module - -val domainModule = module { - single { LoginUseCaseImpl(get(), get(), get()) } - single { RegisterUseCaseImpl(get(), get()) } - single { DeleteUseCaseImpl(get(), get(), get()) } - single { UserService(get(), get(), get(), get()) } - single { BcryptPasswordHash() } - single { SimpleJwt(get()) } - single { JwtPayloadExtractor(get()) } - single { - NoteService(get(), get(), get(), get()).apply { - dropAllIndexes() - indexAll() - } - } - single { MarkdownConverterImpl() } - single { ExportUseCaseImpl(get(), get()) } -} diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/JwtPayloadExtractor.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/JwtPayloadExtractor.kt index e316bdc..f46d935 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/JwtPayloadExtractor.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/JwtPayloadExtractor.kt @@ -2,7 +2,9 @@ package be.simplenotes.domain.security import be.simplenotes.types.LoggedInUser import com.auth0.jwt.exceptions.JWTVerificationException +import javax.inject.Singleton +@Singleton class JwtPayloadExtractor(private val jwt: SimpleJwt) { operator fun invoke(token: String): LoggedInUser? = try { val decodedJWT = jwt.verifier.verify(token) diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/PasswordHash.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/PasswordHash.kt index 13650b5..003a59c 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/PasswordHash.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/PasswordHash.kt @@ -1,13 +1,19 @@ package be.simplenotes.domain.security import org.mindrot.jbcrypt.BCrypt +import javax.inject.Inject +import javax.inject.Singleton internal interface PasswordHash { fun crypt(password: String): String fun verify(password: String, hashedPassword: String): Boolean } -internal class BcryptPasswordHash(test: Boolean = false) : PasswordHash { +@Singleton +internal class BcryptPasswordHash constructor(test: Boolean) : PasswordHash { + @Inject + constructor() : this(false) + private val rounds = if (test) 4 else 10 override fun crypt(password: String) = BCrypt.hashpw(password, BCrypt.gensalt(rounds))!! override fun verify(password: String, hashedPassword: String) = BCrypt.checkpw(password, hashedPassword) diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/SimpleJwt.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/SimpleJwt.kt index e8ced0f..50e12b7 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/SimpleJwt.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/SimpleJwt.kt @@ -7,10 +7,12 @@ import com.auth0.jwt.JWTVerifier import com.auth0.jwt.algorithms.Algorithm import java.util.* import java.util.concurrent.TimeUnit +import javax.inject.Singleton internal const val userIdField = "i" internal const val usernameField = "u" +@Singleton class SimpleJwt(jwtConfig: JwtConfig) { private val validityInMs = TimeUnit.MILLISECONDS.convert(jwtConfig.validity, jwtConfig.timeUnit) private val algorithm = Algorithm.HMAC256(jwtConfig.secret) diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/NoteService.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/NoteService.kt index ceb940e..ce3c9d8 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/NoteService.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/NoteService.kt @@ -12,7 +12,9 @@ import be.simplenotes.types.Note import be.simplenotes.types.PersistedNote import be.simplenotes.types.PersistedNoteMetadata import java.util.* +import javax.inject.Singleton +@Singleton class NoteService( private val markdownConverter: MarkdownConverter, private val noteRepository: NoteRepository, diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/UserService.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/UserService.kt index bc0021e..80fc475 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/UserService.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/UserService.kt @@ -4,7 +4,9 @@ import be.simplenotes.domain.usecases.export.ExportUseCase import be.simplenotes.domain.usecases.users.delete.DeleteUseCase import be.simplenotes.domain.usecases.users.login.LoginUseCase import be.simplenotes.domain.usecases.users.register.RegisterUseCase +import javax.inject.Singleton +@Singleton class UserService( loginUseCase: LoginUseCase, registerUseCase: RegisterUseCase, diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/export/ExportUseCaseImpl.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/export/ExportUseCaseImpl.kt index 14a9a9a..4eadc61 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/export/ExportUseCaseImpl.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/export/ExportUseCaseImpl.kt @@ -2,6 +2,7 @@ package be.simplenotes.domain.usecases.export import be.simplenotes.persistance.repositories.NoteRepository import be.simplenotes.types.ExportedNote +import io.micronaut.context.annotation.Primary import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json import org.apache.commons.compress.archivers.zip.ZipArchiveEntry @@ -9,8 +10,14 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.InputStream +import javax.inject.Singleton -internal class ExportUseCaseImpl(private val noteRepository: NoteRepository, private val json: Json) : ExportUseCase { +@Primary +@Singleton +internal class ExportUseCaseImpl( + private val noteRepository: NoteRepository, + private val json: Json, +) : ExportUseCase { override fun exportAsJson(userId: Int): String { val notes = noteRepository.export(userId) return json.encodeToString(ListSerializer(ExportedNote.serializer()), notes) diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/markdown/MarkdownConverter.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/markdown/MarkdownConverter.kt index 85bdc0e..833e4dc 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/markdown/MarkdownConverter.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/markdown/MarkdownConverter.kt @@ -14,6 +14,7 @@ import io.konform.validation.ValidationErrors import org.yaml.snakeyaml.Yaml import org.yaml.snakeyaml.parser.ParserException import org.yaml.snakeyaml.scanner.ScannerException +import javax.inject.Singleton sealed class MarkdownParsingError object MissingMeta : MarkdownParsingError() @@ -28,6 +29,7 @@ interface MarkdownConverter { fun renderDocument(input: String): Either } +@Singleton internal class MarkdownConverterImpl : MarkdownConverter { private val yamlBoundPattern = "-{3}".toRegex() private fun splitMetaFromDocument(input: String): Either { diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/delete/DeleteUseCaseImpl.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/delete/DeleteUseCaseImpl.kt index d5eddce..d662436 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/delete/DeleteUseCaseImpl.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/delete/DeleteUseCaseImpl.kt @@ -7,7 +7,11 @@ import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.validation.UserValidations import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.search.NoteSearcher +import io.micronaut.context.annotation.Primary +import javax.inject.Singleton +@Primary +@Singleton internal class DeleteUseCaseImpl( private val userRepository: UserRepository, private val passwordHash: PasswordHash, diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/login/LoginUseCaseImpl.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/login/LoginUseCaseImpl.kt index 19f4f4b..6cc88c6 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/login/LoginUseCaseImpl.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/login/LoginUseCaseImpl.kt @@ -8,7 +8,11 @@ import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.validation.UserValidations import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.types.LoggedInUser +import io.micronaut.context.annotation.Primary +import javax.inject.Singleton +@Singleton +@Primary internal class LoginUseCaseImpl( private val userRepository: UserRepository, private val passwordHash: PasswordHash, diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/register/RegisterUseCaseImpl.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/register/RegisterUseCaseImpl.kt index 15657f4..06551c6 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/register/RegisterUseCaseImpl.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/register/RegisterUseCaseImpl.kt @@ -7,7 +7,11 @@ import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.validation.UserValidations import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.types.PersistedUser +import io.micronaut.context.annotation.Primary +import javax.inject.Singleton +@Primary +@Singleton internal class RegisterUseCaseImpl( private val userRepository: UserRepository, private val passwordHash: PasswordHash diff --git a/simplenotes-persistance/build.gradle.kts b/simplenotes-persistance/build.gradle.kts index 12a9d1d..6cea2fd 100644 --- a/simplenotes-persistance/build.gradle.kts +++ b/simplenotes-persistance/build.gradle.kts @@ -10,8 +10,6 @@ dependencies { implementation(project(":simplenotes-types")) implementation(project(":simplenotes-config")) - implementation(Libs.mapstruct) - implementation(Libs.koinCore) implementation(Libs.mariadbClient) implementation(Libs.h2) implementation(Libs.flywayCore) @@ -19,19 +17,36 @@ dependencies { implementation(Libs.ktormCore) implementation(Libs.ktormMysql) + implementation(Libs.mapstruct) kapt(Libs.mapstructProcessor) + implementation(Libs.micronaut) + kapt(Libs.micronautProcessor) + + testImplementation(Libs.micronaut) + kaptTest(Libs.micronautProcessor) + testImplementation(Libs.junit) testImplementation(Libs.assertJ) testImplementation(Libs.logbackClassic) - testImplementation("org.testcontainers:mariadb:1.15.0-rc2") + testImplementation(Libs.mariaTestContainer) testFixturesImplementation(project(":simplenotes-types")) testFixturesImplementation(project(":simplenotes-config")) - testFixturesImplementation("com.github.javafaker:javafaker:1.0.2") - testFixturesImplementation("org.testcontainers:mariadb:1.15.0-rc2") - testFixturesImplementation(Libs.koinCore) + testFixturesImplementation(project(":simplenotes-persistance")) + + testFixturesImplementation(Libs.micronaut) + kaptTestFixtures(Libs.micronautProcessor) + + testFixturesImplementation(Libs.faker) { + exclude(group = "org.yaml") + } + + testFixturesImplementation(Libs.snakeyaml) + + testFixturesImplementation(Libs.mariaTestContainer) testFixturesImplementation(Libs.flywayCore) testFixturesImplementation(Libs.junit) - + testFixturesImplementation(Libs.ktormCore) + testFixturesImplementation(Libs.hikariCP) } diff --git a/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/HealthCheck.kt b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/HealthCheck.kt index 30ebd68..214be9a 100644 --- a/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/HealthCheck.kt +++ b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/HealthCheck.kt @@ -7,11 +7,13 @@ import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.database.asIterable import me.liuwj.ktorm.database.use import java.sql.SQLTransientException +import javax.inject.Singleton interface DbHealthCheck { fun isOk(): Boolean } +@Singleton internal class DbHealthCheckImpl( private val db: Database, private val dataSourceConfig: DataSourceConfig, diff --git a/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/Migrations.kt b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/Migrations.kt index 1e4ac3e..7681c15 100644 --- a/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/Migrations.kt +++ b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/Migrations.kt @@ -4,12 +4,14 @@ import be.simplenotes.config.DataSourceConfig import be.simplenotes.persistance.utils.DbType import be.simplenotes.persistance.utils.type import org.flywaydb.core.Flyway +import javax.inject.Singleton import javax.sql.DataSource interface DbMigrations { fun migrate() } +@Singleton internal class DbMigrationsImpl( private val dataSource: DataSource, private val dataSourceConfig: DataSourceConfig, diff --git a/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/PersistanceModule.kt b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/PersistanceModule.kt index 3238e4a..6ddc2a9 100644 --- a/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/PersistanceModule.kt +++ b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/PersistanceModule.kt @@ -2,46 +2,42 @@ package be.simplenotes.persistance import be.simplenotes.config.DataSourceConfig import be.simplenotes.persistance.converters.NoteConverter -import be.simplenotes.persistance.converters.NoteConverterImpl import be.simplenotes.persistance.converters.UserConverter -import be.simplenotes.persistance.converters.UserConverterImpl -import be.simplenotes.persistance.notes.NoteRepositoryImpl -import be.simplenotes.persistance.repositories.NoteRepository -import be.simplenotes.persistance.repositories.UserRepository -import be.simplenotes.persistance.users.UserRepositoryImpl import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory import me.liuwj.ktorm.database.Database -import org.koin.dsl.bind -import org.koin.dsl.module -import org.koin.dsl.onClose +import org.mapstruct.factory.Mappers +import javax.inject.Singleton import javax.sql.DataSource -private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource { - val hikariConfig = HikariConfig().also { - it.jdbcUrl = conf.jdbcUrl - it.driverClassName = conf.driverClassName - it.username = conf.username - it.password = conf.password - it.maximumPoolSize = conf.maximumPoolSize - it.connectionTimeout = conf.connectionTimeout - } - return HikariDataSource(hikariConfig) -} +@Factory +class PersistanceModule { -val migrationModule = module { - single { DbMigrationsImpl(get(), get()) } -} + @Singleton + internal fun noteConverter() = Mappers.getMapper(NoteConverter::class.java) -val persistanceModule = module { - single { NoteConverterImpl() } - single { UserConverterImpl() } - single { UserRepositoryImpl(get(), get()) } - single { NoteRepositoryImpl(get(), get()) } - single { hikariDataSource(get()) } bind DataSource::class onClose { it?.close() } - single { - get().migrate() - Database.connect(get()) + @Singleton + internal fun userConverter() = Mappers.getMapper(UserConverter::class.java) + + @Singleton + internal fun database(migrations: DbMigrations, dataSource: DataSource): Database { + migrations.migrate() + return Database.connect(dataSource) + } + + @Singleton + @Bean(preDestroy = "close") + internal fun dataSource(conf: DataSourceConfig): HikariDataSource { + val hikariConfig = HikariConfig().also { + it.jdbcUrl = conf.jdbcUrl + it.driverClassName = conf.driverClassName + it.username = conf.username + it.password = conf.password + it.maximumPoolSize = conf.maximumPoolSize + it.connectionTimeout = conf.connectionTimeout + } + return HikariDataSource(hikariConfig) } - single { DbHealthCheckImpl(get(), get()) } } diff --git a/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/notes/NoteRepositoryImpl.kt b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/notes/NoteRepositoryImpl.kt index 45b86e1..0bf9f3d 100644 --- a/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/notes/NoteRepositoryImpl.kt +++ b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/notes/NoteRepositoryImpl.kt @@ -11,9 +11,14 @@ import me.liuwj.ktorm.dsl.* import me.liuwj.ktorm.entity.* import java.time.LocalDateTime import java.util.* +import javax.inject.Singleton import kotlin.collections.HashMap -internal class NoteRepositoryImpl(private val db: Database, private val converter: NoteConverter) : NoteRepository { +@Singleton +internal class NoteRepositoryImpl( + private val db: Database, + private val converter: NoteConverter, +) : NoteRepository { @Throws(IllegalArgumentException::class) override fun findAll( diff --git a/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/users/UserRepositoryImpl.kt b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/users/UserRepositoryImpl.kt index a07b9ec..92c969d 100644 --- a/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/users/UserRepositoryImpl.kt +++ b/simplenotes-persistance/src/main/kotlin/be/simplenotes/persistance/users/UserRepositoryImpl.kt @@ -9,8 +9,13 @@ import me.liuwj.ktorm.dsl.* import me.liuwj.ktorm.entity.any import me.liuwj.ktorm.entity.find import java.sql.SQLIntegrityConstraintViolationException +import javax.inject.Singleton -internal class UserRepositoryImpl(private val db: Database, private val converter: UserConverter) : UserRepository { +@Singleton +internal class UserRepositoryImpl( + private val db: Database, + private val converter: UserConverter, +) : UserRepository { override fun create(user: User): PersistedUser? { return try { val id = db.insertAndGenerateKey(Users) { diff --git a/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DataSources.kt b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DataSources.kt similarity index 100% rename from simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DataSources.kt rename to simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DataSources.kt diff --git a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DbHealthCheckImplTest.kt b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DbHealthCheckImplTest.kt index a1d29aa..0032368 100644 --- a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DbHealthCheckImplTest.kt +++ b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DbHealthCheckImplTest.kt @@ -7,19 +7,16 @@ import org.junit.jupiter.api.parallel.ResourceLock @ResourceLock("h2") class H2DbHealthCheckImplTest : DbTest() { - private val healthCheck = koin.get() override fun dataSourceConfig() = h2dataSourceConfig() @Test fun healthCheck() { - assertThat(healthCheck.isOk()).isTrue + assertThat(beanContext.getBean().isOk()).isTrue } - } @ResourceLock("mariadb") class MariaDbHealthCheckImplTest : DbTest() { - private val healthCheck = koin.get() lateinit var mariaDB: KMariadbContainer override fun dataSourceConfig(): DataSourceConfig { @@ -30,9 +27,9 @@ class MariaDbHealthCheckImplTest : DbTest() { @Test fun healthCheck() { + val healthCheck = beanContext.getBean() assertThat(healthCheck.isOk()).isTrue mariaDB.stop() assertThat(healthCheck.isOk()).isFalse } - } diff --git a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DbTest.kt b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DbTest.kt new file mode 100644 index 0000000..cf065b0 --- /dev/null +++ b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/DbTest.kt @@ -0,0 +1,44 @@ +package be.simplenotes.persistance + +import be.simplenotes.config.DataSourceConfig +import io.micronaut.context.BeanContext +import org.flywaydb.core.Flyway +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import javax.sql.DataSource + +abstract class DbTest { + + abstract fun dataSourceConfig(): DataSourceConfig + + val beanContext = BeanContext + .build() + + inline fun BeanContext.getBean(): T = getBean(T::class.java) + + @BeforeAll + fun setComponent() { + beanContext + .registerSingleton(dataSourceConfig()) + .start() + } + + @BeforeEach + fun beforeEach() { + val migration = beanContext.getBean() + val dataSource = beanContext.getBean() + + Flyway.configure() + .dataSource(dataSource) + .load() + .clean() + + migration.migrate() + } + + @AfterAll + fun closeCtx() { + beanContext.close() + } +} diff --git a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/BaseNoteRepositoryImplTest.kt b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/BaseNoteRepositoryImplTest.kt index 051d3c6..1fce436 100644 --- a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/BaseNoteRepositoryImplTest.kt +++ b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/notes/BaseNoteRepositoryImplTest.kt @@ -21,15 +21,18 @@ import java.sql.SQLIntegrityConstraintViolationException internal abstract class BaseNoteRepositoryImplTest : DbTest() { - private val noteRepo = koin.get() - private val userRepo = koin.get() - private val db = koin.get() + private lateinit var noteRepo: NoteRepository + private lateinit var userRepo: UserRepository + private lateinit var db: Database private lateinit var user1: PersistedUser private lateinit var user2: PersistedUser @BeforeEach fun insertUsers() { + noteRepo = beanContext.getBean() + userRepo = beanContext.getBean() + db = beanContext.getBean() user1 = userRepo.createFakeUser()!! user2 = userRepo.createFakeUser()!! } diff --git a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/BaseUserRepositoryImplTest.kt b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/BaseUserRepositoryImplTest.kt index 0103c6f..3ab2eb3 100644 --- a/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/BaseUserRepositoryImplTest.kt +++ b/simplenotes-persistance/src/test/kotlin/be/simplenotes/persistance/users/BaseUserRepositoryImplTest.kt @@ -7,13 +7,20 @@ import me.liuwj.ktorm.dsl.eq import me.liuwj.ktorm.entity.find import me.liuwj.ktorm.entity.toList import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test internal abstract class BaseUserRepositoryImplTest : DbTest() { - private val userRepo = koin.get() - private val db = koin.get() + private lateinit var userRepo: UserRepository + private lateinit var db: Database + + @BeforeEach + fun setup() { + userRepo = beanContext.getBean() + db = beanContext.getBean() + } @Test fun `insert user`() { diff --git a/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DbTest.kt b/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DbTest.kt index 6ba9d22..0415cca 100644 --- a/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DbTest.kt +++ b/simplenotes-persistance/src/testFixtures/kotlin/be/simplenotes/persistance/DbTest.kt @@ -1,40 +1,35 @@ package be.simplenotes.persistance import be.simplenotes.config.DataSourceConfig -import org.flywaydb.core.Flyway.* -import org.junit.jupiter.api.AfterAll +import io.micronaut.context.BeanContext +import org.flywaydb.core.Flyway +import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach -import org.koin.dsl.koinApplication -import org.koin.dsl.module import javax.sql.DataSource abstract class DbTest { abstract fun dataSourceConfig(): DataSourceConfig - private val testModule = module { - single { dataSourceConfig() } + val beanContext = BeanContext.build() + + inline fun BeanContext.getBean(): T = getBean(T::class.java) + + @BeforeAll + fun setComponent() { + beanContext.registerSingleton(dataSourceConfig()) } - private val koinApp = koinApplication { - modules(persistanceModule, migrationModule, testModule) - } - - val koin = koinApp.koin - - @AfterAll - fun afterAll() = koinApp.close() - @BeforeEach fun beforeEach() { - val migration = koin.get() - val dataSource = koin.get() - configure() + val migration = beanContext.getBean() + val dataSource = beanContext.getBean() + + Flyway.configure() .dataSource(dataSource) .load() .clean() migration.migrate() } - } diff --git a/simplenotes-search/build.gradle.kts b/simplenotes-search/build.gradle.kts index c1d0953..d5b3c87 100644 --- a/simplenotes-search/build.gradle.kts +++ b/simplenotes-search/build.gradle.kts @@ -2,6 +2,7 @@ import be.simplenotes.Libs plugins { id("be.simplenotes.base") + kotlin("kapt") } dependencies { @@ -11,7 +12,9 @@ dependencies { implementation(Libs.luceneQueryParser) implementation(Libs.luceneAnalyzersCommon) implementation(Libs.slf4jApi) - implementation(Libs.koinCore) + + implementation(Libs.micronaut) + kapt(Libs.micronautProcessor) testImplementation(Libs.junit) testImplementation(Libs.assertJ) diff --git a/simplenotes-search/src/main/kotlin/be/simplenotes/search/NoteSearcherImpl.kt b/simplenotes-search/src/main/kotlin/be/simplenotes/search/NoteSearcherImpl.kt index a5f5b44..26b7949 100644 --- a/simplenotes-search/src/main/kotlin/be/simplenotes/search/NoteSearcherImpl.kt +++ b/simplenotes-search/src/main/kotlin/be/simplenotes/search/NoteSearcherImpl.kt @@ -12,8 +12,17 @@ import org.slf4j.LoggerFactory import java.io.File import java.nio.file.Path import java.util.* +import javax.inject.Named +import javax.inject.Singleton + +@Singleton +internal class NoteSearcherImpl( + @Named("search-index") + basePath: Path, +) : NoteSearcher { + + constructor() : this(Path.of("/tmp", "lucene")) -internal class NoteSearcherImpl(basePath: Path = Path.of("/tmp", "lucene")) : NoteSearcher { private val baseFile = basePath.toFile() private val logger = LoggerFactory.getLogger(javaClass) diff --git a/simplenotes-search/src/main/kotlin/be/simplenotes/search/SeachModule.kt b/simplenotes-search/src/main/kotlin/be/simplenotes/search/SeachModule.kt deleted file mode 100644 index 5fd12e9..0000000 --- a/simplenotes-search/src/main/kotlin/be/simplenotes/search/SeachModule.kt +++ /dev/null @@ -1,8 +0,0 @@ -package be.simplenotes.search - -import org.koin.dsl.module -import java.nio.file.Path - -val searchModule = module { - single { NoteSearcherImpl(Path.of(".lucene")) } -} diff --git a/simplenotes-search/src/main/kotlin/be/simplenotes/search/SearchModule.kt b/simplenotes-search/src/main/kotlin/be/simplenotes/search/SearchModule.kt new file mode 100644 index 0000000..4ab7866 --- /dev/null +++ b/simplenotes-search/src/main/kotlin/be/simplenotes/search/SearchModule.kt @@ -0,0 +1,14 @@ +package be.simplenotes.search + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Prototype +import java.nio.file.Path +import javax.inject.Named + +@Factory +class SearchModule { + + @Named("search-index") + @Prototype + internal fun luceneIndex() = Path.of(".lucene") +} diff --git a/simplenotes-views/build.gradle.kts b/simplenotes-views/build.gradle.kts index 0185584..6a49a8b 100644 --- a/simplenotes-views/build.gradle.kts +++ b/simplenotes-views/build.gradle.kts @@ -2,13 +2,16 @@ import be.simplenotes.Libs plugins { id("be.simplenotes.base") + kotlin("kapt") } dependencies { implementation(project(":simplenotes-types")) - implementation(Libs.koinCore) implementation(Libs.konform) implementation(Libs.kotlinxHtml) implementation(Libs.prettytime) + + implementation(Libs.micronaut) + kapt(Libs.micronautProcessor) } diff --git a/simplenotes-views/src/main/kotlin/be/simplenotes/views/BaseView.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/BaseView.kt index 64f95ea..30d2c6b 100644 --- a/simplenotes-views/src/main/kotlin/be/simplenotes/views/BaseView.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/BaseView.kt @@ -3,8 +3,11 @@ package be.simplenotes.views import be.simplenotes.types.LoggedInUser import kotlinx.html.* import kotlinx.html.ThScope.col +import javax.inject.Named +import javax.inject.Singleton -class BaseView(styles: String) : View(styles) { +@Singleton +class BaseView(@Named("styles") styles: String) : View(styles) { fun renderHome(loggedInUser: LoggedInUser?) = renderPage( title = "Home", description = "A fast and simple note taking website", diff --git a/simplenotes-views/src/main/kotlin/be/simplenotes/views/ErrorView.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/ErrorView.kt index 58e1f50..aaf8802 100644 --- a/simplenotes-views/src/main/kotlin/be/simplenotes/views/ErrorView.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/ErrorView.kt @@ -4,8 +4,11 @@ import be.simplenotes.views.components.Alert import be.simplenotes.views.components.alert import kotlinx.html.a import kotlinx.html.div +import javax.inject.Named +import javax.inject.Singleton -class ErrorView(styles: String) : View(styles) { +@Singleton +class ErrorView(@Named("styles") styles: String) : View(styles) { enum class Type(val title: String) { SqlTransientError("Database unavailable"), diff --git a/simplenotes-views/src/main/kotlin/be/simplenotes/views/NoteView.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/NoteView.kt index 0c9e944..b98760c 100644 --- a/simplenotes-views/src/main/kotlin/be/simplenotes/views/NoteView.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/NoteView.kt @@ -6,8 +6,11 @@ import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.views.components.* import io.konform.validation.ValidationError import kotlinx.html.* +import javax.inject.Named +import javax.inject.Singleton -class NoteView(styles: String) : View(styles) { +@Singleton +class NoteView(@Named("styles") styles: String) : View(styles) { fun noteEditor( loggedInUser: LoggedInUser, diff --git a/simplenotes-views/src/main/kotlin/be/simplenotes/views/SettingView.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/SettingView.kt index cdc736f..81876cb 100644 --- a/simplenotes-views/src/main/kotlin/be/simplenotes/views/SettingView.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/SettingView.kt @@ -8,8 +8,11 @@ import be.simplenotes.views.extensions.summary import io.konform.validation.ValidationError import kotlinx.html.* import kotlinx.html.ButtonType.submit +import javax.inject.Named +import javax.inject.Singleton -class SettingView(styles: String) : View(styles) { +@Singleton +class SettingView(@Named("styles") styles: String) : View(styles) { fun settings( loggedInUser: LoggedInUser, diff --git a/simplenotes-views/src/main/kotlin/be/simplenotes/views/UserView.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/UserView.kt index b8bbdf6..c6661a2 100644 --- a/simplenotes-views/src/main/kotlin/be/simplenotes/views/UserView.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/UserView.kt @@ -7,8 +7,11 @@ import be.simplenotes.views.components.input import be.simplenotes.views.components.submitButton import io.konform.validation.ValidationError import kotlinx.html.* +import javax.inject.Named +import javax.inject.Singleton -class UserView(styles: String) : View(styles) { +@Singleton +class UserView(@Named("styles") styles: String) : View(styles) { fun register( loggedInUser: LoggedInUser?, error: String? = null, diff --git a/simplenotes-views/src/main/kotlin/be/simplenotes/views/ViewModule.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/ViewModule.kt deleted file mode 100644 index 27699a4..0000000 --- a/simplenotes-views/src/main/kotlin/be/simplenotes/views/ViewModule.kt +++ /dev/null @@ -1,12 +0,0 @@ -package be.simplenotes.views - -import org.koin.core.qualifier.named -import org.koin.dsl.module - -val viewModule = module { - single { ErrorView(get(named("styles"))) } - single { UserView(get(named("styles"))) } - single { BaseView(get(named("styles"))) } - single { SettingView(get(named("styles"))) } - single { NoteView(get(named("styles"))) } -}