add update accounts

- add update accounts to account.http
- add updateAccount to AccountController
- add AccountEntity updateAccountEntity to Mapping.kt
- add updateAccount in AccountService
- update catalog version in gradle.properties
This commit is contained in:
2025-01-28 16:30:57 +01:00
parent 72ac37e603
commit 6e6ea72d54
7 changed files with 284 additions and 24 deletions

View File

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

View File

@@ -1,7 +1,7 @@
### get user by id ### get user by id
GET {{hostname}}/account-00000000-0000-7000-0000-000000000001 GET {{hostname}}/account-00000000-0000-7000-0000-000000000001
### Get admin information ### add user
POST {{hostname}}/account POST {{hostname}}/account
Content-Type: application/json Content-Type: application/json
@@ -18,8 +18,33 @@ Content-Type: application/json
### Get accounts ### Get accounts
GET {{hostname}}/accounts GET {{hostname}}/accounts
### Get accounts ### Get accounts by page
GET {{hostname}}/accounts/page-1 GET {{hostname}}/accounts/page-1
### Get accounts ### Get accounts by page and size
GET {{hostname}}/accounts/page-1/show-5 GET {{hostname}}/accounts/page-1/show-5
### 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"
]
}

View File

@@ -1,16 +1,21 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import java.util.UUID import java.util.UUID
import ltd.hlaeja.entity.AccountEntity
import ltd.hlaeja.library.accountRegistry.Account import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.service.AccountService import ltd.hlaeja.service.AccountService
import ltd.hlaeja.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.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.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@@ -30,6 +35,18 @@ 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 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")
fun addAccount( fun addAccount(
@RequestBody request: Account.Request, @RequestBody request: Account.Request,
@@ -55,4 +72,12 @@ class AccountController(
size: Int, size: Int,
): Flux<Account.Response> = accountService.getAccounts(page, size) ): Flux<Account.Response> = accountService.getAccounts(page, size)
.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
} }

View File

@@ -35,13 +35,7 @@ class AccountService(
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 {
it is DuplicateKeyException -> Mono.error(ResponseStatusException(HttpStatus.CONFLICT))
else -> Mono.error(ResponseStatusException(HttpStatus.BAD_REQUEST))
}
}
fun getAccounts(page: Int, size: Int): Flux<AccountEntity> = try { fun getAccounts(page: Int, size: Int): Flux<AccountEntity> = try {
accountRepository.findAll() accountRepository.findAll()
@@ -49,6 +43,20 @@ class AccountService(
.take(size.toLong()) .take(size.toLong())
.doOnNext { log.debug { "Retrieved accounts $page with size $size" } } .doOnNext { log.debug { "Retrieved accounts $page with size $size" } }
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Flux.error(ResponseStatusException(HttpStatus.BAD_REQUEST)) Flux.error(ResponseStatusException(HttpStatus.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(HttpStatus.CONFLICT))
else -> Mono.error(ResponseStatusException(HttpStatus.BAD_REQUEST))
}
} }
} }

View File

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

View File

@@ -197,4 +197,50 @@ class AccountServiceTest {
// then // then
verify { accountRepository.findAll() } verify { accountRepository.findAll() }
} }
@Test
fun `update account - success`() {
// given
every { accountRepository.save(any()) } returns Mono.just(accountEntity)
// when
StepVerifier.create(accountService.updateAccount(accountEntity))
.expectNext(accountEntity)
.verifyComplete()
// then
verify { accountRepository.save(any()) }
}
@Test
fun `update account - fail duplicated user`() {
// given
every { accountRepository.save(any()) } returns Mono.error(DuplicateKeyException("Test"))
// when
StepVerifier.create(accountService.updateAccount(accountEntity))
.expectErrorMatches { error ->
error is ResponseStatusException && error.statusCode == CONFLICT
}
.verify()
// then
verify { accountRepository.save(any()) }
}
@Test
fun `update account - fail`() {
// given
every { accountRepository.save(any()) } returns Mono.error(RuntimeException())
// when
StepVerifier.create(accountService.updateAccount(accountEntity))
.expectErrorMatches { error ->
error is ResponseStatusException && error.statusCode == BAD_REQUEST
}
.verify()
// then
verify { accountRepository.save(any()) }
}
} }

View File

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