Remove Boilerplate Use-case thingy
This commit is contained in:
parent
3e1683dfe5
commit
a4bf998c5b
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -25,5 +25,7 @@ tasks.withType<JavaCompile> {
|
||||
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<String>())
|
||||
sourceSets["test"].resources.setSrcDirs(listOf("testresources"))
|
||||
sourceSets["test"].java.setSrcDirs(emptyList<String>())
|
||||
|
||||
@ -25,5 +25,5 @@ tasks.withType<KotlinCompile> {
|
||||
}
|
||||
}
|
||||
|
||||
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"))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package be.simplenotes.domain.usecases
|
||||
package be.simplenotes.domain
|
||||
|
||||
import be.simplenotes.persistence.DbHealthCheck
|
||||
import javax.inject.Singleton
|
||||
@ -1 +0,0 @@
|
||||
package be.simplenotes.domain
|
||||
@ -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<MarkdownParsingError, Document>
|
||||
}
|
||||
|
||||
private typealias MetaMdPair = Pair<String, String>
|
||||
|
||||
@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<MissingMeta, MetaMdPair> {
|
||||
private fun splitMetaFromDocument(input: String): Either<MarkdownParsingError.MissingMeta, MetaMdPair> {
|
||||
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<InvalidMeta, NoteMetadata> {
|
||||
private fun parseMeta(input: String): Either<MarkdownParsingError.InvalidMeta, NoteMetadata> {
|
||||
val load: Map<String, Any> = 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)
|
||||
@ -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<MarkdownParsingError, PersistedNote> {
|
||||
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<MarkdownParsingError, PersistedNote?> {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
92
domain/src/UserService.kt
Normal file
92
domain/src/UserService.kt
Normal file
@ -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<RegisterError, PersistedUser>
|
||||
fun login(form: LoginForm): Either<LoginError, Token>
|
||||
fun delete(form: DeleteForm): Either<DeleteError, Unit>
|
||||
}
|
||||
|
||||
@Singleton
|
||||
internal class UserServiceImpl(
|
||||
private val userRepository: UserRepository,
|
||||
private val passwordHash: PasswordHash,
|
||||
private val jwt: SimpleJwt<LoggedInUser>,
|
||||
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<LoginError, Token> {
|
||||
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<DeleteError, Unit> {
|
||||
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?)
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
}
|
||||
@ -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<MarkdownParsingError, Document>
|
||||
}
|
||||
@ -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<String>, val entries: Map<String, String>)
|
||||
|
||||
object SearchInputParser {
|
||||
fun parseInput(input: String): ParsedSearchInput {
|
||||
val tokenizer = StringTokenizer(input, ":\"' ", true)
|
||||
|
||||
val tokens = ArrayList<String>()
|
||||
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<String, String>()
|
||||
|
||||
val colonIndexes = ArrayList<Int>()
|
||||
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
|
||||
)
|
||||
}
|
||||
@ -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<DeleteError, Unit> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<DeleteError, Unit>
|
||||
}
|
||||
@ -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<LoggedInUser>
|
||||
) : LoginUseCase {
|
||||
override fun login(form: LoginForm) = either.eager<LoginError, Token> {
|
||||
val user = !UserValidations.validateLogin(form)
|
||||
!userRepository.find(user.username)
|
||||
.rightIfNotNull { Unregistered }
|
||||
.filterOrElse({ passwordHash.verify(form.password!!, it.password) }, { WrongPassword })
|
||||
.map { jwt.sign(LoggedInUser(it)) }
|
||||
}
|
||||
}
|
||||
@ -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<LoginError, Token>
|
||||
}
|
||||
@ -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<RegisterError, PersistedUser> = 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 }
|
||||
}
|
||||
}
|
||||
@ -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<RegisterError, PersistedUser>
|
||||
}
|
||||
94
domain/src/utils/SearchTermsParser.kt
Normal file
94
domain/src/utils/SearchTermsParser.kt
Normal file
@ -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<String>, val entries: Map<String, String>)
|
||||
|
||||
private fun parseInput(input: String): ParsedSearchInput {
|
||||
val tokenizer = StringTokenizer(input, ":\"' ", true)
|
||||
|
||||
val tokens = ArrayList<String>()
|
||||
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<String, String>()
|
||||
|
||||
val colonIndexes = ArrayList<Int>()
|
||||
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
|
||||
)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<InvalidLoginForm, User> {
|
||||
fun validateLogin(form: LoginForm): Either<LoginError.InvalidLoginForm, User> {
|
||||
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<InvalidRegisterForm, User> {
|
||||
fun validateRegister(form: RegisterForm): Either<RegisterError.InvalidRegisterForm, User> {
|
||||
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<DeleteForm> {
|
||||
|
||||
@ -1 +0,0 @@
|
||||
package be.simplenotes.domain
|
||||
101
domain/test/UserServiceTest.kt
Normal file
101
domain/test/UserServiceTest.kt
Normal file
@ -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<UserRepository>()
|
||||
val passwordHash = BcryptPasswordHash(test = true)
|
||||
val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS)
|
||||
val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper())
|
||||
val noopTransactionService = object : TransactionService {
|
||||
override fun <T> 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<RegisterError.InvalidRegisterForm>())
|
||||
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<RegisterError.UserExists>())
|
||||
}
|
||||
|
||||
@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<LoginError.InvalidLoginForm>())
|
||||
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<LoginError.Unregistered>())
|
||||
}
|
||||
|
||||
@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<LoginError.WrongPassword>())
|
||||
}
|
||||
|
||||
@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())
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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<UserRepository>()
|
||||
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<InvalidLoginForm>())
|
||||
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<Unregistered>())
|
||||
}
|
||||
|
||||
@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<WrongPassword>())
|
||||
}
|
||||
|
||||
@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())
|
||||
}
|
||||
}
|
||||
@ -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<UserRepository>()
|
||||
private val passwordHash = BcryptPasswordHash(test = true)
|
||||
private val noopTransactionService = object : TransactionService {
|
||||
override fun <T> 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<InvalidRegisterForm>())
|
||||
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<UserExists>())
|
||||
}
|
||||
|
||||
@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)) }
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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<InvalidLoginForm>())
|
||||
assertThat(UserValidations.validateLogin(form), isLeftOfType<LoginError.InvalidLoginForm>())
|
||||
}
|
||||
|
||||
@Suppress("Unused")
|
||||
@ -59,7 +59,7 @@ internal class UserValidationsTest {
|
||||
@ParameterizedTest
|
||||
@MethodSource("invalidRegisterForms")
|
||||
fun `validate invalid register`(form: LoginForm) {
|
||||
assertThat(UserValidations.validateLogin(form), isLeftOfType<InvalidLoginForm>())
|
||||
assertThat(UserValidations.validateLogin(form), isLeftOfType<LoginError.InvalidLoginForm>())
|
||||
}
|
||||
|
||||
@Suppress("Unused")
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user