17 Commits

Author SHA1 Message Date
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
41 changed files with 319 additions and 190 deletions

View File

@@ -9,7 +9,7 @@ insert_final_newline = true
max_line_length = 120
tab_width = 4
[*.{md,sh,sql,yaml,yml}]
[*.{md,sh,sql,xml,xsd,yaml,yml}]
max_line_length = 1024
indent_size = 2
tab_width = 2

12
.github/workflows/release.yml vendored Normal file
View 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
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
Run `release.sh` script from `master` branch.
Run release pipeline from `master` branch.
## Development Information

View File

@@ -1,10 +1,10 @@
plugins {
alias(hlaeja.plugins.kotlin.jvm)
alias(hlaeja.plugins.kotlin.spring)
alias(hlaeja.plugins.ltd.hlaeja.plugin.certificate)
alias(hlaeja.plugins.ltd.hlaeja.plugin.service)
alias(hlaeja.plugins.spring.boot)
alias(hlaeja.plugins.spring.dependency.management)
alias(hlaeja.plugins.springframework.boot)
alias(hlaeja.plugins.certificate)
alias(hlaeja.plugins.service)
}
dependencies {
@@ -14,6 +14,7 @@ dependencies {
implementation(hlaeja.kotlinx.coroutines)
implementation(hlaeja.library.common.messages)
implementation(hlaeja.library.jwt)
implementation(hlaeja.springboot.kafka)
implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.springboot.starter.r2dbc)
implementation(hlaeja.springboot.starter.security)
@@ -28,18 +29,19 @@ dependencies {
testImplementation(hlaeja.projectreactor.reactor.test)
testImplementation(hlaeja.kotlin.test.junit5)
testImplementation(hlaeja.kotlinx.coroutines.test)
testImplementation(hlaeja.springboot.kafka.test)
testImplementation(hlaeja.springboot.starter.test)
testRuntimeOnly(hlaeja.junit.platform.launcher)
integrationTestImplementation(hlaeja.assertj.core)
integrationTestImplementation(hlaeja.library.test)
integrationTestImplementation(hlaeja.projectreactor.reactor.test)
integrationTestImplementation(hlaeja.kotlin.test.junit5)
integrationTestImplementation(hlaeja.kotlinx.coroutines.test)
integrationTestImplementation(hlaeja.springboot.starter.test)
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)
integrationTestRuntimeOnly(hlaeja.junit.platform.launcher)
testIntegrationRuntimeOnly(hlaeja.junit.platform.launcher)
}
group = "ltd.hlaeja"

View File

@@ -1,4 +1,4 @@
kotlin.code.style=official
version=0.2.0
catalog=0.9.0
version=0.4.0
catalog=0.12.0
container.port.host=9050

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
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
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

9
gradlew vendored
View File

@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# 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
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -115,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# 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.
# * 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.
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

4
gradlew.bat vendored
View File

@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@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
@rem End local scope for the variables with windows NT shell

View File

@@ -10,8 +10,8 @@ Content-Type: application/json
"password": "p4ssw0rd",
"enabled": true,
"roles": [
"ROLE_ADMIN",
"ROLE_TEST"
"ADMIN",
"TEST"
]
}
@@ -24,7 +24,7 @@ Content-Type: application/json
"password": "pass",
"enabled": true,
"roles": [
"ROLE_TEST"
"TEST"
]
}
@@ -36,6 +36,6 @@ Content-Type: application/json
"username": "user",
"enabled": true,
"roles": [
"ROLE_TEST"
"TEST"
]
}

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

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

View File

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

View File

@@ -1,4 +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', 'ROLE_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-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');
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

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

View File

@@ -1 +0,0 @@
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMSIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiUk9MRV9BRE1JTiJ9.D6pK86XPWcdu1imV_y_4nAM6R4WEZvJpQ7oGaPAYe0_rg3UWdmVMa8Iw7L21bRgFoyIa7FQBwb_0AXojFVdb2mdOVDeGOwxQZAx23dwqeicOGd8yUMnuBaRSnd7z4P65KPMbbf0NOTQtho0Iv5mBAwFMJoF67sw-yntfx3cD_bfrI-Rf4oZaZsVn38Y2HJBe2sO2QI4e5_7s82ikxac416OX7PcIEgaf3IeEK1fSzSjRG_dyBGT_Jq_vAzVURsSu4ep976kI-k5ZXNE9EMxKu1S-n5c5eiaqo96ObnaSl4eWFik5q8vLhNLYIYO-bQi1xlJKnStwZqtUwlR763Gd5w

View File

@@ -1 +0,0 @@
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMiIsInVzZXJuYW1lIjoidXNlciIsInJvbGUiOiJST0xFX1VTRVIifQ.GvZIq0VF9xB8UY3PUGdnc6JNeUXtv4LzHJ56hWSeqUS6BXH0M_QJ5Lu9ndh9_P85CECp3eKrW4fKymGYe-NUXCtrzhr9-SSZLF6D7GRzAJ4yZjVRCOa_dgqe1RGuIZyZpli36z4NPqeBFqtHJ3Cs5rAI-WdvxGfWPgtM2kzpSJ_0zFihp9mVcZBlWP57HlN7-oKzDJWVpO2E17fWZTy-y4pdrIUsff63c256Cy8NhiAgux9aqZTdzaqp9TsXw59bRsS5d0YH7-gJuBd4xctZwgy_41BOcRk2q-nLyLZgWJs1wmCa_zaW0Fj6fjAsYvpdPNegkpIqrHJcQpGd7nE0KQ

View File

@@ -1,13 +1,15 @@
package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.validator.ValidAccount
import ltd.hlaeja.entity.AccountEntity
import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.service.AccountService
import ltd.hlaeja.service.PublicEventService
import ltd.hlaeja.util.detectChanges
import ltd.hlaeja.util.toAccountEntity
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
@@ -25,6 +27,7 @@ import reactor.core.publisher.Mono
class AccountController(
private val accountService: AccountService,
private val passwordEncoder: PasswordEncoder,
private val publicEventService: PublicEventService,
) {
@GetMapping("/account-{uuid}")
@@ -40,9 +43,13 @@ class AccountController(
): Mono<Account.Response> = accountService.getUserById(uuid)
.map { user ->
user.updateAccountEntity(request, passwordEncoder)
.also { if (hasChange(user, it)) throw ResponseStatusException(ACCEPTED) }
.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) }
}
.flatMap { accountService.updateAccount(it) }
.map { it.toAccountResponse() }
@PostMapping("/account")
@@ -51,12 +58,4 @@ class AccountController(
@RequestBody @ValidAccount request: Account.Request,
): Mono<Account.Response> = accountService.addAccount(request.toAccountEntity(passwordEncoder))
.map { it.toAccountResponse() }
private fun hasChange(
user: AccountEntity,
update: AccountEntity,
): Boolean = user.password == update.password &&
user.username == update.username &&
user.enabled == update.enabled &&
user.roles == update.roles
}

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

@@ -9,6 +9,25 @@ spring:
os:
name: "%APP_BUILD_OS_NAME%"
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:
private-key: cert/private_key.pem
@@ -23,8 +42,9 @@ spring:
on-profile: development
r2dbc:
url: r2dbc:postgresql://localhost:5432/account_registry
username: services
password: password
kafka:
bootstrap-servers: localhost:9091
---
##########################
@@ -36,14 +56,17 @@ spring:
on-profile: docker
r2dbc:
url: r2dbc:postgresql://PostgreSQL:5432/account_registry
username: services
password: password
kafka:
bootstrap-servers: kafka:9092
---
##############################
### Production environment ###
### Kubernetes environment ###
##############################
spring:
config:
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">
<appender-ref ref="STDOUT"/>
</root>
<springProfile name="develop|docker">
<springProfile name="development">
<logger level="TRACE" name="ltd.hlaeja"/>
</springProfile>
<springProfile name="docker">
<logger level="DEBUG" name="ltd.hlaeja"/>
</springProfile>
</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

@@ -2,7 +2,7 @@ package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.test.container.PostgresContainer
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
@@ -20,7 +20,7 @@ import org.springframework.http.HttpStatus.CONFLICT
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresContainer
@KafkaPostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(SoftAssertionsExtension::class)
class AccountEndpoint {
@@ -57,7 +57,7 @@ class AccountEndpoint {
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("ROLE_ADMIN")
softly.assertThat(it.responseBody?.roles?.get(0)).isEqualTo("ADMIN")
}
}
@@ -97,7 +97,7 @@ class AccountEndpoint {
username = "usernameA",
password = "abc123",
enabled = true,
roles = listOf("ROLE_USER", "ROLE_TEST"),
roles = listOf("USER", "TEST"),
)
// when
@@ -111,8 +111,8 @@ class AccountEndpoint {
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("ROLE_USER")
softly.assertThat(it.responseBody?.roles).contains("ROLE_TEST")
softly.assertThat(it.responseBody?.roles).contains("USER")
softly.assertThat(it.responseBody?.roles).contains("TEST")
}
}
@@ -124,7 +124,7 @@ class AccountEndpoint {
username = "usernameB",
password = null,
enabled = false,
roles = listOf("ROLE_TEST"),
roles = listOf("TEST"),
)
// when
@@ -138,7 +138,7 @@ class AccountEndpoint {
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("ROLE_TEST")
softly.assertThat(it.responseBody?.roles).contains("TEST")
}
}
@@ -150,7 +150,7 @@ class AccountEndpoint {
username = "user",
password = null,
enabled = true,
roles = listOf("ROLE_USER"),
roles = listOf("USER"),
)
// when
@@ -168,7 +168,7 @@ class AccountEndpoint {
username = "admin",
password = null,
enabled = true,
roles = listOf("ROLE_USER"),
roles = listOf("USER"),
)
// when
@@ -186,7 +186,7 @@ class AccountEndpoint {
username = "admin",
password = null,
enabled = true,
roles = listOf("ROLE_USER"),
roles = listOf("USER"),
)
// when
@@ -202,9 +202,9 @@ class AccountEndpoint {
@ParameterizedTest
@CsvSource(
"new-user, new-pass, true, 2, ROLE_USER;ROLE_TEST",
"admin-user, admin-pass, false, 1, ROLE_ADMIN",
"test-user, test-pass, true, 1, ROLE_USER",
"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,
@@ -242,8 +242,8 @@ class AccountEndpoint {
@ParameterizedTest
@CsvSource(
"'', new-pass, ROLE_TEST",
"new-user, '', ROLE_ADMIN",
"'', new-pass, TEST",
"new-user, '', ADMIN",
"new-user, new-pass, ''",
)
fun `validation fail on empty values`(
@@ -276,7 +276,7 @@ class AccountEndpoint {
username = "user",
password = "new-pass",
enabled = true,
roles = listOf("ROLE_USER", "ROLE_TEST"),
roles = listOf("USER", "TEST"),
)
// when
@@ -293,7 +293,7 @@ class AccountEndpoint {
username = "user",
password = null,
enabled = true,
roles = listOf("ROLE_USER", "ROLE_TEST"),
roles = listOf("USER", "TEST"),
)
// when

View File

@@ -1,7 +1,7 @@
package ltd.hlaeja.controller
import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.test.container.PostgresContainer
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
@@ -13,7 +13,7 @@ import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresContainer
@PostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
class AccountsEndpoint {

View File

@@ -1,9 +1,9 @@
package ltd.hlaeja.controller
import org.assertj.core.api.Assertions.assertThat
import ltd.hlaeja.library.accountRegistry.Authentication
import ltd.hlaeja.test.compareToFile
import ltd.hlaeja.test.container.PostgresContainer
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
@@ -17,7 +17,7 @@ import org.springframework.http.HttpStatus
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresContainer
@PostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(SoftAssertionsExtension::class)
class AuthenticationEndpoint {

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,5 +1,5 @@
-- Test data
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'),
('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-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');
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-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

@@ -3,8 +3,8 @@ ALTER TABLE accounts DISABLE TRIGGER ALL;
ALTER TABLE accounts_audit DISABLE TRIGGER ALL;
-- Truncate tables
TRUNCATE TABLE accounts_audit;
TRUNCATE TABLE accounts;
TRUNCATE TABLE accounts_audit CASCADE;
TRUNCATE TABLE accounts CASCADE;
-- Enable triggers on the account table
ALTER TABLE accounts ENABLE TRIGGER ALL;

View File

@@ -72,8 +72,3 @@ CREATE OR REPLACE TRIGGER accounts_audit_trigger
ON public.accounts
FOR EACH ROW
EXECUTE FUNCTION public.accounts_audit();
-- Test data
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'),
('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-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');

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")
}
}