Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afedd19143 | ||
| f0ff324cf2 | |||
| cf1b78ae0a | |||
| 69e293a25f | |||
| dec6b99281 | |||
| 93aad65385 | |||
| 3effd930ad | |||
| 97b8becd08 | |||
|
|
5e0ba7ed2a | ||
|
|
32a630d6a3 | ||
| c468a5ffa3 | |||
| 3849fa8676 | |||
| 1ee306c151 | |||
| 18e95f7213 | |||
| 82c590dc30 | |||
| da491cecfa | |||
| 3bc5805a87 | |||
| ec5991f20b | |||
| a9b5abda1a | |||
| c7eb3484e6 | |||
| 5951af7d44 | |||
| c08c3cb880 | |||
| 6165bcd512 | |||
| ddc701ea51 | |||
| a762a05c11 | |||
| 6e6ea72d54 | |||
| 72ac37e603 | |||
| f8154fe05f |
@@ -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
|
||||||
|
|||||||
12
.github/workflows/release.yml
vendored
Normal file
12
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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
|
||||||
12
.github/workflows/run-checks.yml
vendored
Normal file
12
.github/workflows/run-checks.yml
vendored
Normal 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 }}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
version=0.1.0
|
version=0.4.0
|
||||||
catalog=0.8.0
|
catalog=0.12.0
|
||||||
container.port.host=9050
|
container.port.host=9050
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
9
gradlew
vendored
@@ -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
4
gradlew.bat
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
8
http/accounts.http
Normal 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
5
http/actuator.http
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
### get actuator
|
||||||
|
GET {{hostname}}/actuator
|
||||||
|
|
||||||
|
### get actuator health
|
||||||
|
GET {{hostname}}/actuator/health
|
||||||
89
release.sh
89
release.sh
@@ -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'
|
|
||||||
6
sql/001-update_user_roles.sql
Normal file
6
sql/001-update_user_roles.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
UPDATE public.accounts
|
||||||
|
SET
|
||||||
|
roles = REPLACE(roles, 'ROLE_', ''),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE
|
||||||
|
roles LIKE '%ROLE_%';
|
||||||
@@ -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';
|
||||||
5
sql/test/test_001-account.sql
Normal file
5
sql/test/test_001-account.sql
Normal 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');
|
||||||
@@ -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() }
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/main/kotlin/ltd/hlaeja/controller/AccountsController.kt
Normal file
42
src/main/kotlin/ltd/hlaeja/controller/AccountsController.kt
Normal 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() }
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/main/kotlin/ltd/hlaeja/service/PublicEventService.kt
Normal file
25
src/main/kotlin/ltd/hlaeja/service/PublicEventService.kt
Normal 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)
|
||||||
|
}
|
||||||
25
src/main/kotlin/ltd/hlaeja/util/AccountUtil.kt
Normal file
25
src/main/kotlin/ltd/hlaeja/util/AccountUtil.kt
Normal 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
|
||||||
|
}
|
||||||
@@ -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(","),
|
||||||
|
)
|
||||||
|
|||||||
19
src/main/kotlin/ltd/hlaeja/validator/AccountValidator.kt
Normal file
19
src/main/kotlin/ltd/hlaeja/validator/AccountValidator.kt
Normal 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()
|
||||||
|
}
|
||||||
15
src/main/kotlin/ltd/hlaeja/validator/ValidAccount.kt
Normal file
15
src/main/kotlin/ltd/hlaeja/validator/ValidAccount.kt
Normal 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>> = [],
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/test-integration/resources/application.yml
Normal file
19
src/test-integration/resources/application.yml
Normal 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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMSIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiQURNSU4ifQ.Z2mc__k9apWjJoZzJ3DWZuDiVN_jpisWtd0ecwrMnk1NrJ5Uw25pgrXPwn2aY0qYFAe0UGbE-4FhjUCxWLkknR0B-2_86IKHmN1A7z8lTqMRkK7qH-71uK0Y3o0kWKn117-FoSKDG-oefQE42NTwsSrzhiaEIzhUd0fsIyKuQCbDRol79rX5cU1HwOI8DHowpNEgvCLW1ogMWJDygq5GDgQI2HmV8vbnO1soFjKzvW3pz0sHWTimhAi76gl5mD_Lv_DdywcQWndwcGEoNj-SgHuKWktaG2_yzkoC9FQqWBgU7tukuycmLkbde_Oagydt2CAfPsBebu4Ac81UHGdUpw
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMiIsInVzZXJuYW1lIjoidXNlciIsInJvbGUiOiJVU0VSIn0.kpmQYxhkyKsIjo9mJaysBXW0xdv8UjlmNnVsYNfBu-Tdro_0nQFVzhCcjaD6_TUhx2-3vSkvTwDtmMHsP0JC5B43K473o2zQjyHYzCNakPcNHiste9llNj12n5qUCOUMgCKb7ZztLffSIsYlSL7hyRwwmTaz73MDMYvLWAa4AgSNm8JPe3HkTkqRJ4YZ-saKO9Q0Vb9LLftB7T3b9P5kHYqzwISBsRm1rYHRRpGYs5goR2Qax1hLJBbQR4bswaeTRfl3fQ66mIr6mZqiY279wCzzueLuGyJPFzeZQYiQ2JiYRq3H2NyXCsWKCt2bK-YNwol1K3fYLPSq9kap-AGasQ
|
||||||
@@ -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');
|
||||||
11
src/test-integration/resources/postgres/reset.sql
Normal file
11
src/test-integration/resources/postgres/reset.sql
Normal 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;
|
||||||
74
src/test-integration/resources/postgres/schema.sql
Normal file
74
src/test-integration/resources/postgres/schema.sql
Normal 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();
|
||||||
@@ -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()) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
109
src/test/kotlin/ltd/hlaeja/util/AccountUtilKtTest.kt
Normal file
109
src/test/kotlin/ltd/hlaeja/util/AccountUtilKtTest.kt
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
108
src/test/kotlin/ltd/hlaeja/validator/AccountValidatorTest.kt
Normal file
108
src/test/kotlin/ltd/hlaeja/validator/AccountValidatorTest.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
jwt:
|
|
||||||
private-key: cert/valid-private-key.pem
|
|
||||||
|
|
||||||
spring:
|
|
||||||
r2dbc:
|
|
||||||
url: r2dbc:postgresql://placeholder
|
|
||||||
username: placeholder
|
|
||||||
password: placeholder
|
|
||||||
Reference in New Issue
Block a user