From a4bf998c5b6a09741ee7d7ec9ffaf02097734027 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Wed, 3 Mar 2021 16:36:08 +0100 Subject: [PATCH] Remove Boilerplate Use-case thingy --- .editorconfig | 5 +- app/src/api/ApiNoteController.kt | 4 +- app/src/api/ApiUserController.kt | 4 +- app/src/controllers/HealthCheckController.kt | 2 +- app/src/controllers/NoteController.kt | 18 ++-- app/src/controllers/SettingsController.kt | 14 +-- app/src/controllers/UserController.kt | 16 ++- .../be/simplenotes/java-convention.gradle.kts | 6 +- .../simplenotes/kotlin-convention.gradle.kts | 4 +- .../ExportUseCaseImpl.kt => ExportService.kt} | 16 +-- .../src/{usecases => }/HealthCheckService.kt | 2 +- domain/src/Index.kt | 1 - ...ownConverterImpl.kt => MarkdownService.kt} | 31 ++++-- domain/src/{usecases => }/NoteService.kt | 28 ++--- domain/src/UserService.kt | 92 ++++++++++++++++ .../markdown => modules}/FlexmarkFactory.kt | 2 +- domain/src/usecases/UserService.kt | 18 ---- domain/src/usecases/export/ExportUseCase.kt | 8 -- .../usecases/markdown/MarkdownConverter.kt | 16 --- .../src/usecases/search/SearchTermsParser.kt | 96 ----------------- .../users/delete/DeleteUseCaseImpl.kt | 35 ------ .../usecases/users/delete/DeleteUsecase.kt | 16 --- .../usecases/users/login/LoginUseCaseImpl.kt | 28 ----- .../src/usecases/users/login/LoginUsecase.kt | 19 ---- .../users/register/RegisterUseCaseImpl.kt | 28 ----- .../users/register/RegisterUsecase.kt | 16 --- domain/src/utils/SearchTermsParser.kt | 94 ++++++++++++++++ domain/src/validation/NoteValidations.kt | 6 +- domain/src/validation/UserValidations.kt | 15 +-- domain/test/Index.kt | 1 - domain/test/UserServiceTest.kt | 101 ++++++++++++++++++ .../security/LoggedInUserExtractorTest.kt | 2 +- .../users/login/LoginUseCaseImplTest.kt | 65 ----------- .../users/register/RegisterUseCaseImplTest.kt | 54 ---------- .../SearchTermsParserKtTest.kt | 2 +- domain/test/validation/UserValidationsTest.kt | 10 +- views/src/BaseView.kt | 2 +- 37 files changed, 382 insertions(+), 495 deletions(-) rename domain/src/{usecases/export/ExportUseCaseImpl.kt => ExportService.kt} (87%) rename domain/src/{usecases => }/HealthCheckService.kt (88%) delete mode 100644 domain/src/Index.kt rename domain/src/{usecases/markdown/MarkdownConverterImpl.kt => MarkdownService.kt} (63%) rename domain/src/{usecases => }/NoteService.kt (83%) create mode 100644 domain/src/UserService.kt rename domain/src/{usecases/markdown => modules}/FlexmarkFactory.kt (95%) delete mode 100644 domain/src/usecases/UserService.kt delete mode 100644 domain/src/usecases/export/ExportUseCase.kt delete mode 100644 domain/src/usecases/markdown/MarkdownConverter.kt delete mode 100644 domain/src/usecases/search/SearchTermsParser.kt delete mode 100644 domain/src/usecases/users/delete/DeleteUseCaseImpl.kt delete mode 100644 domain/src/usecases/users/delete/DeleteUsecase.kt delete mode 100644 domain/src/usecases/users/login/LoginUseCaseImpl.kt delete mode 100644 domain/src/usecases/users/login/LoginUsecase.kt delete mode 100644 domain/src/usecases/users/register/RegisterUseCaseImpl.kt delete mode 100644 domain/src/usecases/users/register/RegisterUsecase.kt create mode 100644 domain/src/utils/SearchTermsParser.kt delete mode 100644 domain/test/Index.kt create mode 100644 domain/test/UserServiceTest.kt delete mode 100644 domain/test/usecases/users/login/LoginUseCaseImplTest.kt delete mode 100644 domain/test/usecases/users/register/RegisterUseCaseImplTest.kt rename domain/test/{usecases/search => utils}/SearchTermsParserKtTest.kt (98%) diff --git a/.editorconfig b/.editorconfig index 0af84ce..7183a1a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,9 +9,8 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.{kt, kts}] +[*.{kt,kts}] indent_size = 4 insert_final_newline = true max_line_length = 120 -disabled_rules = no-wildcard-imports -kotlin_imports_layout = idea +disabled_rules = no-wildcard-imports,import-ordering diff --git a/app/src/api/ApiNoteController.kt b/app/src/api/ApiNoteController.kt index 1bfb695..a2fabd6 100644 --- a/app/src/api/ApiNoteController.kt +++ b/app/src/api/ApiNoteController.kt @@ -1,7 +1,7 @@ package be.simplenotes.app.api import be.simplenotes.app.extensions.auto -import be.simplenotes.domain.usecases.NoteService +import be.simplenotes.domain.NoteService import be.simplenotes.types.LoggedInUser import be.simplenotes.types.PersistedNote import be.simplenotes.types.PersistedNoteMetadata @@ -76,4 +76,4 @@ data class NoteContent(val content: String) data class UuidContent(@Contextual val uuid: UUID) @Serializable -data class SearchContent(@Contextual val query: String) +data class SearchContent(val query: String) diff --git a/app/src/api/ApiUserController.kt b/app/src/api/ApiUserController.kt index b963f2d..bf1ab3a 100644 --- a/app/src/api/ApiUserController.kt +++ b/app/src/api/ApiUserController.kt @@ -1,8 +1,8 @@ package be.simplenotes.app.api import be.simplenotes.app.extensions.auto -import be.simplenotes.domain.usecases.UserService -import be.simplenotes.domain.usecases.users.login.LoginForm +import be.simplenotes.domain.LoginForm +import be.simplenotes.domain.UserService import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import org.http4k.core.Request diff --git a/app/src/controllers/HealthCheckController.kt b/app/src/controllers/HealthCheckController.kt index 33c827e..0184a32 100644 --- a/app/src/controllers/HealthCheckController.kt +++ b/app/src/controllers/HealthCheckController.kt @@ -1,6 +1,6 @@ package be.simplenotes.app.controllers -import be.simplenotes.domain.usecases.HealthCheckService +import be.simplenotes.domain.HealthCheckService import org.http4k.core.Request import org.http4k.core.Response import org.http4k.core.Status.Companion.OK diff --git a/app/src/controllers/NoteController.kt b/app/src/controllers/NoteController.kt index f0b4286..d43cd5d 100644 --- a/app/src/controllers/NoteController.kt +++ b/app/src/controllers/NoteController.kt @@ -2,10 +2,8 @@ package be.simplenotes.app.controllers import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.redirect -import be.simplenotes.domain.usecases.NoteService -import be.simplenotes.domain.usecases.markdown.InvalidMeta -import be.simplenotes.domain.usecases.markdown.MissingMeta -import be.simplenotes.domain.usecases.markdown.ValidationError +import be.simplenotes.domain.MarkdownParsingError +import be.simplenotes.domain.NoteService import be.simplenotes.types.LoggedInUser import be.simplenotes.views.NoteView import org.http4k.core.Method @@ -34,17 +32,17 @@ class NoteController( return noteService.create(loggedInUser, markdownForm).fold( { val html = when (it) { - MissingMeta -> view.noteEditor( + MarkdownParsingError.MissingMeta -> view.noteEditor( loggedInUser, error = "Missing note metadata", textarea = markdownForm ) - InvalidMeta -> view.noteEditor( + MarkdownParsingError.InvalidMeta -> view.noteEditor( loggedInUser, error = "Invalid note metadata", textarea = markdownForm ) - is ValidationError -> view.noteEditor( + is MarkdownParsingError.ValidationError -> view.noteEditor( loggedInUser, validationErrors = it.validationErrors, textarea = markdownForm @@ -113,17 +111,17 @@ class NoteController( return noteService.update(loggedInUser, note.uuid, markdownForm).fold( { val html = when (it) { - MissingMeta -> view.noteEditor( + MarkdownParsingError.MissingMeta -> view.noteEditor( loggedInUser, error = "Missing note metadata", textarea = markdownForm ) - InvalidMeta -> view.noteEditor( + MarkdownParsingError.InvalidMeta -> view.noteEditor( loggedInUser, error = "Invalid note metadata", textarea = markdownForm ) - is ValidationError -> view.noteEditor( + is MarkdownParsingError.ValidationError -> view.noteEditor( loggedInUser, validationErrors = it.validationErrors, textarea = markdownForm diff --git a/app/src/controllers/SettingsController.kt b/app/src/controllers/SettingsController.kt index ef32d6c..63f5567 100644 --- a/app/src/controllers/SettingsController.kt +++ b/app/src/controllers/SettingsController.kt @@ -2,9 +2,10 @@ package be.simplenotes.app.controllers import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.redirect -import be.simplenotes.domain.usecases.UserService -import be.simplenotes.domain.usecases.users.delete.DeleteError -import be.simplenotes.domain.usecases.users.delete.DeleteForm +import be.simplenotes.domain.DeleteError +import be.simplenotes.domain.DeleteForm +import be.simplenotes.domain.ExportService +import be.simplenotes.domain.UserService import be.simplenotes.types.LoggedInUser import be.simplenotes.views.SettingView import org.http4k.core.* @@ -15,6 +16,7 @@ import javax.inject.Singleton @Singleton class SettingsController( private val userService: UserService, + private val exportService: ExportService, private val settingView: SettingView, ) { fun settings(request: Request, loggedInUser: LoggedInUser): Response { @@ -61,15 +63,15 @@ class SettingsController( return if (isDownload) { val filename = "simplenotes-export-${loggedInUser.username}" if (request.form("format") == "zip") { - val zip = userService.exportAsZip(loggedInUser.userId) + val zip = exportService.exportAsZip(loggedInUser.userId) Response(Status.OK) .with(attachment("$filename.zip", "application/zip")) .body(zip) } else Response(Status.OK) .with(attachment("$filename.json", "application/json")) - .body(userService.exportAsJson(loggedInUser.userId)) - } else Response(Status.OK).body(userService.exportAsJson(loggedInUser.userId)).header( + .body(exportService.exportAsJson(loggedInUser.userId)) + } else Response(Status.OK).body(exportService.exportAsJson(loggedInUser.userId)).header( "Content-Type", "application/json" ) diff --git a/app/src/controllers/UserController.kt b/app/src/controllers/UserController.kt index 48526f8..a780e17 100644 --- a/app/src/controllers/UserController.kt +++ b/app/src/controllers/UserController.kt @@ -4,11 +4,7 @@ import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.isSecure import be.simplenotes.app.extensions.redirect import be.simplenotes.config.JwtConfig -import be.simplenotes.domain.usecases.UserService -import be.simplenotes.domain.usecases.users.login.* -import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm -import be.simplenotes.domain.usecases.users.register.RegisterForm -import be.simplenotes.domain.usecases.users.register.UserExists +import be.simplenotes.domain.* import be.simplenotes.types.LoggedInUser import be.simplenotes.views.UserView import org.http4k.core.Method.GET @@ -39,11 +35,11 @@ class UserController( return result.fold( { val html = when (it) { - UserExists -> userView.register( + RegisterError.UserExists -> userView.register( loggedInUser, error = "User already exists" ) - is InvalidRegisterForm -> + is RegisterError.InvalidRegisterForm -> userView.register( loggedInUser, validationErrors = it.validationErrors @@ -70,17 +66,17 @@ class UserController( return result.fold( { val html = when (it) { - Unregistered -> + LoginError.Unregistered -> userView.login( loggedInUser, error = "User does not exist" ) - WrongPassword -> + LoginError.WrongPassword -> userView.login( loggedInUser, error = "Wrong password" ) - is InvalidLoginForm -> + is LoginError.InvalidLoginForm -> userView.login( loggedInUser, validationErrors = it.validationErrors diff --git a/buildSrc/src/main/kotlin/be/simplenotes/java-convention.gradle.kts b/buildSrc/src/main/kotlin/be/simplenotes/java-convention.gradle.kts index d137415..4300b0e 100644 --- a/buildSrc/src/main/kotlin/be/simplenotes/java-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/be/simplenotes/java-convention.gradle.kts @@ -25,5 +25,7 @@ tasks.withType { options.encoding = "UTF-8" } -sourceSets["main"].resources.srcDirs("resources") -sourceSets["test"].resources.srcDirs("testresources") +sourceSets["main"].resources.setSrcDirs(listOf("resources")) +sourceSets["main"].java.setSrcDirs(emptyList()) +sourceSets["test"].resources.setSrcDirs(listOf("testresources")) +sourceSets["test"].java.setSrcDirs(emptyList()) diff --git a/buildSrc/src/main/kotlin/be/simplenotes/kotlin-convention.gradle.kts b/buildSrc/src/main/kotlin/be/simplenotes/kotlin-convention.gradle.kts index 4dd3e89..642c368 100644 --- a/buildSrc/src/main/kotlin/be/simplenotes/kotlin-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/be/simplenotes/kotlin-convention.gradle.kts @@ -25,5 +25,5 @@ tasks.withType { } } -kotlin.sourceSets["main"].kotlin.srcDirs("src") -kotlin.sourceSets["test"].kotlin.srcDirs("test") +kotlin.sourceSets["main"].kotlin.setSrcDirs(listOf("src")) +kotlin.sourceSets["test"].kotlin.setSrcDirs(listOf("test")) diff --git a/domain/src/usecases/export/ExportUseCaseImpl.kt b/domain/src/ExportService.kt similarity index 87% rename from domain/src/usecases/export/ExportUseCaseImpl.kt rename to domain/src/ExportService.kt index 2c3bc76..a3e1fca 100644 --- a/domain/src/usecases/export/ExportUseCaseImpl.kt +++ b/domain/src/ExportService.kt @@ -1,8 +1,7 @@ -package be.simplenotes.domain.usecases.export +package be.simplenotes.domain import be.simplenotes.persistence.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 @@ -12,12 +11,17 @@ import java.io.ByteArrayOutputStream import java.io.InputStream import javax.inject.Singleton -@Primary +interface ExportService { + fun exportAsJson(userId: Int): String + fun exportAsZip(userId: Int): InputStream +} + @Singleton -internal class ExportUseCaseImpl( +internal class ExportServiceImpl( private val noteRepository: NoteRepository, private val json: Json, -) : ExportUseCase { +) : ExportService { + override fun exportAsJson(userId: Int): String { val notes = noteRepository.export(userId) return json.encodeToString(ListSerializer(ExportedNote.serializer()), notes) @@ -38,7 +42,7 @@ internal class ExportUseCaseImpl( } } -class ZipOutput : AutoCloseable { +private class ZipOutput : AutoCloseable { val outputStream = ByteArrayOutputStream() private val zipOutputStream = ZipArchiveOutputStream(outputStream) diff --git a/domain/src/usecases/HealthCheckService.kt b/domain/src/HealthCheckService.kt similarity index 88% rename from domain/src/usecases/HealthCheckService.kt rename to domain/src/HealthCheckService.kt index fc144e7..8c52570 100644 --- a/domain/src/usecases/HealthCheckService.kt +++ b/domain/src/HealthCheckService.kt @@ -1,4 +1,4 @@ -package be.simplenotes.domain.usecases +package be.simplenotes.domain import be.simplenotes.persistence.DbHealthCheck import javax.inject.Singleton diff --git a/domain/src/Index.kt b/domain/src/Index.kt deleted file mode 100644 index c2a7303..0000000 --- a/domain/src/Index.kt +++ /dev/null @@ -1 +0,0 @@ -package be.simplenotes.domain diff --git a/domain/src/usecases/markdown/MarkdownConverterImpl.kt b/domain/src/MarkdownService.kt similarity index 63% rename from domain/src/usecases/markdown/MarkdownConverterImpl.kt rename to domain/src/MarkdownService.kt index 6fcd9e6..b1688d8 100644 --- a/domain/src/usecases/markdown/MarkdownConverterImpl.kt +++ b/domain/src/MarkdownService.kt @@ -1,4 +1,4 @@ -package be.simplenotes.domain.usecases.markdown +package be.simplenotes.domain import arrow.core.Either import arrow.core.computations.either @@ -8,38 +8,43 @@ import be.simplenotes.domain.validation.NoteValidations import be.simplenotes.types.NoteMetadata import com.vladsch.flexmark.html.HtmlRenderer import com.vladsch.flexmark.parser.Parser +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 +interface MarkdownService { + fun renderDocument(input: String): Either +} + private typealias MetaMdPair = Pair @Singleton -internal class MarkdownConverterImpl( +internal class MarkdownServiceImpl( private val parser: Parser, private val renderer: HtmlRenderer, -) : MarkdownConverter { +) : MarkdownService { private val yamlBoundPattern = "-{3}".toRegex() - private fun splitMetaFromDocument(input: String): Either { + private fun splitMetaFromDocument(input: String): Either { val split = input.split(yamlBoundPattern, 3) - if (split.size < 3) return MissingMeta.left() + if (split.size < 3) return MarkdownParsingError.MissingMeta.left() return (split[1].trim() to split[2].trim()).right() } private val yaml = Yaml() - private fun parseMeta(input: String): Either { + private fun parseMeta(input: String): Either { val load: Map = try { yaml.load(input) } catch (e: ParserException) { - return InvalidMeta.left() + return MarkdownParsingError.InvalidMeta.left() } catch (e: ScannerException) { - return InvalidMeta.left() + return MarkdownParsingError.InvalidMeta.left() } val title = when (val titleNode = load["title"]) { is String, is Number -> titleNode.toString() - else -> return InvalidMeta.left() + else -> return MarkdownParsingError.InvalidMeta.left() } val tagsNode = load["tags"] @@ -61,3 +66,11 @@ internal class MarkdownConverterImpl( Document(parsedMeta, html) } } + +sealed class MarkdownParsingError { + object MissingMeta : MarkdownParsingError() + object InvalidMeta : MarkdownParsingError() + class ValidationError(val validationErrors: ValidationErrors) : MarkdownParsingError() +} + +data class Document(val metadata: NoteMetadata, val html: String) diff --git a/domain/src/usecases/NoteService.kt b/domain/src/NoteService.kt similarity index 83% rename from domain/src/usecases/NoteService.kt rename to domain/src/NoteService.kt index 158f2fd..b46d50a 100644 --- a/domain/src/usecases/NoteService.kt +++ b/domain/src/NoteService.kt @@ -1,10 +1,8 @@ -package be.simplenotes.domain.usecases +package be.simplenotes.domain import arrow.core.computations.either import be.simplenotes.domain.security.HtmlSanitizer -import be.simplenotes.domain.usecases.markdown.MarkdownConverter -import be.simplenotes.domain.usecases.markdown.MarkdownParsingError -import be.simplenotes.domain.usecases.search.parseSearchTerms +import be.simplenotes.domain.utils.parseSearchTerms import be.simplenotes.persistence.repositories.NoteRepository import be.simplenotes.persistence.repositories.UserRepository import be.simplenotes.persistence.transactions.TransactionService @@ -20,7 +18,7 @@ import javax.inject.Singleton @Singleton class NoteService( - private val markdownConverter: MarkdownConverter, + private val markdownService: MarkdownService, private val noteRepository: NoteRepository, private val userRepository: UserRepository, private val searcher: NoteSearcher, @@ -30,29 +28,23 @@ class NoteService( fun create(user: LoggedInUser, markdownText: String) = transaction.use { either.eager { - val persistedNote = !markdownConverter.renderDocument(markdownText) + markdownService.renderDocument(markdownText) .map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) } .map { Note(it.metadata, markdown = markdownText, html = it.html) } .map { noteRepository.create(user.userId, it) } - - searcher.indexNote(user.userId, persistedNote) - persistedNote + .bind() + .also { searcher.indexNote(user.userId, it) } } } - fun update( - user: LoggedInUser, - uuid: UUID, - markdownText: String, - ) = transaction.use { + fun update(user: LoggedInUser, uuid: UUID, markdownText: String) = transaction.use { either.eager { - val persistedNote = !markdownConverter.renderDocument(markdownText) + markdownService.renderDocument(markdownText) .map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) } .map { Note(it.metadata, markdown = markdownText, html = it.html) } .map { noteRepository.update(user.userId, uuid, it) } - - persistedNote?.let { searcher.updateIndex(user.userId, it) } - persistedNote + .bind() + ?.also { searcher.updateIndex(user.userId, it) } } } diff --git a/domain/src/UserService.kt b/domain/src/UserService.kt new file mode 100644 index 0000000..fc11d9a --- /dev/null +++ b/domain/src/UserService.kt @@ -0,0 +1,92 @@ +package be.simplenotes.domain + +import arrow.core.Either +import arrow.core.computations.either +import arrow.core.filterOrElse +import arrow.core.leftIfNull +import arrow.core.rightIfNotNull +import be.simplenotes.domain.security.PasswordHash +import be.simplenotes.domain.security.SimpleJwt +import be.simplenotes.domain.validation.UserValidations +import be.simplenotes.persistence.repositories.UserRepository +import be.simplenotes.persistence.transactions.TransactionService +import be.simplenotes.search.NoteSearcher +import be.simplenotes.types.LoggedInUser +import be.simplenotes.types.PersistedUser +import io.konform.validation.ValidationErrors +import kotlinx.serialization.Serializable +import javax.inject.Singleton + +interface UserService { + fun register(form: RegisterForm): Either + fun login(form: LoginForm): Either + fun delete(form: DeleteForm): Either +} + +@Singleton +internal class UserServiceImpl( + private val userRepository: UserRepository, + private val passwordHash: PasswordHash, + private val jwt: SimpleJwt, + private val searcher: NoteSearcher, + private val transactionService: TransactionService, +) : UserService { + + override fun register(form: RegisterForm) = transactionService.use { + UserValidations.validateRegister(form) + .filterOrElse({ !userRepository.exists(it.username) }, { RegisterError.UserExists }) + .map { it.copy(password = passwordHash.crypt(it.password)) } + .map { userRepository.create(it) } + .leftIfNull { RegisterError.UserExists } + } + + override fun login(form: LoginForm) = either.eager { + UserValidations.validateLogin(form) + .bind() + .let { userRepository.find(it.username) } + .rightIfNotNull { LoginError.Unregistered } + .filterOrElse({ passwordHash.verify(form.password!!, it.password) }, { LoginError.WrongPassword }) + .map { jwt.sign(LoggedInUser(it)) } + .bind() + } + + override fun delete(form: DeleteForm) = transactionService.use { + either.eager { + val user = !UserValidations.validateDelete(form) + val persistedUser = !userRepository.find(user.username).rightIfNotNull { DeleteError.Unregistered } + !Either.conditionally( + passwordHash.verify(user.password, persistedUser.password), + { DeleteError.WrongPassword }, + { } + ) + !Either.conditionally(userRepository.delete(persistedUser.id), { DeleteError.Unregistered }, { }) + searcher.dropIndex(persistedUser.id) + } + } +} + +sealed class DeleteError { + object Unregistered : DeleteError() + object WrongPassword : DeleteError() + class InvalidForm(val validationErrors: ValidationErrors) : DeleteError() +} + +data class DeleteForm(val username: String?, val password: String?, val checked: Boolean) + +sealed class LoginError { + object Unregistered : LoginError() + object WrongPassword : LoginError() + class InvalidLoginForm(val validationErrors: ValidationErrors) : LoginError() +} + +typealias Token = String + +sealed class RegisterError { + object UserExists : RegisterError() + class InvalidRegisterForm(val validationErrors: ValidationErrors) : RegisterError() +} + +typealias RegisterForm = LoginForm + +@Serializable +data class LoginForm(val username: String?, val password: String?) diff --git a/domain/src/usecases/markdown/FlexmarkFactory.kt b/domain/src/modules/FlexmarkFactory.kt similarity index 95% rename from domain/src/usecases/markdown/FlexmarkFactory.kt rename to domain/src/modules/FlexmarkFactory.kt index b56063f..560d401 100644 --- a/domain/src/usecases/markdown/FlexmarkFactory.kt +++ b/domain/src/modules/FlexmarkFactory.kt @@ -1,4 +1,4 @@ -package be.simplenotes.domain.usecases.markdown +package be.simplenotes.domain.modules import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension import com.vladsch.flexmark.html.HtmlRenderer diff --git a/domain/src/usecases/UserService.kt b/domain/src/usecases/UserService.kt deleted file mode 100644 index 80fc475..0000000 --- a/domain/src/usecases/UserService.kt +++ /dev/null @@ -1,18 +0,0 @@ -package be.simplenotes.domain.usecases - -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, - deleteUseCase: DeleteUseCase, - exportUseCase: ExportUseCase, -) : LoginUseCase by loginUseCase, - RegisterUseCase by registerUseCase, - DeleteUseCase by deleteUseCase, - ExportUseCase by exportUseCase diff --git a/domain/src/usecases/export/ExportUseCase.kt b/domain/src/usecases/export/ExportUseCase.kt deleted file mode 100644 index 2655141..0000000 --- a/domain/src/usecases/export/ExportUseCase.kt +++ /dev/null @@ -1,8 +0,0 @@ -package be.simplenotes.domain.usecases.export - -import java.io.InputStream - -interface ExportUseCase { - fun exportAsJson(userId: Int): String - fun exportAsZip(userId: Int): InputStream -} diff --git a/domain/src/usecases/markdown/MarkdownConverter.kt b/domain/src/usecases/markdown/MarkdownConverter.kt deleted file mode 100644 index c6e45fa..0000000 --- a/domain/src/usecases/markdown/MarkdownConverter.kt +++ /dev/null @@ -1,16 +0,0 @@ -package be.simplenotes.domain.usecases.markdown - -import arrow.core.Either -import be.simplenotes.types.NoteMetadata -import io.konform.validation.ValidationErrors - -sealed class MarkdownParsingError -object MissingMeta : MarkdownParsingError() -object InvalidMeta : MarkdownParsingError() -class ValidationError(val validationErrors: ValidationErrors) : MarkdownParsingError() - -data class Document(val metadata: NoteMetadata, val html: String) - -interface MarkdownConverter { - fun renderDocument(input: String): Either -} diff --git a/domain/src/usecases/search/SearchTermsParser.kt b/domain/src/usecases/search/SearchTermsParser.kt deleted file mode 100644 index cbc9a64..0000000 --- a/domain/src/usecases/search/SearchTermsParser.kt +++ /dev/null @@ -1,96 +0,0 @@ -package be.simplenotes.domain.usecases.search - -import be.simplenotes.search.SearchTerms -import java.util.* - -private enum class Quote { SingleQuote, DoubleQuote, } - -data class ParsedSearchInput(val global: List, val entries: Map) - -object SearchInputParser { - fun parseInput(input: String): ParsedSearchInput { - val tokenizer = StringTokenizer(input, ":\"' ", true) - - val tokens = ArrayList() - val current = StringBuilder() - var quoteOpen: Quote? = null - - fun push() { - if (current.isNotEmpty()) { - tokens.add(current.toString()) - } - current.setLength(0) - quoteOpen = null - } - - while (tokenizer.hasMoreTokens()) { - when (val token = tokenizer.nextToken()) { - "\"" -> when { - Quote.DoubleQuote == quoteOpen -> push() - quoteOpen == null -> quoteOpen = Quote.DoubleQuote - else -> current.append(token) - } - "'" -> when { - Quote.SingleQuote == quoteOpen -> push() - quoteOpen == null -> quoteOpen = Quote.SingleQuote - else -> current.append(token) - } - " " -> { - if (quoteOpen != null) current.append(" ") - else push() - } - ":" -> { - push() - tokens.add(token) - } - else -> { - current.append(token) - } - } - } - - push() - - val entries = HashMap() - - val colonIndexes = ArrayList() - tokens.forEachIndexed { index, token -> - if (token == ":") colonIndexes += index - } - - var changes = 0 - for (colonIndex in colonIndexes) { - val offset = changes * 3 - - val key = tokens.getOrNull(colonIndex - 1 - offset) - val value = tokens.getOrNull(colonIndex + 1 - offset) - - if (key != null && value != null) { - entries[key] = value - tokens.removeAt(colonIndex - 1 - offset) // remove key - tokens.removeAt(colonIndex - 1 - offset) // remove : - tokens.removeAt(colonIndex - 1 - offset) // remove value - changes++ - } - } - - return ParsedSearchInput(global = tokens, entries = entries) - } -} - -internal fun parseSearchTerms(input: String): SearchTerms { - val parsedInput = SearchInputParser.parseInput(input) - - val title: String? = parsedInput.entries["title"] - val tag: String? = parsedInput.entries["tag"] - val content: String? = parsedInput.entries["content"] - - val all = parsedInput.global.takeIf { it.isNotEmpty() }?.joinToString(" ") - - return SearchTerms( - title = title, - tag = tag, - content = content, - all = all - ) -} diff --git a/domain/src/usecases/users/delete/DeleteUseCaseImpl.kt b/domain/src/usecases/users/delete/DeleteUseCaseImpl.kt deleted file mode 100644 index 21ff619..0000000 --- a/domain/src/usecases/users/delete/DeleteUseCaseImpl.kt +++ /dev/null @@ -1,35 +0,0 @@ -package be.simplenotes.domain.usecases.users.delete - -import arrow.core.Either -import arrow.core.computations.either -import arrow.core.rightIfNotNull -import be.simplenotes.domain.security.PasswordHash -import be.simplenotes.domain.validation.UserValidations -import be.simplenotes.persistence.repositories.UserRepository -import be.simplenotes.persistence.transactions.TransactionService -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, - private val searcher: NoteSearcher, - private val transactionService: TransactionService, -) : DeleteUseCase { - override fun delete(form: DeleteForm) = transactionService.use { - either.eager { - val user = !UserValidations.validateDelete(form) - val persistedUser = !userRepository.find(user.username).rightIfNotNull { DeleteError.Unregistered } - !Either.conditionally( - passwordHash.verify(user.password, persistedUser.password), - { DeleteError.WrongPassword }, - { Unit } - ) - !Either.conditionally(userRepository.delete(persistedUser.id), { DeleteError.Unregistered }, { Unit }) - searcher.dropIndex(persistedUser.id) - } - } -} diff --git a/domain/src/usecases/users/delete/DeleteUsecase.kt b/domain/src/usecases/users/delete/DeleteUsecase.kt deleted file mode 100644 index e34d7ae..0000000 --- a/domain/src/usecases/users/delete/DeleteUsecase.kt +++ /dev/null @@ -1,16 +0,0 @@ -package be.simplenotes.domain.usecases.users.delete - -import arrow.core.Either -import io.konform.validation.ValidationErrors - -sealed class DeleteError { - object Unregistered : DeleteError() - object WrongPassword : DeleteError() - class InvalidForm(val validationErrors: ValidationErrors) : DeleteError() -} - -data class DeleteForm(val username: String?, val password: String?, val checked: Boolean) - -interface DeleteUseCase { - fun delete(form: DeleteForm): Either -} diff --git a/domain/src/usecases/users/login/LoginUseCaseImpl.kt b/domain/src/usecases/users/login/LoginUseCaseImpl.kt deleted file mode 100644 index e30ece0..0000000 --- a/domain/src/usecases/users/login/LoginUseCaseImpl.kt +++ /dev/null @@ -1,28 +0,0 @@ -package be.simplenotes.domain.usecases.users.login - -import arrow.core.computations.either -import arrow.core.filterOrElse -import arrow.core.rightIfNotNull -import be.simplenotes.domain.security.PasswordHash -import be.simplenotes.domain.security.SimpleJwt -import be.simplenotes.domain.validation.UserValidations -import be.simplenotes.persistence.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, - private val jwt: SimpleJwt -) : LoginUseCase { - override fun login(form: LoginForm) = either.eager { - val user = !UserValidations.validateLogin(form) - !userRepository.find(user.username) - .rightIfNotNull { Unregistered } - .filterOrElse({ passwordHash.verify(form.password!!, it.password) }, { WrongPassword }) - .map { jwt.sign(LoggedInUser(it)) } - } -} diff --git a/domain/src/usecases/users/login/LoginUsecase.kt b/domain/src/usecases/users/login/LoginUsecase.kt deleted file mode 100644 index 3bdb243..0000000 --- a/domain/src/usecases/users/login/LoginUsecase.kt +++ /dev/null @@ -1,19 +0,0 @@ -package be.simplenotes.domain.usecases.users.login - -import arrow.core.Either -import io.konform.validation.ValidationErrors -import kotlinx.serialization.Serializable - -sealed class LoginError -object Unregistered : LoginError() -object WrongPassword : LoginError() -class InvalidLoginForm(val validationErrors: ValidationErrors) : LoginError() - -typealias Token = String - -@Serializable -data class LoginForm(val username: String?, val password: String?) - -interface LoginUseCase { - fun login(form: LoginForm): Either -} diff --git a/domain/src/usecases/users/register/RegisterUseCaseImpl.kt b/domain/src/usecases/users/register/RegisterUseCaseImpl.kt deleted file mode 100644 index 6b3c87b..0000000 --- a/domain/src/usecases/users/register/RegisterUseCaseImpl.kt +++ /dev/null @@ -1,28 +0,0 @@ -package be.simplenotes.domain.usecases.users.register - -import arrow.core.Either -import arrow.core.filterOrElse -import arrow.core.leftIfNull -import be.simplenotes.domain.security.PasswordHash -import be.simplenotes.domain.validation.UserValidations -import be.simplenotes.persistence.repositories.UserRepository -import be.simplenotes.persistence.transactions.TransactionService -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, - private val transactionService: TransactionService, -) : RegisterUseCase { - override fun register(form: RegisterForm): Either = transactionService.use { - UserValidations.validateRegister(form) - .filterOrElse({ !userRepository.exists(it.username) }, { UserExists }) - .map { it.copy(password = passwordHash.crypt(it.password)) } - .map { userRepository.create(it) } - .leftIfNull { UserExists } - } -} diff --git a/domain/src/usecases/users/register/RegisterUsecase.kt b/domain/src/usecases/users/register/RegisterUsecase.kt deleted file mode 100644 index 8f47d38..0000000 --- a/domain/src/usecases/users/register/RegisterUsecase.kt +++ /dev/null @@ -1,16 +0,0 @@ -package be.simplenotes.domain.usecases.users.register - -import arrow.core.Either -import be.simplenotes.domain.usecases.users.login.LoginForm -import be.simplenotes.types.PersistedUser -import io.konform.validation.ValidationErrors - -sealed class RegisterError -object UserExists : RegisterError() -class InvalidRegisterForm(val validationErrors: ValidationErrors) : RegisterError() - -typealias RegisterForm = LoginForm - -interface RegisterUseCase { - fun register(form: RegisterForm): Either -} diff --git a/domain/src/utils/SearchTermsParser.kt b/domain/src/utils/SearchTermsParser.kt new file mode 100644 index 0000000..b4d4f8d --- /dev/null +++ b/domain/src/utils/SearchTermsParser.kt @@ -0,0 +1,94 @@ +package be.simplenotes.domain.utils + +import be.simplenotes.search.SearchTerms +import java.util.* + +private enum class Quote { SingleQuote, DoubleQuote, } + +private data class ParsedSearchInput(val global: List, val entries: Map) + +private fun parseInput(input: String): ParsedSearchInput { + val tokenizer = StringTokenizer(input, ":\"' ", true) + + val tokens = ArrayList() + val current = StringBuilder() + var quoteOpen: Quote? = null + + fun push() { + if (current.isNotEmpty()) { + tokens.add(current.toString()) + } + current.setLength(0) + quoteOpen = null + } + + while (tokenizer.hasMoreTokens()) { + when (val token = tokenizer.nextToken()) { + "\"" -> when { + Quote.DoubleQuote == quoteOpen -> push() + quoteOpen == null -> quoteOpen = Quote.DoubleQuote + else -> current.append(token) + } + "'" -> when { + Quote.SingleQuote == quoteOpen -> push() + quoteOpen == null -> quoteOpen = Quote.SingleQuote + else -> current.append(token) + } + " " -> { + if (quoteOpen != null) current.append(" ") + else push() + } + ":" -> { + push() + tokens.add(token) + } + else -> { + current.append(token) + } + } + } + + push() + + val entries = HashMap() + + val colonIndexes = ArrayList() + tokens.forEachIndexed { index, token -> + if (token == ":") colonIndexes += index + } + + var changes = 0 + for (colonIndex in colonIndexes) { + val offset = changes * 3 + + val key = tokens.getOrNull(colonIndex - 1 - offset) + val value = tokens.getOrNull(colonIndex + 1 - offset) + + if (key != null && value != null) { + entries[key] = value + tokens.removeAt(colonIndex - 1 - offset) // remove key + tokens.removeAt(colonIndex - 1 - offset) // remove : + tokens.removeAt(colonIndex - 1 - offset) // remove value + changes++ + } + } + + return ParsedSearchInput(global = tokens, entries = entries) +} + +internal fun parseSearchTerms(input: String): SearchTerms { + val parsedInput = parseInput(input) + + val title: String? = parsedInput.entries["title"] + val tag: String? = parsedInput.entries["tag"] + val content: String? = parsedInput.entries["content"] + + val all = parsedInput.global.takeIf { it.isNotEmpty() }?.joinToString(" ") + + return SearchTerms( + title = title, + tag = tag, + content = content, + all = all + ) +} diff --git a/domain/src/validation/NoteValidations.kt b/domain/src/validation/NoteValidations.kt index 1f5c043..56fd898 100644 --- a/domain/src/validation/NoteValidations.kt +++ b/domain/src/validation/NoteValidations.kt @@ -1,6 +1,6 @@ package be.simplenotes.domain.validation -import be.simplenotes.domain.usecases.markdown.ValidationError +import be.simplenotes.domain.MarkdownParsingError import be.simplenotes.types.NoteMetadata import io.konform.validation.Validation import io.konform.validation.jsonschema.maxItems @@ -27,9 +27,9 @@ internal object NoteValidations { } } - fun validateMetadata(meta: NoteMetadata): ValidationError? { + fun validateMetadata(meta: NoteMetadata): MarkdownParsingError.ValidationError? { val errors = metaValidator.validate(meta).errors return if (errors.isEmpty()) null - else return ValidationError(errors) + else return MarkdownParsingError.ValidationError(errors) } } diff --git a/domain/src/validation/UserValidations.kt b/domain/src/validation/UserValidations.kt index e7d5191..9b113d3 100644 --- a/domain/src/validation/UserValidations.kt +++ b/domain/src/validation/UserValidations.kt @@ -3,12 +3,7 @@ package be.simplenotes.domain.validation import arrow.core.Either import arrow.core.left import arrow.core.right -import be.simplenotes.domain.usecases.users.delete.DeleteError -import be.simplenotes.domain.usecases.users.delete.DeleteForm -import be.simplenotes.domain.usecases.users.login.InvalidLoginForm -import be.simplenotes.domain.usecases.users.login.LoginForm -import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm -import be.simplenotes.domain.usecases.users.register.RegisterForm +import be.simplenotes.domain.* import be.simplenotes.types.User import io.konform.validation.Validation import io.konform.validation.jsonschema.maxLength @@ -26,16 +21,16 @@ internal object UserValidations { } } - fun validateLogin(form: LoginForm): Either { + fun validateLogin(form: LoginForm): Either { val errors = loginValidator.validate(form).errors return if (errors.isEmpty()) User(form.username!!, form.password!!).right() - else return InvalidLoginForm(errors).left() + else return LoginError.InvalidLoginForm(errors).left() } - fun validateRegister(form: RegisterForm): Either { + fun validateRegister(form: RegisterForm): Either { val errors = loginValidator.validate(form).errors return if (errors.isEmpty()) User(form.username!!, form.password!!).right() - else return InvalidRegisterForm(errors).left() + else return RegisterError.InvalidRegisterForm(errors).left() } private val deleteValidator = Validation { diff --git a/domain/test/Index.kt b/domain/test/Index.kt deleted file mode 100644 index c2a7303..0000000 --- a/domain/test/Index.kt +++ /dev/null @@ -1 +0,0 @@ -package be.simplenotes.domain diff --git a/domain/test/UserServiceTest.kt b/domain/test/UserServiceTest.kt new file mode 100644 index 0000000..1f00845 --- /dev/null +++ b/domain/test/UserServiceTest.kt @@ -0,0 +1,101 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + +package be.simplenotes.domain + +import be.simplenotes.config.JwtConfig +import be.simplenotes.domain.security.BcryptPasswordHash +import be.simplenotes.domain.security.SimpleJwt +import be.simplenotes.domain.security.UserJwtMapper +import be.simplenotes.domain.testutils.isLeftOfType +import be.simplenotes.domain.testutils.isRight +import be.simplenotes.persistence.repositories.UserRepository +import be.simplenotes.persistence.transactions.TransactionService +import be.simplenotes.types.PersistedUser +import com.natpryce.hamkrest.assertion.assertThat +import com.natpryce.hamkrest.equalTo +import io.mockk.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.concurrent.TimeUnit + +internal class UserServiceTest { + val userRepository = mockk() + val passwordHash = BcryptPasswordHash(test = true) + val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS) + val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper()) + val noopTransactionService = object : TransactionService { + override fun use(block: () -> T) = block() + } + + val userService = UserServiceImpl( + userRepository = userRepository, + passwordHash = passwordHash, + jwt = simpleJwt, + searcher = mockk(), + transactionService = noopTransactionService + ) + + @BeforeEach + fun resetMocks() { + clearMocks(userRepository) + } + + @Test + fun `register should fail with invalid form`() { + val form = RegisterForm("", "a".repeat(10)) + assertThat(userService.register(form), isLeftOfType()) + verify { userRepository wasNot called } + } + + @Test + fun `Register should fail with existing username`() { + val form = RegisterForm("someuser", "somepassword") + every { userRepository.exists(form.username!!) } returns true + assertThat(userService.register(form), isLeftOfType()) + } + + @Test + fun `Register should succeed with new user`() { + val form = RegisterForm("someuser", "somepassword") + every { userRepository.exists(form.username!!) } returns false + every { userRepository.create(any()) } returns PersistedUser(form.username!!, form.password!!, 1) + val res = userService.register(form) + assertThat(res, isRight()) + res.map { assertThat(it.username, equalTo(form.username)) } + } + + @Test + fun `Login should fail with invalid form`() { + val form = LoginForm("", "a") + assertThat(userService.login(form), isLeftOfType()) + verify { userRepository wasNot called } + } + + @Test + fun `Login should fail with non existing user`() { + val form = LoginForm("someusername", "somepassword") + every { userRepository.find(form.username!!) } returns null + assertThat(userService.login(form), isLeftOfType()) + } + + @Test + fun `Login should fail with wrong password`() { + val form = LoginForm("someusername", "wrongpassword") + + every { userRepository.find(form.username!!) } returns + PersistedUser(form.username!!, passwordHash.crypt("right password"), 1) + + assertThat(userService.login(form), isLeftOfType()) + } + + @Test + fun `Login should succeed with existing user and correct password`() { + val loginForm = LoginForm("someusername", "somepassword") + + every { userRepository.find(loginForm.username!!) } returns + PersistedUser(loginForm.username!!, passwordHash.crypt(loginForm.password!!), 1) + + val res = userService.login(loginForm) + assertThat(res, isRight()) + } +} diff --git a/domain/test/security/LoggedInUserExtractorTest.kt b/domain/test/security/LoggedInUserExtractorTest.kt index 600255e..d85ed4d 100644 --- a/domain/test/security/LoggedInUserExtractorTest.kt +++ b/domain/test/security/LoggedInUserExtractorTest.kt @@ -1,7 +1,7 @@ package be.simplenotes.domain.security import be.simplenotes.config.JwtConfig -import be.simplenotes.domain.usecases.users.login.Token +import be.simplenotes.domain.Token import be.simplenotes.types.LoggedInUser import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm diff --git a/domain/test/usecases/users/login/LoginUseCaseImplTest.kt b/domain/test/usecases/users/login/LoginUseCaseImplTest.kt deleted file mode 100644 index bfe691b..0000000 --- a/domain/test/usecases/users/login/LoginUseCaseImplTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -package be.simplenotes.domain.usecases.users.login - -import be.simplenotes.config.JwtConfig -import be.simplenotes.domain.security.BcryptPasswordHash -import be.simplenotes.domain.security.SimpleJwt -import be.simplenotes.domain.security.UserJwtMapper -import be.simplenotes.domain.testutils.isLeftOfType -import be.simplenotes.domain.testutils.isRight -import be.simplenotes.persistence.repositories.UserRepository -import be.simplenotes.types.PersistedUser -import com.natpryce.hamkrest.assertion.assertThat -import io.mockk.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import java.util.concurrent.TimeUnit - -internal class LoginUseCaseImplTest { - // region setup - private val mockUserRepository = mockk() - private val passwordHash = BcryptPasswordHash(test = true) - private val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS) - private val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper()) - private val loginUseCase = LoginUseCaseImpl(mockUserRepository, passwordHash, simpleJwt) - - @BeforeEach - fun resetMocks() { - clearMocks(mockUserRepository) - } - // endregion - - @Test - fun `Login should fail with invalid form`() { - val form = LoginForm("", "a") - assertThat(loginUseCase.login(form), isLeftOfType()) - verify { mockUserRepository wasNot called } - } - - @Test - fun `Login should fail with non existing user`() { - val form = LoginForm("someusername", "somepassword") - every { mockUserRepository.find(form.username!!) } returns null - assertThat(loginUseCase.login(form), isLeftOfType()) - } - - @Test - fun `Login should fail with wrong password`() { - val form = LoginForm("someusername", "wrongpassword") - - every { mockUserRepository.find(form.username!!) } returns - PersistedUser(form.username!!, passwordHash.crypt("right password"), 1) - - assertThat(loginUseCase.login(form), isLeftOfType()) - } - - @Test - fun `Login should succeed with existing user and correct password`() { - val loginForm = LoginForm("someusername", "somepassword") - - every { mockUserRepository.find(loginForm.username!!) } returns - PersistedUser(loginForm.username!!, passwordHash.crypt(loginForm.password!!), 1) - - val res = loginUseCase.login(loginForm) - assertThat(res, isRight()) - } -} diff --git a/domain/test/usecases/users/register/RegisterUseCaseImplTest.kt b/domain/test/usecases/users/register/RegisterUseCaseImplTest.kt deleted file mode 100644 index 1f35006..0000000 --- a/domain/test/usecases/users/register/RegisterUseCaseImplTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package be.simplenotes.domain.usecases.users.register - -import be.simplenotes.domain.security.BcryptPasswordHash -import be.simplenotes.domain.testutils.isLeftOfType -import be.simplenotes.domain.testutils.isRight -import be.simplenotes.persistence.repositories.UserRepository -import be.simplenotes.persistence.transactions.TransactionService -import be.simplenotes.types.PersistedUser -import com.natpryce.hamkrest.assertion.assertThat -import com.natpryce.hamkrest.equalTo -import io.mockk.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -internal class RegisterUseCaseImplTest { - - // region setup - private val mockUserRepository = mockk() - private val passwordHash = BcryptPasswordHash(test = true) - private val noopTransactionService = object : TransactionService { - override fun use(block: () -> T) = block() - } - private val registerUseCase = RegisterUseCaseImpl(mockUserRepository, passwordHash, noopTransactionService) - - @BeforeEach - fun resetMocks() { - clearMocks(mockUserRepository) - } - // endregion - - @Test - fun `register should fail with invalid form`() { - val form = RegisterForm("", "a".repeat(10)) - assertThat(registerUseCase.register(form), isLeftOfType()) - verify { mockUserRepository wasNot called } - } - - @Test - fun `Register should fail with existing username`() { - val form = RegisterForm("someuser", "somepassword") - every { mockUserRepository.exists(form.username!!) } returns true - assertThat(registerUseCase.register(form), isLeftOfType()) - } - - @Test - fun `Register should succeed with new user`() { - val form = RegisterForm("someuser", "somepassword") - every { mockUserRepository.exists(form.username!!) } returns false - every { mockUserRepository.create(any()) } returns PersistedUser(form.username!!, form.password!!, 1) - val res = registerUseCase.register(form) - assertThat(res, isRight()) - res.map { assertThat(it.username, equalTo(form.username)) } - } -} diff --git a/domain/test/usecases/search/SearchTermsParserKtTest.kt b/domain/test/utils/SearchTermsParserKtTest.kt similarity index 98% rename from domain/test/usecases/search/SearchTermsParserKtTest.kt rename to domain/test/utils/SearchTermsParserKtTest.kt index 85da907..273c17a 100644 --- a/domain/test/usecases/search/SearchTermsParserKtTest.kt +++ b/domain/test/utils/SearchTermsParserKtTest.kt @@ -1,4 +1,4 @@ -package be.simplenotes.domain.usecases.search +package be.simplenotes.domain.utils import be.simplenotes.search.SearchTerms import com.natpryce.hamkrest.assertion.assertThat diff --git a/domain/test/validation/UserValidationsTest.kt b/domain/test/validation/UserValidationsTest.kt index 0659157..8924ab0 100644 --- a/domain/test/validation/UserValidationsTest.kt +++ b/domain/test/validation/UserValidationsTest.kt @@ -1,10 +1,10 @@ package be.simplenotes.domain.validation +import be.simplenotes.domain.LoginError +import be.simplenotes.domain.LoginForm +import be.simplenotes.domain.RegisterForm import be.simplenotes.domain.testutils.isLeftOfType import be.simplenotes.domain.testutils.isRight -import be.simplenotes.domain.usecases.users.login.InvalidLoginForm -import be.simplenotes.domain.usecases.users.login.LoginForm -import be.simplenotes.domain.usecases.users.register.RegisterForm import com.natpryce.hamkrest.assertion.assertThat import org.junit.jupiter.api.Nested import org.junit.jupiter.params.ParameterizedTest @@ -28,7 +28,7 @@ internal class UserValidationsTest { @ParameterizedTest @MethodSource("invalidLoginForms") fun `validate invalid logins`(form: LoginForm) { - assertThat(UserValidations.validateLogin(form), isLeftOfType()) + assertThat(UserValidations.validateLogin(form), isLeftOfType()) } @Suppress("Unused") @@ -59,7 +59,7 @@ internal class UserValidationsTest { @ParameterizedTest @MethodSource("invalidRegisterForms") fun `validate invalid register`(form: LoginForm) { - assertThat(UserValidations.validateLogin(form), isLeftOfType()) + assertThat(UserValidations.validateLogin(form), isLeftOfType()) } @Suppress("Unused") diff --git a/views/src/BaseView.kt b/views/src/BaseView.kt index 30d2c6b..a1986a2 100644 --- a/views/src/BaseView.kt +++ b/views/src/BaseView.kt @@ -37,7 +37,7 @@ class BaseView(@Named("styles") styles: String) : View(styles) { attributes["aria-label"] = "demo-search" attributes["name"] = "search" attributes["disabled"] = "" - attributes["value"] = "tag:\"demo\"" + attributes["value"] = "tag:demo" } span { id = "buttons"