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,16 +1,21 @@
package ltd.hlaeja.controller
import java.util.UUID
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.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.RestController
import org.springframework.web.server.ResponseStatusException
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@@ -30,6 +35,18 @@ class AccountController(
): Mono<Account.Response> = accountService.getUserById(uuid)
.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")
fun addAccount(
@RequestBody request: Account.Request,
@@ -55,4 +72,12 @@ class AccountController(
size: Int,
): Flux<Account.Response> = accountService.getAccounts(page, size)
.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,
): 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()
@@ -49,6 +43,20 @@ class AccountService(
.take(size.toLong())
.doOnNext { log.debug { "Retrieved accounts $page with size $size" } }
} 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.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(","),
)

View File

@@ -153,11 +153,11 @@ class AccountServiceTest {
every { accountRepository.findAll() } returns accounts
// when
StepVerifier.create(accountService.getAccounts(1,2))
StepVerifier.create(accountService.getAccounts(1, 2))
.expectNextMatches { accountEntity ->
accountEntity.username == "username1"
}
.expectNextMatches {accountEntity ->
.expectNextMatches { accountEntity ->
accountEntity.username == "username2"
}
.verifyComplete()
@@ -172,7 +172,7 @@ class AccountServiceTest {
every { accountRepository.findAll() } returns accounts
// when
StepVerifier.create(accountService.getAccounts(-1,10))
StepVerifier.create(accountService.getAccounts(-1, 10))
.expectErrorMatches { error ->
error is ResponseStatusException && error.statusCode == BAD_REQUEST
}
@@ -188,7 +188,7 @@ class AccountServiceTest {
every { accountRepository.findAll() } returns accounts
// when
StepVerifier.create(accountService.getAccounts(1,-10))
StepVerifier.create(accountService.getAccounts(1, -10))
.expectErrorMatches { error ->
error is ResponseStatusException && error.statusCode == BAD_REQUEST
}
@@ -197,4 +197,50 @@ class AccountServiceTest {
// 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()) }
}
}

View File

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