Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec5991f20b | |||
| a9b5abda1a | |||
| c7eb3484e6 | |||
| 5951af7d44 | |||
| c08c3cb880 | |||
| 6165bcd512 | |||
| ddc701ea51 | |||
| a762a05c11 | |||
| 6e6ea72d54 | |||
| 72ac37e603 | |||
| f8154fe05f |
@@ -17,6 +17,10 @@ tab_width = 2
|
||||
[*.bat]
|
||||
end_of_line = crlf
|
||||
|
||||
[*.data]
|
||||
max_line_length = 1024
|
||||
insert_final_newline = false
|
||||
|
||||
[*.pem]
|
||||
max_line_length = 64
|
||||
insert_final_newline = false
|
||||
|
||||
@@ -8,14 +8,16 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(hlaeja.fasterxml.jackson.module.kotlin)
|
||||
implementation(hlaeja.kotlin.logging)
|
||||
implementation(hlaeja.kotlin.reflect)
|
||||
implementation(hlaeja.kotlinx.coroutines)
|
||||
implementation(hlaeja.library.hlaeja.common.messages)
|
||||
implementation(hlaeja.library.hlaeja.jwt)
|
||||
implementation(hlaeja.library.common.messages)
|
||||
implementation(hlaeja.library.jwt)
|
||||
implementation(hlaeja.springboot.starter.actuator)
|
||||
implementation(hlaeja.springboot.starter.r2dbc)
|
||||
implementation(hlaeja.springboot.starter.security)
|
||||
implementation(hlaeja.springboot.starter.validation)
|
||||
implementation(hlaeja.springboot.starter.webflux)
|
||||
|
||||
runtimeOnly(hlaeja.postgresql)
|
||||
@@ -29,6 +31,15 @@ dependencies {
|
||||
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)
|
||||
|
||||
integrationTestRuntimeOnly(hlaeja.junit.platform.launcher)
|
||||
}
|
||||
|
||||
group = "ltd.hlaeja"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
kotlin.code.style=official
|
||||
version=0.1.0
|
||||
catalog=0.8.0
|
||||
version=0.2.0
|
||||
catalog=0.9.0
|
||||
container.port.host=9050
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
### get user by id
|
||||
GET {{hostname}}/account-00000000-0000-7000-0000-000000000001
|
||||
|
||||
### Get admin information
|
||||
### add user
|
||||
POST {{hostname}}/account
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -14,3 +14,28 @@ Content-Type: application/json
|
||||
"ROLE_TEST"
|
||||
]
|
||||
}
|
||||
|
||||
### update user all information
|
||||
PUT {{hostname}}/account-00000000-0000-7000-0000-000000000002
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"enabled": true,
|
||||
"roles": [
|
||||
"ROLE_TEST"
|
||||
]
|
||||
}
|
||||
|
||||
### update user information
|
||||
PUT {{hostname}}/account-00000000-0000-7000-0000-000000000002
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "user",
|
||||
"enabled": true,
|
||||
"roles": [
|
||||
"ROLE_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
|
||||
@@ -0,0 +1,306 @@
|
||||
package ltd.hlaeja.controller
|
||||
|
||||
import java.util.UUID
|
||||
import ltd.hlaeja.library.accountRegistry.Account
|
||||
import ltd.hlaeja.test.container.PostgresContainer
|
||||
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
|
||||
|
||||
@PostgresContainer
|
||||
@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("ROLE_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("ROLE_USER", "ROLE_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("ROLE_USER")
|
||||
softly.assertThat(it.responseBody?.roles).contains("ROLE_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("ROLE_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("ROLE_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("ROLE_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("ROLE_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("ROLE_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, ROLE_USER;ROLE_TEST",
|
||||
"admin-user, admin-pass, false, 1, ROLE_ADMIN",
|
||||
"test-user, test-pass, true, 1, ROLE_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, ROLE_TEST",
|
||||
"new-user, '', ROLE_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("ROLE_USER", "ROLE_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("ROLE_USER", "ROLE_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.PostgresContainer
|
||||
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
|
||||
|
||||
@PostgresContainer
|
||||
@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 org.assertj.core.api.Assertions.assertThat
|
||||
import ltd.hlaeja.library.accountRegistry.Authentication
|
||||
import ltd.hlaeja.test.compareToFile
|
||||
import ltd.hlaeja.test.container.PostgresContainer
|
||||
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
|
||||
|
||||
@PostgresContainer
|
||||
@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
|
||||
}
|
||||
}
|
||||
8
src/integration-test/resources/application.yml
Normal file
8
src/integration-test/resources/application.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
jwt:
|
||||
private-key: cert/valid-private-key.pem
|
||||
|
||||
spring:
|
||||
r2dbc:
|
||||
url: r2dbc:pool:postgresql://localhost:5432/test
|
||||
username: test
|
||||
password: test
|
||||
@@ -0,0 +1 @@
|
||||
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMSIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiUk9MRV9BRE1JTiJ9.D6pK86XPWcdu1imV_y_4nAM6R4WEZvJpQ7oGaPAYe0_rg3UWdmVMa8Iw7L21bRgFoyIa7FQBwb_0AXojFVdb2mdOVDeGOwxQZAx23dwqeicOGd8yUMnuBaRSnd7z4P65KPMbbf0NOTQtho0Iv5mBAwFMJoF67sw-yntfx3cD_bfrI-Rf4oZaZsVn38Y2HJBe2sO2QI4e5_7s82ikxac416OX7PcIEgaf3IeEK1fSzSjRG_dyBGT_Jq_vAzVURsSu4ep976kI-k5ZXNE9EMxKu1S-n5c5eiaqo96ObnaSl4eWFik5q8vLhNLYIYO-bQi1xlJKnStwZqtUwlR763Gd5w
|
||||
@@ -0,0 +1 @@
|
||||
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMiIsInVzZXJuYW1lIjoidXNlciIsInJvbGUiOiJST0xFX1VTRVIifQ.GvZIq0VF9xB8UY3PUGdnc6JNeUXtv4LzHJ56hWSeqUS6BXH0M_QJ5Lu9ndh9_P85CECp3eKrW4fKymGYe-NUXCtrzhr9-SSZLF6D7GRzAJ4yZjVRCOa_dgqe1RGuIZyZpli36z4NPqeBFqtHJ3Cs5rAI-WdvxGfWPgtM2kzpSJ_0zFihp9mVcZBlWP57HlN7-oKzDJWVpO2E17fWZTy-y4pdrIUsff63c256Cy8NhiAgux9aqZTdzaqp9TsXw59bRsS5d0YH7-gJuBd4xctZwgy_41BOcRk2q-nLyLZgWJs1wmCa_zaW0Fj6fjAsYvpdPNegkpIqrHJcQpGd7nE0KQ
|
||||
5
src/integration-test/resources/postgres/data.sql
Normal file
5
src/integration-test/resources/postgres/data.sql
Normal file
@@ -0,0 +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');
|
||||
11
src/integration-test/resources/postgres/reset.sql
Normal file
11
src/integration-test/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;
|
||||
TRUNCATE TABLE accounts;
|
||||
|
||||
-- Enable triggers on the account table
|
||||
ALTER TABLE accounts ENABLE TRIGGER ALL;
|
||||
ALTER TABLE accounts_audit ENABLE TRIGGER ALL;
|
||||
79
src/integration-test/resources/postgres/schema.sql
Normal file
79
src/integration-test/resources/postgres/schema.sql
Normal file
@@ -0,0 +1,79 @@
|
||||
-- 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();
|
||||
-- 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');
|
||||
@@ -1,16 +1,24 @@
|
||||
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.util.toAccountEntity
|
||||
import ltd.hlaeja.util.toAccountResponse
|
||||
import ltd.hlaeja.util.updateAccountEntity
|
||||
import org.springframework.http.HttpStatus.ACCEPTED
|
||||
import org.springframework.http.HttpStatus.CREATED
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
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.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@RestController
|
||||
@@ -25,9 +33,30 @@ class AccountController(
|
||||
): Mono<Account.Response> = accountService.getUserById(uuid)
|
||||
.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)
|
||||
.also { if (hasChange(user, it)) throw ResponseStatusException(ACCEPTED) }
|
||||
}
|
||||
.flatMap { accountService.updateAccount(it) }
|
||||
.map { it.toAccountResponse() }
|
||||
|
||||
@PostMapping("/account")
|
||||
@ResponseStatus(CREATED)
|
||||
fun addAccount(
|
||||
@RequestBody request: Account.Request,
|
||||
@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
|
||||
}
|
||||
|
||||
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.repository.AccountRepository
|
||||
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.web.server.ResponseStatusException
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
@@ -21,23 +24,40 @@ class AccountService(
|
||||
uuid: UUID,
|
||||
): Mono<AccountEntity> = accountRepository.findById(uuid)
|
||||
.doOnNext { log.debug { "Get account ${it.id}" } }
|
||||
.switchIfEmpty(Mono.error(ResponseStatusException(HttpStatus.NOT_FOUND)))
|
||||
.switchIfEmpty(Mono.error(ResponseStatusException(NOT_FOUND)))
|
||||
|
||||
fun getUserByUsername(
|
||||
username: String,
|
||||
): Mono<AccountEntity> = accountRepository.findByUsername(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(
|
||||
accountEntity: AccountEntity,
|
||||
): Mono<AccountEntity> = accountRepository.save(accountEntity)
|
||||
.doOnNext { log.debug { "Added new type: $it.id" } }
|
||||
.onErrorResume {
|
||||
log.debug { it.localizedMessage }
|
||||
when {
|
||||
it is DuplicateKeyException -> Mono.error(ResponseStatusException(HttpStatus.CONFLICT))
|
||||
else -> Mono.error(ResponseStatusException(HttpStatus.BAD_REQUEST))
|
||||
.onErrorResume(::onSaveError)
|
||||
|
||||
fun getAccounts(page: Int, size: Int): Flux<AccountEntity> = try {
|
||||
accountRepository.findAll()
|
||||
.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.time.ZonedDateTime
|
||||
import ltd.hlaeja.entity.AccountEntity
|
||||
import ltd.hlaeja.library.accountRegistry.Account
|
||||
import org.springframework.http.HttpStatus.EXPECTATION_FAILED
|
||||
import org.springframework.http.HttpStatus.BAD_REQUEST
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
|
||||
@@ -23,6 +24,21 @@ fun Account.Request.toAccountEntity(
|
||||
updatedAt = ZonedDateTime.now(),
|
||||
enabled = enabled,
|
||||
username = username,
|
||||
password = passwordEncoder.encode(password),
|
||||
password = password
|
||||
?.let { passwordEncoder.encode(it) }
|
||||
?: throw ResponseStatusException(BAD_REQUEST),
|
||||
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,13 +9,31 @@ import ltd.hlaeja.entity.AccountEntity
|
||||
import ltd.hlaeja.repository.AccountRepository
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
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 reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.test.StepVerifier
|
||||
|
||||
class AccountServiceTest {
|
||||
companion object {
|
||||
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
|
||||
@@ -30,16 +48,6 @@ class AccountServiceTest {
|
||||
@Test
|
||||
fun `get account by id - success`() {
|
||||
// given
|
||||
val accountEntity = AccountEntity(
|
||||
account,
|
||||
ZonedDateTime.now(),
|
||||
ZonedDateTime.now(),
|
||||
true,
|
||||
"username",
|
||||
"password",
|
||||
"ROLE_TEST",
|
||||
)
|
||||
|
||||
every { accountRepository.findById(any(UUID::class)) } returns Mono.just(accountEntity)
|
||||
|
||||
// when
|
||||
@@ -64,4 +72,175 @@ class AccountServiceTest {
|
||||
// then
|
||||
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()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ltd.hlaeja.util
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import java.time.LocalDateTime
|
||||
@@ -9,23 +10,46 @@ import java.time.ZonedDateTime
|
||||
import java.util.UUID
|
||||
import kotlin.test.assertFailsWith
|
||||
import ltd.hlaeja.entity.AccountEntity
|
||||
import ltd.hlaeja.library.accountRegistry.Account
|
||||
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.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
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
|
||||
|
||||
@ExtendWith(SoftAssertionsExtension::class)
|
||||
class MappingKtTest {
|
||||
companion object {
|
||||
val account = UUID.fromString("00000000-0000-0000-0000-000000000002")
|
||||
val timestamp: ZonedDateTime = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0, 1), ZoneId.of("UTC"))
|
||||
val account = UUID.fromString("00000000-0000-0000-0000-000000000000")
|
||||
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
|
||||
fun setUp() {
|
||||
mockkStatic(ZonedDateTime::class)
|
||||
every { ZonedDateTime.now() } returns timestamp
|
||||
every { passwordEncoder.encode(any()) } answers { firstArg<String>() }
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -38,7 +62,7 @@ class MappingKtTest {
|
||||
|
||||
@Test
|
||||
fun `test toAccountResponse when id is not null`() {
|
||||
// Arrange
|
||||
// given
|
||||
val accountEntity = AccountEntity(
|
||||
id = account,
|
||||
createdAt = timestamp,
|
||||
@@ -49,10 +73,10 @@ class MappingKtTest {
|
||||
roles = "ROLE_ADMIN,ROLE_USER",
|
||||
)
|
||||
|
||||
// Act
|
||||
// when
|
||||
val result = accountEntity.toAccountResponse()
|
||||
|
||||
// Assert
|
||||
// then
|
||||
assertThat(result.id).isEqualTo(accountEntity.id)
|
||||
assertThat(result.timestamp).isEqualTo(accountEntity.updatedAt)
|
||||
assertThat(result.enabled).isEqualTo(accountEntity.enabled)
|
||||
@@ -62,7 +86,7 @@ class MappingKtTest {
|
||||
|
||||
@Test
|
||||
fun `test toAccountResponse when id is null`() {
|
||||
// Arrange
|
||||
// given
|
||||
val accountEntity = AccountEntity(
|
||||
id = null,
|
||||
createdAt = timestamp,
|
||||
@@ -73,10 +97,126 @@ class MappingKtTest {
|
||||
roles = "ROLE_ADMIN,ROLE_USER",
|
||||
)
|
||||
|
||||
// Act and Assert
|
||||
// when exception
|
||||
assertFailsWith<ResponseStatusException> {
|
||||
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