30 Commits

Author SHA1 Message Date
92debb56bb add database type archiving 2025-08-18 13:05:34 +02:00
hlaeja
eb65cfac51 [RELEASE] - Bump version 2025-08-18 12:52:44 +02:00
hlaeja
afedd19143 [RELEASE] - Release version: 0.4.0 2025-08-18 10:46:29 +00:00
f0ff324cf2 add sql script to fix user 2025-08-18 12:45:48 +02:00
cf1b78ae0a clean up for change to test library and logging 2025-08-18 12:45:48 +02:00
69e293a25f update updateAccount in AccountController for PublicEventService 2025-08-18 12:45:48 +02:00
dec6b99281 add PublicEventService 2025-08-18 12:45:48 +02:00
93aad65385 set up kafka 2025-08-18 12:45:48 +02:00
3effd930ad add AccountUtil.kt with detectChanges 2025-08-18 12:45:48 +02:00
97b8becd08 update test remove role_ for user in DB 2025-08-18 12:45:48 +02:00
hlaeja
5e0ba7ed2a [RELEASE] - Bump version 2025-07-29 18:05:42 +00:00
hlaeja
32a630d6a3 [RELEASE] - Release version: 0.3.0 2025-07-29 18:05:41 +00:00
c468a5ffa3 update gradlew 2025-07-29 20:04:46 +02:00
3849fa8676 update project 2025-07-29 20:04:46 +02:00
1ee306c151 add GitHub action
- update release in README.md
- add action run checks
- add action release
- remove release.sh
2025-07-29 20:04:46 +02:00
18e95f7213 add actuator.http 2025-07-29 20:04:46 +02:00
82c590dc30 update sql files 2025-07-29 20:04:46 +02:00
da491cecfa update release.sh to move sql files to version folder. 2025-03-05 14:43:15 +01:00
3bc5805a87 [RELEASE] - bump version 2025-02-07 17:13:48 +01:00
ec5991f20b [RELEASE] - release version: 0.2.0 2025-02-07 17:13:45 +01:00
a9b5abda1a update name for hlaeja libraries 2025-02-07 17:12:21 +01:00
c7eb3484e6 Authentication integration test
- add end-to-end test AuthenticationEndpoint
- add user-token.data
- add admin-token.data
- add .data to .editorconfig
2025-02-07 14:42:17 +01:00
5951af7d44 use data and reset sql files and some clean up 2025-02-07 14:33:42 +01:00
c08c3cb880 account validation and integration test
- add end-to-end test AccountEndpoint
- update AccountController validation to
  - updateAccount
  - addAccount
2025-02-03 22:41:24 +01:00
6165bcd512 cleanup AccountService 2025-02-03 22:06:53 +01:00
ddc701ea51 add valid account annotation
- add AccountValidator
- add ValidAccount
2025-02-03 22:05:51 +01:00
a762a05c11 extract accounts and add integration test
- add end-to-end test AccountsEndpoint
- extract get accounts from account.http to accounts.http
- extract get accounts from AccountController to AccountsController
- move spring boot test file to integration test
- add schema.sql
- add dependencies for integration test
2025-02-03 19:51:36 +01:00
6e6ea72d54 add update accounts
- add update accounts to account.http
- add updateAccount to AccountController
- add AccountEntity updateAccountEntity to Mapping.kt
- add updateAccount in AccountService
- update catalog version in gradle.properties
2025-01-28 16:45:15 +01:00
72ac37e603 add get accounts
- add get accounts to account.http
- add getAccounts to AccountController
- add missing test in AccountServiceTest
- add getAccounts to AccountService
2025-01-28 11:44:56 +01:00
f8154fe05f [RELEASE] - bump version 2025-01-02 07:16:07 +01:00
48 changed files with 1542 additions and 166 deletions

View File

@@ -9,7 +9,7 @@ insert_final_newline = true
max_line_length = 120 max_line_length = 120
tab_width = 4 tab_width = 4
[*.{md,sh,sql,yaml,yml}] [*.{md,sh,sql,xml,xsd,yaml,yml}]
max_line_length = 1024 max_line_length = 1024
indent_size = 2 indent_size = 2
tab_width = 2 tab_width = 2
@@ -17,6 +17,10 @@ tab_width = 2
[*.bat] [*.bat]
end_of_line = crlf end_of_line = crlf
[*.data]
max_line_length = 1024
insert_final_newline = false
[*.pem] [*.pem]
max_line_length = 64 max_line_length = 64
insert_final_newline = false insert_final_newline = false

13
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Release
on:
workflow_dispatch:
jobs:
release:
uses: swordsteel/hlaeja-common-workflows/.github/workflows/release.yml@master
secrets:
CI_BOT_PAT: ${{ secrets.CI_BOT_PAT }}
with:
TYPE: service
DATABASE_FILES: sql

12
.github/workflows/run-checks.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Pull request validation
on:
pull_request:
paths-ignore:
- '.github/**'
jobs:
validate:
uses: swordsteel/hlaeja-common-workflows/.github/workflows/run-checks.yml@master
secrets:
CI_BOT_PAT: ${{ secrets.CI_BOT_PAT }}

View File

@@ -16,7 +16,7 @@ In twilight's hush, where mythic tales unfold, A ledger of legends, the bravest
## Development Configuration ## Development Configuration
Run `release.sh` script from `master` branch. Run release pipeline from `master` branch.
## Development Information ## Development Information

View File

@@ -1,21 +1,24 @@
plugins { plugins {
alias(hlaeja.plugins.kotlin.jvm) alias(hlaeja.plugins.kotlin.jvm)
alias(hlaeja.plugins.kotlin.spring) alias(hlaeja.plugins.kotlin.spring)
alias(hlaeja.plugins.ltd.hlaeja.plugin.certificate) alias(hlaeja.plugins.spring.boot)
alias(hlaeja.plugins.ltd.hlaeja.plugin.service)
alias(hlaeja.plugins.spring.dependency.management) alias(hlaeja.plugins.spring.dependency.management)
alias(hlaeja.plugins.springframework.boot) alias(hlaeja.plugins.certificate)
alias(hlaeja.plugins.service)
} }
dependencies { dependencies {
implementation(hlaeja.fasterxml.jackson.module.kotlin)
implementation(hlaeja.kotlin.logging) implementation(hlaeja.kotlin.logging)
implementation(hlaeja.kotlin.reflect) implementation(hlaeja.kotlin.reflect)
implementation(hlaeja.kotlinx.coroutines) implementation(hlaeja.kotlinx.coroutines)
implementation(hlaeja.library.hlaeja.common.messages) implementation(hlaeja.library.common.messages)
implementation(hlaeja.library.hlaeja.jwt) implementation(hlaeja.library.jwt)
implementation(hlaeja.springboot.kafka)
implementation(hlaeja.springboot.starter.actuator) implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.springboot.starter.r2dbc) implementation(hlaeja.springboot.starter.r2dbc)
implementation(hlaeja.springboot.starter.security) implementation(hlaeja.springboot.starter.security)
implementation(hlaeja.springboot.starter.validation)
implementation(hlaeja.springboot.starter.webflux) implementation(hlaeja.springboot.starter.webflux)
runtimeOnly(hlaeja.postgresql) runtimeOnly(hlaeja.postgresql)
@@ -26,9 +29,19 @@ dependencies {
testImplementation(hlaeja.projectreactor.reactor.test) testImplementation(hlaeja.projectreactor.reactor.test)
testImplementation(hlaeja.kotlin.test.junit5) testImplementation(hlaeja.kotlin.test.junit5)
testImplementation(hlaeja.kotlinx.coroutines.test) testImplementation(hlaeja.kotlinx.coroutines.test)
testImplementation(hlaeja.springboot.kafka.test)
testImplementation(hlaeja.springboot.starter.test) testImplementation(hlaeja.springboot.starter.test)
testRuntimeOnly(hlaeja.junit.platform.launcher) testRuntimeOnly(hlaeja.junit.platform.launcher)
testIntegrationImplementation(hlaeja.assertj.core)
testIntegrationImplementation(hlaeja.library.test)
testIntegrationImplementation(hlaeja.projectreactor.reactor.test)
testIntegrationImplementation(hlaeja.kotlin.test.junit5)
testIntegrationImplementation(hlaeja.kotlinx.coroutines.test)
testIntegrationImplementation(hlaeja.springboot.starter.test)
testIntegrationRuntimeOnly(hlaeja.junit.platform.launcher)
} }
group = "ltd.hlaeja" group = "ltd.hlaeja"

View File

@@ -1,4 +1,4 @@
kotlin.code.style=official kotlin.code.style=official
version=0.1.0 version=0.5.0-SNAPSHOT
catalog=0.8.0 catalog=0.12.0
container.port.host=9050 container.port.host=9050

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

9
gradlew vendored
View File

@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -115,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped. # and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line. # treated as '${Hostname}' itself on the command line.
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.

4
gradlew.bat vendored
View File

@@ -70,11 +70,11 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

View File

@@ -1,7 +1,7 @@
### get user by id ### get user by id
GET {{hostname}}/account-00000000-0000-7000-0000-000000000001 GET {{hostname}}/account-00000000-0000-7000-0000-000000000001
### Get admin information ### add user
POST {{hostname}}/account POST {{hostname}}/account
Content-Type: application/json Content-Type: application/json
@@ -10,7 +10,32 @@ Content-Type: application/json
"password": "p4ssw0rd", "password": "p4ssw0rd",
"enabled": true, "enabled": true,
"roles": [ "roles": [
"ROLE_ADMIN", "ADMIN",
"ROLE_TEST" "TEST"
]
}
### update user all information
PUT {{hostname}}/account-00000000-0000-7000-0000-000000000002
Content-Type: application/json
{
"username": "user",
"password": "pass",
"enabled": true,
"roles": [
"TEST"
]
}
### update user information
PUT {{hostname}}/account-00000000-0000-7000-0000-000000000002
Content-Type: application/json
{
"username": "user",
"enabled": true,
"roles": [
"TEST"
] ]
} }

8
http/accounts.http Normal file
View File

@@ -0,0 +1,8 @@
### Get accounts
GET {{hostname}}/accounts
### Get accounts by page
GET {{hostname}}/accounts/page-1
### Get accounts by page and size
GET {{hostname}}/accounts/page-1/show-1

5
http/actuator.http Normal file
View File

@@ -0,0 +1,5 @@
### get actuator
GET {{hostname}}/actuator
### get actuator health
GET {{hostname}}/actuator/health

View File

@@ -1,89 +0,0 @@
#!/bin/sh
### This should be a pipeline, but for this example let use this ###
check_active_branch() {
if [ "$(git rev-parse --abbrev-ref HEAD)" != "$1" ]; then
echo "Error: The current branch is not $1."
exit 1
fi
}
check_uncommitted_changes() {
if [ -n "$(git status --porcelain)" ]; then
echo "Error: There are uncommitted changes in the repository."
exit 1
fi
}
prepare_environment() {
git fetch origin
}
check_last_commit() {
last_commit_message=$(git log -1 --pretty=format:%s)
if [ "$last_commit_message" = "[RELEASE] - bump version" ]; then
echo "Warning: Nothing to release!!!"
exit 1
fi
}
check_differences() {
if ! git diff --quiet origin/"$1" "$1"; then
echo "Error: The branches origin/$1 and $1 have differences."
exit 1
fi
}
un_snapshot_version() {
sed -i "s/\($1\s*=\s*[0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/" gradle.properties
}
current_version() {
awk -F '=' '/version\s*=\s*[0-9.]*/ {gsub(/^ +| +$/,"",$2); print $2}' gradle.properties
}
stage_files() {
for file in "$@"; do
if git diff --exit-code --quiet -- "$file"; then
echo "No changes in $file"
else
git add "$file"
echo "Changes in $file staged for commit"
fi
done
}
commit_change() {
stage_files gradle.properties
git commit -m "[RELEASE] - $1"
git push --porcelain origin master
}
add_release_tag() {
gitTag="v$(current_version)"
git tag -a "$gitTag" -m "Release version $gitTag"
git push --porcelain origin "$gitTag"
}
snapshot_version() {
new_version="$(current_version | awk -F '.' '{print $1 "." $2+1 ".0"}')"
sed -i "s/\(version\s*=\s*\)[0-9.]*/\1$new_version-SNAPSHOT/" gradle.properties
}
# check and prepare for release
check_active_branch master
check_uncommitted_changes
prepare_environment
check_last_commit
check_differences master
# un-snapshot version for release
un_snapshot_version version
un_snapshot_version catalog
# release changes and prepare for next release
commit_change "release version: $(current_version)"
add_release_tag
snapshot_version
commit_change 'bump version'

View File

@@ -56,10 +56,12 @@ CREATE DATABASE account_registry
WITH WITH
OWNER = role_administrator OWNER = role_administrator
ENCODING = 'UTF8' ENCODING = 'UTF8'
LC_COLLATE = 'en_US.utf8' LC_COLLATE = 'en_US.UTF-8'
LC_CTYPE = 'en_US.utf8' LC_CTYPE = 'en_US.UTF-8'
LOCALE_PROVIDER = 'libc' LOCALE_PROVIDER = 'libc'
TABLESPACE = pg_default TABLESPACE = pg_default
CONNECTION LIMIT = -1 CONNECTION LIMIT = -1
IS_TEMPLATE = False; IS_TEMPLATE = False;
COMMENT ON DATABASE account_registry
IS 'Primary database for user account registration and identity management';

View File

@@ -0,0 +1,5 @@
insert into public.accounts (id, created_at, updated_at, enabled, username, password, roles)
values ('00000000-0000-7000-0000-000000000001'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'admin', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ADMIN'),
('00000000-0000-7000-0000-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'user', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'USER'),
('00000000-0000-7000-0000-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'user', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'USER'),
('00000000-0000-7000-0000-000000000003'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', false, 'disabled', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'USER');

View File

@@ -0,0 +1,6 @@
UPDATE public.accounts
SET
roles = REPLACE(roles, 'ROLE_', ''),
updated_at = CURRENT_TIMESTAMP
WHERE
roles LIKE '%ROLE_%';

View File

@@ -1,22 +1,33 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import java.util.UUID import java.util.UUID
import ltd.hlaeja.entity.AccountEntity
import ltd.hlaeja.library.accountRegistry.Account import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.service.AccountService import ltd.hlaeja.service.AccountService
import ltd.hlaeja.service.PublicEventService
import ltd.hlaeja.util.detectChanges
import ltd.hlaeja.util.toAccountEntity import ltd.hlaeja.util.toAccountEntity
import ltd.hlaeja.util.toAccountResponse import ltd.hlaeja.util.toAccountResponse
import ltd.hlaeja.util.updateAccountEntity
import ltd.hlaeja.validator.ValidAccount
import org.springframework.http.HttpStatus.ACCEPTED
import org.springframework.http.HttpStatus.CREATED
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@RestController @RestController
class AccountController( class AccountController(
private val accountService: AccountService, private val accountService: AccountService,
private val passwordEncoder: PasswordEncoder, private val passwordEncoder: PasswordEncoder,
private val publicEventService: PublicEventService,
) { ) {
@GetMapping("/account-{uuid}") @GetMapping("/account-{uuid}")
@@ -25,9 +36,26 @@ class AccountController(
): Mono<Account.Response> = accountService.getUserById(uuid) ): Mono<Account.Response> = accountService.getUserById(uuid)
.map { it.toAccountResponse() } .map { it.toAccountResponse() }
@PutMapping("/account-{uuid}")
fun updateAccount(
@PathVariable uuid: UUID,
@RequestBody @ValidAccount request: Account.Request,
): Mono<Account.Response> = accountService.getUserById(uuid)
.map { user ->
user.updateAccountEntity(request, passwordEncoder)
.let { it to it.detectChanges(user) }
.also { if (it.second.isEmpty()) throw ResponseStatusException(ACCEPTED) }
}
.flatMap { (updated: AccountEntity, changes: List<String>) ->
accountService.updateAccount(updated)
.flatMap { publicEventService.updateAccount(it, changes) }
}
.map { it.toAccountResponse() }
@PostMapping("/account") @PostMapping("/account")
@ResponseStatus(CREATED)
fun addAccount( fun addAccount(
@RequestBody request: Account.Request, @RequestBody @ValidAccount request: Account.Request,
): Mono<Account.Response> = accountService.addAccount(request.toAccountEntity(passwordEncoder)) ): Mono<Account.Response> = accountService.addAccount(request.toAccountEntity(passwordEncoder))
.map { it.toAccountResponse() } .map { it.toAccountResponse() }
} }

View File

@@ -0,0 +1,42 @@
package ltd.hlaeja.controller
import jakarta.validation.constraints.Min
import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.service.AccountService
import ltd.hlaeja.util.toAccountResponse
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux
@RestController
@RequestMapping("/accounts")
class AccountsController(
private val accountService: AccountService,
) {
companion object {
const val DEFAULT_PAGE: Int = 1
const val DEFAULT_SIZE: Int = 25
}
@GetMapping
fun getDefaultAccounts(): Flux<Account.Response> = getAccounts(DEFAULT_PAGE, DEFAULT_SIZE)
@GetMapping("/page-{page}")
fun getAccountsPage(
@PathVariable @Min(1) page: Int,
): Flux<Account.Response> = getAccounts(page, DEFAULT_SIZE)
@GetMapping("/page-{page}/show-{size}")
fun getAccountsPageSize(
@PathVariable @Min(1) page: Int,
@PathVariable @Min(1) size: Int,
): Flux<Account.Response> = getAccounts(page, size)
private fun getAccounts(
page: Int,
size: Int,
): Flux<Account.Response> = accountService.getAccounts(page, size)
.map { it.toAccountResponse() }
}

View File

@@ -5,9 +5,12 @@ import java.util.UUID
import ltd.hlaeja.entity.AccountEntity import ltd.hlaeja.entity.AccountEntity
import ltd.hlaeja.repository.AccountRepository import ltd.hlaeja.repository.AccountRepository
import org.springframework.dao.DuplicateKeyException import org.springframework.dao.DuplicateKeyException
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus.BAD_REQUEST
import org.springframework.http.HttpStatus.CONFLICT
import org.springframework.http.HttpStatus.NOT_FOUND
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
@@ -21,23 +24,40 @@ class AccountService(
uuid: UUID, uuid: UUID,
): Mono<AccountEntity> = accountRepository.findById(uuid) ): Mono<AccountEntity> = accountRepository.findById(uuid)
.doOnNext { log.debug { "Get account ${it.id}" } } .doOnNext { log.debug { "Get account ${it.id}" } }
.switchIfEmpty(Mono.error(ResponseStatusException(HttpStatus.NOT_FOUND))) .switchIfEmpty(Mono.error(ResponseStatusException(NOT_FOUND)))
fun getUserByUsername( fun getUserByUsername(
username: String, username: String,
): Mono<AccountEntity> = accountRepository.findByUsername(username) ): Mono<AccountEntity> = accountRepository.findByUsername(username)
.doOnNext { log.debug { "Get account ${it.id} for username $username" } } .doOnNext { log.debug { "Get account ${it.id} for username $username" } }
.switchIfEmpty(Mono.error(ResponseStatusException(HttpStatus.NOT_FOUND))) .switchIfEmpty(Mono.error(ResponseStatusException(NOT_FOUND)))
fun addAccount( fun addAccount(
accountEntity: AccountEntity, accountEntity: AccountEntity,
): Mono<AccountEntity> = accountRepository.save(accountEntity) ): Mono<AccountEntity> = accountRepository.save(accountEntity)
.doOnNext { log.debug { "Added new type: $it.id" } } .doOnNext { log.debug { "Added new type: $it.id" } }
.onErrorResume { .onErrorResume(::onSaveError)
log.debug { it.localizedMessage }
when { fun getAccounts(page: Int, size: Int): Flux<AccountEntity> = try {
it is DuplicateKeyException -> Mono.error(ResponseStatusException(HttpStatus.CONFLICT)) accountRepository.findAll()
else -> Mono.error(ResponseStatusException(HttpStatus.BAD_REQUEST)) .skip((page - 1).toLong() * size)
} .take(size.toLong())
.doOnNext { log.debug { "Retrieved accounts $page with size $size" } }
} catch (e: IllegalArgumentException) {
Flux.error(ResponseStatusException(BAD_REQUEST, null, e))
}
fun updateAccount(
accountEntity: AccountEntity,
): Mono<AccountEntity> = accountRepository.save(accountEntity)
.doOnNext { log.debug { "updated users: $it.id" } }
.onErrorResume(::onSaveError)
private fun onSaveError(throwable: Throwable): Mono<out AccountEntity> {
log.debug { throwable.localizedMessage }
return when {
throwable is DuplicateKeyException -> Mono.error(ResponseStatusException(CONFLICT))
else -> Mono.error(ResponseStatusException(BAD_REQUEST))
} }
}
} }

View File

@@ -0,0 +1,25 @@
package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging
import ltd.hlaeja.entity.AccountEntity
import ltd.hlaeja.library.accountRegistry.event.AccountMessage
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
private val log = KotlinLogging.logger {}
@Service
class PublicEventService(
private val kafkaTemplate: KafkaTemplate<String, AccountMessage>,
) {
fun updateAccount(
account: AccountEntity,
changes: List<String>,
): Mono<AccountEntity> = Mono
.fromFuture(kafkaTemplate.send("account", "change", AccountMessage(account.id!!, changes)))
.doOnSuccess { log.trace { "Sent Kafka created event for user ${account.id}" } }
.doOnError { e -> log.error(e) { "Failed to send Kafka event" } }
.thenReturn(account)
}

View File

@@ -0,0 +1,25 @@
package ltd.hlaeja.util
import ltd.hlaeja.entity.AccountEntity
import org.springframework.http.HttpStatus.ACCEPTED
import org.springframework.web.server.ResponseStatusException
fun AccountEntity.detectChanges(original: AccountEntity): List<String> {
val changes: MutableList<String> = mutableListOf()
if (original.password != password) {
changes.add("password")
}
if (original.username != username) {
changes.add("username")
}
if (original.enabled != enabled) {
changes.add("enabled")
}
if (original.roles != roles) {
changes.add("roles")
}
if (changes.isEmpty()) {
throw ResponseStatusException(ACCEPTED)
}
return changes
}

View File

@@ -4,6 +4,7 @@ import java.time.ZonedDateTime
import ltd.hlaeja.entity.AccountEntity import ltd.hlaeja.entity.AccountEntity
import ltd.hlaeja.library.accountRegistry.Account import ltd.hlaeja.library.accountRegistry.Account
import org.springframework.http.HttpStatus.EXPECTATION_FAILED import org.springframework.http.HttpStatus.EXPECTATION_FAILED
import org.springframework.http.HttpStatus.BAD_REQUEST
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
@@ -23,6 +24,21 @@ fun Account.Request.toAccountEntity(
updatedAt = ZonedDateTime.now(), updatedAt = ZonedDateTime.now(),
enabled = enabled, enabled = enabled,
username = username, username = username,
password = passwordEncoder.encode(password), password = password
?.let { passwordEncoder.encode(it) }
?: throw ResponseStatusException(BAD_REQUEST),
roles = roles.joinToString(","), roles = roles.joinToString(","),
) )
fun AccountEntity.updateAccountEntity(
request: Account.Request,
passwordEncoder: PasswordEncoder,
): AccountEntity = this.copy(
updatedAt = ZonedDateTime.now(),
enabled = request.enabled,
username = request.username,
password = request.password
?.let { passwordEncoder.encode(it) }
?: this.password,
roles = request.roles.joinToString(","),
)

View File

@@ -0,0 +1,19 @@
package ltd.hlaeja.validator
import jakarta.validation.ConstraintValidator
import jakarta.validation.ConstraintValidatorContext
import ltd.hlaeja.library.accountRegistry.Account
class AccountValidator : ConstraintValidator<ValidAccount, Any> {
override fun isValid(value: Any?, context: ConstraintValidatorContext): Boolean {
return when (value) {
is Account.Request -> value.validate()
else -> true // Default to valid if the type is not a list
}
}
private fun Account.Request.validate(): Boolean = username.isNotBlank() &&
password?.isNotBlank() ?: true &&
roles.isNotEmpty()
}

View File

@@ -0,0 +1,15 @@
package ltd.hlaeja.validator
import jakarta.validation.Constraint
import jakarta.validation.Payload
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.reflect.KClass
@Constraint(validatedBy = [AccountValidator::class])
@Retention(RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class ValidAccount(
val message: String = "Roles must not be empty",
val groups: Array<KClass<out Any>> = [],
val payload: Array<KClass<out Payload>> = [],
)

View File

@@ -9,6 +9,25 @@ spring:
os: os:
name: "%APP_BUILD_OS_NAME%" name: "%APP_BUILD_OS_NAME%"
version: "%APP_BUILD_OS_VERSION%" version: "%APP_BUILD_OS_VERSION%"
r2dbc:
username: services
kafka:
producer:
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
management:
endpoints:
access:
default: none
web:
exposure:
include: "health,info"
endpoint:
health:
show-details: always
access: read_only
info:
access: read_only
jwt: jwt:
private-key: cert/private_key.pem private-key: cert/private_key.pem
@@ -23,8 +42,9 @@ spring:
on-profile: development on-profile: development
r2dbc: r2dbc:
url: r2dbc:postgresql://localhost:5432/account_registry url: r2dbc:postgresql://localhost:5432/account_registry
username: services
password: password password: password
kafka:
bootstrap-servers: localhost:9091
--- ---
########################## ##########################
@@ -36,14 +56,17 @@ spring:
on-profile: docker on-profile: docker
r2dbc: r2dbc:
url: r2dbc:postgresql://PostgreSQL:5432/account_registry url: r2dbc:postgresql://PostgreSQL:5432/account_registry
username: services
password: password password: password
kafka:
bootstrap-servers: kafka:9092
--- ---
############################## ##############################
### Production environment ### ### Kubernetes environment ###
############################## ##############################
spring: spring:
config: config:
activate: activate:
on-profile: production on-profile: kubernetes
r2dbc:
url: r2dbc:postgresql://dependency-postgresql:5432/account_registry

View File

@@ -7,7 +7,10 @@
<root level="INFO"> <root level="INFO">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
<springProfile name="develop|docker"> <springProfile name="development">
<logger level="TRACE" name="ltd.hlaeja"/>
</springProfile>
<springProfile name="docker">
<logger level="DEBUG" name="ltd.hlaeja"/> <logger level="DEBUG" name="ltd.hlaeja"/>
</springProfile> </springProfile>
</configuration> </configuration>

View File

@@ -1,11 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger level="DEBUG" name="ltd.hlaeja"/>
</configuration>

View File

@@ -0,0 +1,306 @@
package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.test.container.KafkaPostgresTestContainer
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.http.HttpStatus.ACCEPTED
import org.springframework.http.HttpStatus.CONFLICT
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@KafkaPostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(SoftAssertionsExtension::class)
class AccountEndpoint {
@InjectSoftAssertions
lateinit var softly: SoftAssertions
@LocalServerPort
var port: Int = 0
lateinit var webClient: WebTestClient
@BeforeEach
fun setup() {
webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
@Nested
inner class GetAccount {
@Test
fun `get account with valid uuid`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000001")
// when
val result = webClient.get().uri("/account-$uuid").exchange()
// then
result.expectStatus().isOk()
.expectBody<Account.Response>()
.consumeWith {
softly.assertThat(it.responseBody?.id).isEqualTo(uuid)
softly.assertThat(it.responseBody?.username).isEqualTo("admin")
softly.assertThat(it.responseBody?.enabled).isTrue
softly.assertThat(it.responseBody?.roles?.size).isEqualTo(1)
softly.assertThat(it.responseBody?.roles?.get(0)).isEqualTo("ADMIN")
}
}
@Test
fun `get account with invalid uuid`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000000")
// when
val result = webClient.get().uri("/account-$uuid").exchange()
// then
result.expectStatus().isNotFound
}
@Test
fun `get account with bad uuid`() {
// given
val uuidInvalid = "000000000001"
// when
val result = webClient.get().uri("/account-$uuidInvalid").exchange()
// then
result.expectStatus().isBadRequest
}
}
@Nested
inner class PutAccount {
@Test
fun `success account with all changes`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000003")
val request = Account.Request(
username = "usernameA",
password = "abc123",
enabled = true,
roles = listOf("USER", "TEST"),
)
// when
val result = webClient.put().uri("/account-$uuid").bodyValue(request).exchange()
// then
result.expectStatus().isOk()
.expectBody<Account.Response>()
.consumeWith {
softly.assertThat(it.responseBody?.id).isEqualTo(uuid)
softly.assertThat(it.responseBody?.username).isEqualTo("usernameA")
softly.assertThat(it.responseBody?.enabled).isTrue
softly.assertThat(it.responseBody?.roles?.size).isEqualTo(2)
softly.assertThat(it.responseBody?.roles).contains("USER")
softly.assertThat(it.responseBody?.roles).contains("TEST")
}
}
@Test
fun `success account with null password changes`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000003")
val request = Account.Request(
username = "usernameB",
password = null,
enabled = false,
roles = listOf("TEST"),
)
// when
val result = webClient.put().uri("/account-$uuid").bodyValue(request).exchange()
// then
result.expectStatus().isOk()
.expectBody<Account.Response>()
.consumeWith {
softly.assertThat(it.responseBody?.id).isEqualTo(uuid)
softly.assertThat(it.responseBody?.username).isEqualTo("usernameB")
softly.assertThat(it.responseBody?.enabled).isFalse
softly.assertThat(it.responseBody?.roles?.size).isEqualTo(1)
softly.assertThat(it.responseBody?.roles).contains("TEST")
}
}
@Test
fun `success account with no changes`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000002")
val request = Account.Request(
username = "user",
password = null,
enabled = true,
roles = listOf("USER"),
)
// when
val result = webClient.put().uri("/account-$uuid").bodyValue(request).exchange()
// then
result.expectStatus().isEqualTo(ACCEPTED)
}
@Test
fun `failed username duplicate`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000002")
val request = Account.Request(
username = "admin",
password = null,
enabled = true,
roles = listOf("USER"),
)
// when
val result = webClient.put().uri("/account-$uuid").bodyValue(request).exchange()
// then
result.expectStatus().isEqualTo(CONFLICT)
}
@Test
fun `failed account not found`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000000")
val request = Account.Request(
username = "admin",
password = null,
enabled = true,
roles = listOf("USER"),
)
// when
val result = webClient.put().uri("/account-$uuid").bodyValue(request).exchange()
// then
result.expectStatus().isNotFound
}
}
@Nested
inner class PostAccount {
@ParameterizedTest
@CsvSource(
"new-user, new-pass, true, 2, USER;TEST",
"admin-user, admin-pass, false, 1, ADMIN",
"test-user, test-pass, true, 1, USER",
)
fun `success added account`(
username: String,
password: String,
enabled: Boolean,
size: Int,
roleList: String,
) {
// given
val roles: List<String> = roleList.split(";")
val request = Account.Request(
username = username,
password = password,
enabled = enabled,
roles = roles,
)
// when
val result = webClient.post().uri("/account").bodyValue(request).exchange()
// then
result.expectStatus().isCreated
.expectBody<Account.Response>()
.consumeWith {
softly.assertThat(it.responseBody?.id?.version()).isEqualTo(7)
softly.assertThat(it.responseBody?.username).isEqualTo(username)
softly.assertThat(it.responseBody?.enabled).isEqualTo(enabled)
softly.assertThat(it.responseBody?.roles?.size).isEqualTo(size)
for (role in roles) {
softly.assertThat(it.responseBody?.roles).contains(role)
}
}
}
@ParameterizedTest
@CsvSource(
"'', new-pass, TEST",
"new-user, '', ADMIN",
"new-user, new-pass, ''",
)
fun `validation fail on empty values`(
username: String,
password: String,
roleList: String,
) {
// given
val request = Account.Request(
username = username,
password = password,
enabled = true,
roles = when {
roleList.isEmpty() -> emptyList()
else -> listOf(roleList)
},
)
// when
val result = webClient.post().uri("/account").bodyValue(request).exchange()
// then
result.expectStatus().isBadRequest
}
@Test
fun `fail username take`() {
// given
val request = Account.Request(
username = "user",
password = "new-pass",
enabled = true,
roles = listOf("USER", "TEST"),
)
// when
val result = webClient.post().uri("/account").bodyValue(request).exchange()
// then
result.expectStatus().isEqualTo(CONFLICT)
}
@Test
fun `fail password null`() {
// given
val request = Account.Request(
username = "user",
password = null,
enabled = true,
roles = listOf("USER", "TEST"),
)
// when
val result = webClient.post().uri("/account").bodyValue(request).exchange()
// then
result.expectStatus().isBadRequest
}
}
}

View File

@@ -0,0 +1,111 @@
package ltd.hlaeja.controller
import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.test.container.PostgresTestContainer
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
class AccountsEndpoint {
@LocalServerPort
var port: Int = 0
lateinit var webClient: WebTestClient
@BeforeEach
fun setup() {
webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
@Test
fun `get accounts`() {
// when
val result = webClient.get().uri("/accounts").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Account.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(3)
}
}
@ParameterizedTest
@CsvSource(
value = [
"1,3",
"2,0",
],
)
fun `get accounts with pages`(page: Int, expected: Int) {
// when
val result = webClient.get().uri("/accounts/page-$page").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Account.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@Test
fun `get accounts with bad pages`() {
// when
val result = webClient.get().uri("/accounts/page-0").exchange()
// then
result.expectStatus().isBadRequest
}
@ParameterizedTest
@CsvSource(
value = [
"1,2,2",
"2,2,1",
"3,2,0",
"1,5,3",
"2,5,0",
],
)
fun `get accounts with pages and size to show`(page: Int, show: Int, expected: Int) {
// when
val result = webClient.get().uri("/accounts/page-$page/show-$show").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Account.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@ParameterizedTest
@CsvSource(
value = [
"1,0",
"0,1",
"0,0",
"1,-1",
"-1,1",
"-1,-1",
],
)
fun `get accounts with bad pages or bad size to show`(page: Int, show: Int) {
// when
val result = webClient.get().uri("/accounts/page-$page/show-$show").exchange()
// then
result.expectStatus().isBadRequest
}
}

View File

@@ -0,0 +1,116 @@
package ltd.hlaeja.controller
import ltd.hlaeja.library.accountRegistry.Authentication
import ltd.hlaeja.test.compareToFile
import ltd.hlaeja.test.container.PostgresTestContainer
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.http.HttpStatus
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(SoftAssertionsExtension::class)
class AuthenticationEndpoint {
@InjectSoftAssertions
lateinit var softly: SoftAssertions
@LocalServerPort
var port: Int = 0
lateinit var webClient: WebTestClient
@BeforeEach
fun setup() {
webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
@Test
fun `login as admin`() {
// given
val request = Authentication.Request(
username = "admin",
password = "pass",
)
// when
val result = webClient.post().uri("/authenticate").bodyValue(request).exchange()
// then
result.expectStatus().isOk()
.expectBody<Authentication.Response>()
.consumeWith { assertThat(it.responseBody?.token).compareToFile("authenticate/admin-token.data") }
}
@Test
fun `login as user`() {
// given
val request = Authentication.Request(
username = "user",
password = "pass",
)
// when
val result = webClient.post().uri("/authenticate").bodyValue(request).exchange()
// then
result.expectStatus().isOk()
.expectBody<Authentication.Response>()
.consumeWith { assertThat(it.responseBody?.token).compareToFile("authenticate/user-token.data") }
}
@Test
fun `login as disabled user`() {
// given
val request = Authentication.Request(
username = "disabled",
password = "pass",
)
// when
val result = webClient.post().uri("/authenticate").bodyValue(request).exchange()
// then
result.expectStatus().isEqualTo(HttpStatus.LOCKED)
}
@Test
fun `login as non-existent `() {
// given
val request = Authentication.Request(
username = "username",
password = "pass",
)
// when
val result = webClient.post().uri("/authenticate").bodyValue(request).exchange()
// then
result.expectStatus().isNotFound
}
@Test
fun `login as user bad password`() {
// given
val request = Authentication.Request(
username = "user",
password = "password",
)
// when
val result = webClient.post().uri("/authenticate").bodyValue(request).exchange()
// then
result.expectStatus().isUnauthorized
}
}

View File

@@ -0,0 +1,19 @@
jwt:
private-key: cert/valid-private-key.pem
spring:
r2dbc:
url: r2dbc:postgresql://placeholder
username: placeholder
password: placeholder
kafka:
consumer:
group-id: test-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "*"
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer

View File

@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMSIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiQURNSU4ifQ.Z2mc__k9apWjJoZzJ3DWZuDiVN_jpisWtd0ecwrMnk1NrJ5Uw25pgrXPwn2aY0qYFAe0UGbE-4FhjUCxWLkknR0B-2_86IKHmN1A7z8lTqMRkK7qH-71uK0Y3o0kWKn117-FoSKDG-oefQE42NTwsSrzhiaEIzhUd0fsIyKuQCbDRol79rX5cU1HwOI8DHowpNEgvCLW1ogMWJDygq5GDgQI2HmV8vbnO1soFjKzvW3pz0sHWTimhAi76gl5mD_Lv_DdywcQWndwcGEoNj-SgHuKWktaG2_yzkoC9FQqWBgU7tukuycmLkbde_Oagydt2CAfPsBebu4Ac81UHGdUpw

View File

@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMiIsInVzZXJuYW1lIjoidXNlciIsInJvbGUiOiJVU0VSIn0.kpmQYxhkyKsIjo9mJaysBXW0xdv8UjlmNnVsYNfBu-Tdro_0nQFVzhCcjaD6_TUhx2-3vSkvTwDtmMHsP0JC5B43K473o2zQjyHYzCNakPcNHiste9llNj12n5qUCOUMgCKb7ZztLffSIsYlSL7hyRwwmTaz73MDMYvLWAa4AgSNm8JPe3HkTkqRJ4YZ-saKO9Q0Vb9LLftB7T3b9P5kHYqzwISBsRm1rYHRRpGYs5goR2Qax1hLJBbQR4bswaeTRfl3fQ66mIr6mZqiY279wCzzueLuGyJPFzeZQYiQ2JiYRq3H2NyXCsWKCt2bK-YNwol1K3fYLPSq9kap-AGasQ

View File

@@ -1,4 +1,5 @@
-- Test data
insert into public.accounts (id, created_at, updated_at, enabled, username, password, roles) insert into public.accounts (id, created_at, updated_at, enabled, username, password, roles)
values ('00000000-0000-7000-0000-000000000001'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'admin', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_ADMIN'), values ('00000000-0000-7000-0000-000000000001'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'admin', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ADMIN'),
('00000000-0000-7000-0000-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'user', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_USER'), ('00000000-0000-7000-0000-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', true, 'user', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'USER'),
('00000000-0000-7000-0000-000000000003'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', false, 'disabled', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'ROLE_USER'); ('00000000-0000-7000-0000-000000000003'::uuid, '2000-01-01 00:00:00.000001 +00:00', '2000-01-01 00:00:01.000001 +00:00', false, 'disabled', '$2a$12$KoXBoLOANMK11J4xeJHPA.Sy0FG.m8KWk7P4XFsMO.ZbFmFI2DckK', 'USER');

View File

@@ -0,0 +1,11 @@
-- Disable triggers on the tables
ALTER TABLE accounts DISABLE TRIGGER ALL;
ALTER TABLE accounts_audit DISABLE TRIGGER ALL;
-- Truncate tables
TRUNCATE TABLE accounts_audit CASCADE;
TRUNCATE TABLE accounts CASCADE;
-- Enable triggers on the account table
ALTER TABLE accounts ENABLE TRIGGER ALL;
ALTER TABLE accounts_audit ENABLE TRIGGER ALL;

View File

@@ -0,0 +1,74 @@
-- FUNCTION: public.gen_uuid_v7(timestamp with time zone)
CREATE OR REPLACE FUNCTION public.gen_uuid_v7(p_timestamp timestamp with time zone)
RETURNS uuid
LANGUAGE 'sql'
COST 100
VOLATILE PARALLEL UNSAFE
AS
$BODY$
-- Replace the first 48 bits of a uuid v4 with the provided timestamp (in milliseconds) since 1970-01-01 UTC, and set the version to 7
SELECT encode(set_bit(set_bit(overlay(uuid_send(gen_random_uuid()) PLACING substring(int8send((extract(EPOCH FROM p_timestamp) * 1000):: BIGINT) FROM 3) FROM 1 FOR 6), 52, 1), 53, 1), 'hex') ::uuid;
$BODY$;
-- FUNCTION: public.gen_uuid_v7()
CREATE OR REPLACE FUNCTION public.gen_uuid_v7()
RETURNS uuid
LANGUAGE 'sql'
COST 100
VOLATILE PARALLEL UNSAFE
AS
$BODY$
SELECT gen_uuid_v7(clock_timestamp());
$BODY$;
-- Table: public.accounts
CREATE TABLE IF NOT EXISTS public.accounts
(
id UUID DEFAULT gen_uuid_v7(),
created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
enabled boolean NOT NULL DEFAULT true,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
roles VARCHAR(255) NOT NULL,
CONSTRAINT pk_contact_types PRIMARY KEY (id)
);
-- Index: idx_accounts_username
CREATE INDEX IF NOT EXISTS idx_accounts_username
ON public.accounts USING btree (username COLLATE pg_catalog."default" ASC NULLS LAST)
TABLESPACE pg_default;
-- Table: public.accounts_audit
CREATE TABLE IF NOT EXISTS public.accounts_audit
(
id uuid NOT NULL,
timestamp timestamp with time zone NOT NULL,
enabled boolean NOT NULL,
username VARCHAR(50) NOT NULL,
password VARCHAR(255) NOT NULL,
roles VARCHAR(255) NOT NULL,
CONSTRAINT pk_accounts_audit PRIMARY KEY (id, timestamp)
) TABLESPACE pg_default;
-- FUNCTION: public.accounts_audit()
CREATE OR REPLACE FUNCTION public.accounts_audit()
RETURNS trigger
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF
AS
$BODY$
BEGIN
INSERT INTO accounts_audit (id, timestamp, enabled, username, password, roles)
VALUES (NEW.id, NEW.updated_at, NEW.enabled, NEW.username, NEW.password, NEW.roles);
RETURN NULL; -- result is ignored since this is an AFTER trigger
END;
$BODY$;
-- Trigger: accounts_audit_trigger
CREATE OR REPLACE TRIGGER accounts_audit_trigger
AFTER INSERT OR UPDATE
ON public.accounts
FOR EACH ROW
EXECUTE FUNCTION public.accounts_audit();

View File

@@ -9,13 +9,31 @@ import ltd.hlaeja.entity.AccountEntity
import ltd.hlaeja.repository.AccountRepository import ltd.hlaeja.repository.AccountRepository
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.dao.DuplicateKeyException
import org.springframework.http.HttpStatus.BAD_REQUEST
import org.springframework.http.HttpStatus.CONFLICT
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.test.StepVerifier import reactor.test.StepVerifier
class AccountServiceTest { class AccountServiceTest {
companion object { companion object {
val account = UUID.fromString("00000000-0000-0000-0000-000000000002") val account = UUID.fromString("00000000-0000-0000-0000-000000000002")
val accountEntity = AccountEntity(
account,
ZonedDateTime.now(),
ZonedDateTime.now(),
true,
"username",
"password",
"ROLE_TEST",
)
val accounts = Flux.just(
accountEntity.copy(username = "username1"),
accountEntity.copy(username = "username2"),
accountEntity.copy(username = "username3"),
)
} }
private lateinit var accountRepository: AccountRepository private lateinit var accountRepository: AccountRepository
@@ -30,16 +48,6 @@ class AccountServiceTest {
@Test @Test
fun `get account by id - success`() { fun `get account by id - success`() {
// given // given
val accountEntity = AccountEntity(
account,
ZonedDateTime.now(),
ZonedDateTime.now(),
true,
"username",
"password",
"ROLE_TEST",
)
every { accountRepository.findById(any(UUID::class)) } returns Mono.just(accountEntity) every { accountRepository.findById(any(UUID::class)) } returns Mono.just(accountEntity)
// when // when
@@ -64,4 +72,175 @@ class AccountServiceTest {
// then // then
verify { accountRepository.findById(any(UUID::class)) } verify { accountRepository.findById(any(UUID::class)) }
} }
@Test
fun `get account by username - success`() {
// given
every { accountRepository.findByUsername(any()) } returns Mono.just(accountEntity)
// when
StepVerifier.create(accountService.getUserByUsername("username"))
.expectNext(accountEntity)
.verifyComplete()
// then
verify { accountRepository.findByUsername(any()) }
}
@Test
fun `get account by username - fail does not exist`() {
// given
every { accountRepository.findByUsername(any()) } returns Mono.empty()
// when
StepVerifier.create(accountService.getUserByUsername("username"))
.expectError(ResponseStatusException::class.java)
.verify()
// then
verify { accountRepository.findByUsername(any()) }
}
@Test
fun `add account - success`() {
// given
every { accountRepository.save(any()) } returns Mono.just(accountEntity)
// when
StepVerifier.create(accountService.addAccount(accountEntity))
.expectNext(accountEntity)
.verifyComplete()
// then
verify { accountRepository.save(any()) }
}
@Test
fun `add account - fail duplicated user`() {
// given
every { accountRepository.save(any()) } returns Mono.error(DuplicateKeyException("Test"))
// when
StepVerifier.create(accountService.addAccount(accountEntity))
.expectErrorMatches { error ->
error is ResponseStatusException && error.statusCode == CONFLICT
}
.verify()
// then
verify { accountRepository.save(any()) }
}
@Test
fun `add account - fail`() {
// given
every { accountRepository.save(any()) } returns Mono.error(RuntimeException())
// when
StepVerifier.create(accountService.addAccount(accountEntity))
.expectErrorMatches { error ->
error is ResponseStatusException && error.statusCode == BAD_REQUEST
}
.verify()
// then
verify { accountRepository.save(any()) }
}
@Test
fun `get accounts - limit size success`() {
// given
every { accountRepository.findAll() } returns accounts
// when
StepVerifier.create(accountService.getAccounts(1, 2))
.expectNextMatches { accountEntity ->
accountEntity.username == "username1"
}
.expectNextMatches { accountEntity ->
accountEntity.username == "username2"
}
.verifyComplete()
// then
verify { accountRepository.findAll() }
}
@Test
fun `get accounts - negative page fail`() {
// given
every { accountRepository.findAll() } returns accounts
// when
StepVerifier.create(accountService.getAccounts(-1, 10))
.expectErrorMatches { error ->
error is ResponseStatusException && error.statusCode == BAD_REQUEST
}
.verify()
// then
verify { accountRepository.findAll() }
}
@Test
fun `get accounts - negative size fail`() {
// given
every { accountRepository.findAll() } returns accounts
// when
StepVerifier.create(accountService.getAccounts(1, -10))
.expectErrorMatches { error ->
error is ResponseStatusException && error.statusCode == BAD_REQUEST
}
.verify()
// then
verify { accountRepository.findAll() }
}
@Test
fun `update account - success`() {
// given
every { accountRepository.save(any()) } returns Mono.just(accountEntity)
// when
StepVerifier.create(accountService.updateAccount(accountEntity))
.expectNext(accountEntity)
.verifyComplete()
// then
verify { accountRepository.save(any()) }
}
@Test
fun `update account - fail duplicated user`() {
// given
every { accountRepository.save(any()) } returns Mono.error(DuplicateKeyException("Test"))
// when
StepVerifier.create(accountService.updateAccount(accountEntity))
.expectErrorMatches { error ->
error is ResponseStatusException && error.statusCode == CONFLICT
}
.verify()
// then
verify { accountRepository.save(any()) }
}
@Test
fun `update account - fail`() {
// given
every { accountRepository.save(any()) } returns Mono.error(RuntimeException())
// when
StepVerifier.create(accountService.updateAccount(accountEntity))
.expectErrorMatches { error ->
error is ResponseStatusException && error.statusCode == BAD_REQUEST
}
.verify()
// then
verify { accountRepository.save(any()) }
}
} }

View File

@@ -0,0 +1,109 @@
package ltd.hlaeja.util
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.UUID
import ltd.hlaeja.entity.AccountEntity
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.springframework.web.server.ResponseStatusException
@ExtendWith(SoftAssertionsExtension::class)
class AccountUtilKtTest {
companion object {
val account = UUID.fromString("00000000-0000-0000-0000-000000000000")
val timestamp: ZonedDateTime = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0, 1), ZoneId.of("UTC"))
val originalUser = AccountEntity(
id = account,
createdAt = timestamp,
updatedAt = timestamp,
enabled = true,
username = "username",
password = "password",
roles = "ROLE_TEST",
)
}
@InjectSoftAssertions
lateinit var softly: SoftAssertions
@Test
fun `no change detected`() {
// given
val account = AccountEntity(
id = account,
createdAt = timestamp,
updatedAt = timestamp,
enabled = true,
username = "username",
password = "password",
roles = "ROLE_TEST",
)
// then exception
assertThrows(ResponseStatusException::class.java) {
account.detectChanges(originalUser)
}
}
@ParameterizedTest
@CsvSource(
"false, username, password, ROLE_TEST, enabled",
"true, change, password, ROLE_TEST, username",
"true, username, change, ROLE_TEST, password",
"true, username, password, ROLE_CHANGE, roles",
)
fun `change one thing`(
enabled: Boolean,
username: String,
password: String,
roles: String,
expected: String,
) {
// given
val account = AccountEntity(
id = account,
createdAt = timestamp,
updatedAt = timestamp,
enabled = enabled,
username = username,
password = password,
roles = roles,
)
// when
val result = account.detectChanges(originalUser)
// then
softly.assertThat(result).hasSize(1)
softly.assertThat(result[0]).isEqualTo(expected)
}
@Test
fun `change everything`() {
// given
val account = AccountEntity(
id = account,
createdAt = timestamp,
updatedAt = timestamp,
enabled = false,
username = "change",
password = "change",
roles = "ROLE_CHANGE",
)
// when
val result = account.detectChanges(originalUser)
// then
softly.assertThat(result).hasSize(4)
softly.assertThat(result).contains("password", "username", "enabled", "roles")
}
}

View File

@@ -1,6 +1,7 @@
package ltd.hlaeja.util package ltd.hlaeja.util
import io.mockk.every import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.unmockkStatic import io.mockk.unmockkStatic
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -9,23 +10,46 @@ import java.time.ZonedDateTime
import java.util.UUID import java.util.UUID
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import ltd.hlaeja.entity.AccountEntity import ltd.hlaeja.entity.AccountEntity
import ltd.hlaeja.library.accountRegistry.Account
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
@ExtendWith(SoftAssertionsExtension::class)
class MappingKtTest { class MappingKtTest {
companion object { companion object {
val account = UUID.fromString("00000000-0000-0000-0000-000000000002") val account = UUID.fromString("00000000-0000-0000-0000-000000000000")
val timestamp: ZonedDateTime = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0, 1), ZoneId.of("UTC")) val utc = ZoneId.of("UTC")
val timestamp: ZonedDateTime = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0, 1), utc)
val originalTimestamp: ZonedDateTime = ZonedDateTime.of(LocalDateTime.of(1000, 1, 1, 0, 0, 0, 1), utc)
val originalUser = AccountEntity(
id = account,
username = "username",
enabled = true,
roles = "ROLE_TEST",
password = "password",
createdAt = originalTimestamp,
updatedAt = originalTimestamp,
)
} }
@InjectSoftAssertions
lateinit var softly: SoftAssertions
private val passwordEncoder: BCryptPasswordEncoder = mockk()
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
mockkStatic(ZonedDateTime::class) mockkStatic(ZonedDateTime::class)
every { ZonedDateTime.now() } returns timestamp every { ZonedDateTime.now() } returns timestamp
every { passwordEncoder.encode(any()) } answers { firstArg<String>() }
} }
@AfterEach @AfterEach
@@ -38,7 +62,7 @@ class MappingKtTest {
@Test @Test
fun `test toAccountResponse when id is not null`() { fun `test toAccountResponse when id is not null`() {
// Arrange // given
val accountEntity = AccountEntity( val accountEntity = AccountEntity(
id = account, id = account,
createdAt = timestamp, createdAt = timestamp,
@@ -49,10 +73,10 @@ class MappingKtTest {
roles = "ROLE_ADMIN,ROLE_USER", roles = "ROLE_ADMIN,ROLE_USER",
) )
// Act // when
val result = accountEntity.toAccountResponse() val result = accountEntity.toAccountResponse()
// Assert // then
assertThat(result.id).isEqualTo(accountEntity.id) assertThat(result.id).isEqualTo(accountEntity.id)
assertThat(result.timestamp).isEqualTo(accountEntity.updatedAt) assertThat(result.timestamp).isEqualTo(accountEntity.updatedAt)
assertThat(result.enabled).isEqualTo(accountEntity.enabled) assertThat(result.enabled).isEqualTo(accountEntity.enabled)
@@ -62,7 +86,7 @@ class MappingKtTest {
@Test @Test
fun `test toAccountResponse when id is null`() { fun `test toAccountResponse when id is null`() {
// Arrange // given
val accountEntity = AccountEntity( val accountEntity = AccountEntity(
id = null, id = null,
createdAt = timestamp, createdAt = timestamp,
@@ -73,10 +97,126 @@ class MappingKtTest {
roles = "ROLE_ADMIN,ROLE_USER", roles = "ROLE_ADMIN,ROLE_USER",
) )
// Act and Assert // when exception
assertFailsWith<ResponseStatusException> { assertFailsWith<ResponseStatusException> {
accountEntity.toAccountResponse() accountEntity.toAccountResponse()
} }
} }
} }
@Nested
inner class CreateAccountMapping {
@Test
fun `all fields changed`() {
// given
val request = Account.Request(
username = "username",
enabled = false,
roles = listOf("ROLE_TEST"),
password = "password",
)
// when
val updatedUser = request.toAccountEntity(passwordEncoder)
// then
softly.assertThat(updatedUser.id).isNull()
softly.assertThat(updatedUser.createdAt).isEqualTo(timestamp)
softly.assertThat(updatedUser.updatedAt).isEqualTo(timestamp)
softly.assertThat(updatedUser.enabled).isEqualTo(request.enabled)
softly.assertThat(updatedUser.username).isEqualTo(request.username)
softly.assertThat(updatedUser.password).isEqualTo(request.password)
softly.assertThat(updatedUser.roles).isEqualTo("ROLE_TEST")
}
@Test
fun `provided password is null`() {
// Given
val request = Account.Request(
username = "username",
enabled = false,
roles = listOf("ROLE_TEST"),
password = null,
)
// when exception
assertFailsWith<ResponseStatusException> {
request.toAccountEntity(passwordEncoder)
}
}
}
@Nested
inner class UpdateAccountMapping {
@Test
fun `all fields changed`() {
// Given
val request = Account.Request(
username = "new-username",
enabled = false,
roles = listOf("ROLE_MAGIC"),
password = "new-password",
)
// When
val updatedUser = originalUser.updateAccountEntity(request, passwordEncoder)
// Then
softly.assertThat(updatedUser.id).isEqualTo(originalUser.id)
softly.assertThat(updatedUser.createdAt).isEqualTo(originalUser.createdAt)
softly.assertThat(updatedUser.updatedAt).isEqualTo(timestamp)
softly.assertThat(updatedUser.enabled).isEqualTo(request.enabled)
softly.assertThat(updatedUser.username).isEqualTo(request.username)
softly.assertThat(updatedUser.password).isEqualTo(request.password)
softly.assertThat(updatedUser.roles).isEqualTo("ROLE_MAGIC")
}
@Test
fun `provided password is null`() {
// Given
val request = Account.Request(
username = originalUser.username,
enabled = originalUser.enabled,
roles = originalUser.roles.split(","),
password = null,
)
// When
val updatedUser = originalUser.updateAccountEntity(request, passwordEncoder)
// Then
softly.assertThat(updatedUser.id).isEqualTo(account)
softly.assertThat(updatedUser.createdAt).isEqualTo(originalUser.createdAt)
softly.assertThat(updatedUser.updatedAt).isEqualTo(timestamp)
softly.assertThat(updatedUser.enabled).isEqualTo(request.enabled)
softly.assertThat(updatedUser.username).isEqualTo(request.username)
softly.assertThat(updatedUser.password).isEqualTo(originalUser.password)
softly.assertThat(updatedUser.roles).isEqualTo(originalUser.roles)
}
@Test
fun `roles changed from single to multiple`() {
// Given
val request = Account.Request(
username = originalUser.username,
enabled = originalUser.enabled,
roles = listOf("ROLE_MAGIC", "ROLE_TEST"),
password = null,
)
// When
val updatedUser = originalUser.updateAccountEntity(request, passwordEncoder)
// Then
softly.assertThat(updatedUser.id).isEqualTo(originalUser.id)
softly.assertThat(updatedUser.createdAt).isEqualTo(originalUser.createdAt)
softly.assertThat(updatedUser.updatedAt).isEqualTo(timestamp)
softly.assertThat(updatedUser.enabled).isEqualTo(originalUser.enabled)
softly.assertThat(updatedUser.username).isEqualTo(originalUser.username)
softly.assertThat(updatedUser.password).isEqualTo(originalUser.password)
softly.assertThat(updatedUser.roles).isEqualTo("ROLE_MAGIC,ROLE_TEST")
}
}
} }

View File

@@ -0,0 +1,108 @@
package ltd.hlaeja.validator
import io.mockk.mockk
import ltd.hlaeja.library.accountRegistry.Account
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class AccountValidatorTest {
val validator = AccountValidator()
@Test
fun `validate account - success all values`() {
// given
val request = Account.Request(
username = "validUser",
password = "strongPassword",
enabled = true,
roles = listOf("USER", "TEST"),
)
// when
val result = validator.isValid(request, mockk())
// then
assertThat(result).isTrue
}
@Test
fun `validate account - success password null`() {
// given
val request = Account.Request(
username = "validUser",
password = null,
enabled = true,
roles = listOf("USER"),
)
// when
val result = validator.isValid(request, mockk())
// then
assertThat(result).isTrue
}
@Test
fun `validate account - failed username empty`() {
// given
val request = Account.Request(
username = "",
password = "strongPassword",
enabled = true,
roles = listOf("USER"),
)
// when
val result = validator.isValid(request, mockk())
// then
assertThat(result).isFalse
}
@Test
fun `validate account - failed password empty`() {
// given
val request = Account.Request(
username = "validUser",
password = "",
enabled = true,
roles = listOf("USER"),
)
// when
val result = validator.isValid(request, mockk())
// then
assertThat(result).isFalse
}
@Test
fun `validate account - failed roles empty`() {
// given
val request = Account.Request(
username = "validUser",
password = "",
enabled = true,
roles = emptyList(),
)
// when
val result = validator.isValid(request, mockk())
// then
assertThat(result).isFalse
}
@Test
fun `validate account - success wrong data type`() {
// given
val request = "A string"
// when
val result = validator.isValid(request, mockk())
// then
assertThat(result).isTrue
}
}

View File

@@ -1,8 +0,0 @@
jwt:
private-key: cert/valid-private-key.pem
spring:
r2dbc:
url: r2dbc:postgresql://placeholder
username: placeholder
password: placeholder