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]
|
[*.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
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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.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)
|
||||||
@@ -29,6 +31,15 @@ dependencies {
|
|||||||
testImplementation(hlaeja.springboot.starter.test)
|
testImplementation(hlaeja.springboot.starter.test)
|
||||||
|
|
||||||
testRuntimeOnly(hlaeja.junit.platform.launcher)
|
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"
|
group = "ltd.hlaeja"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
version=0.1.0
|
version=0.2.0
|
||||||
catalog=0.8.0
|
catalog=0.9.0
|
||||||
container.port.host=9050
|
container.port.host=9050
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -14,3 +14,28 @@ Content-Type: application/json
|
|||||||
"ROLE_TEST"
|
"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
|
package ltd.hlaeja.controller
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import ltd.hlaeja.validator.ValidAccount
|
||||||
|
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.util.toAccountEntity
|
import ltd.hlaeja.util.toAccountEntity
|
||||||
import ltd.hlaeja.util.toAccountResponse
|
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.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
|
||||||
@@ -25,9 +33,30 @@ 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)
|
||||||
|
.also { if (hasChange(user, it)) throw ResponseStatusException(ACCEPTED) }
|
||||||
|
}
|
||||||
|
.flatMap { accountService.updateAccount(it) }
|
||||||
|
.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() }
|
||||||
|
|
||||||
|
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.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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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()) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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