Get Account

- add AccountController
- add AccountEntity toAccountResponse in Mapping.kt
- add AccountService
- add AccountRepository
- add AccountEntity
This commit is contained in:
2024-12-29 07:07:44 +01:00
parent 573b4bd6fe
commit 6aee16c4a2
12 changed files with 330 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ dependencies {
implementation(hlaeja.kotlin.logging)
implementation(hlaeja.kotlin.reflect)
implementation(hlaeja.kotlinx.coroutines)
implementation(hlaeja.library.hlaeja.common.messages)
implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.springboot.starter.r2dbc)
implementation(hlaeja.springboot.starter.webflux)

2
http/account.http Normal file
View File

@@ -0,0 +1,2 @@
### get user by id
GET {{hostname}}/account-00000000-0000-7000-0000-000000000001

35
sql/002-account.sql Normal file
View File

@@ -0,0 +1,35 @@
-- Table: public.accounts
-- DROP TABLE IF EXISTS 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)
);
ALTER TABLE IF EXISTS public.accounts
OWNER to role_administrator;
-- Index: idx_accounts_username
-- DROP INDEX IF EXISTS public.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;
-- Revoke all permissions from existing roles
REVOKE ALL ON TABLE public.accounts FROM role_administrator, role_maintainer, role_support, services;
-- Grant appropriate permissions
GRANT ALL ON TABLE public.accounts TO role_administrator;
GRANT SELECT, INSERT, UPDATE ON TABLE public.accounts TO role_maintainer, services;
GRANT SELECT ON TABLE public.accounts TO role_support;

26
sql/003-account_audit.sql Normal file
View File

@@ -0,0 +1,26 @@
-- Table: public.accounts_audit
-- DROP TABLE IF EXISTS 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;
ALTER TABLE IF EXISTS public.accounts_audit
OWNER to role_administrator;
-- Revoke all permissions from existing roles
REVOKE ALL ON TABLE public.accounts_audit FROM role_administrator, role_maintainer, role_support, services;
-- Grant appropriate permissions to each role
GRANT ALL ON TABLE public.accounts_audit TO role_administrator;
GRANT SELECT, INSERT ON TABLE public.accounts_audit TO services;
GRANT SELECT ON TABLE public.accounts_audit TO role_maintainer, role_support;

View File

@@ -0,0 +1,29 @@
-- FUNCTION: public.accounts_audit()
-- DROP FUNCTION IF EXISTS 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$;
ALTER FUNCTION public.accounts_audit()
OWNER TO role_administrator;
-- Trigger: accounts_audit_trigger
-- DROP TRIGGER IF EXISTS accounts_audit_trigger ON public.accounts;
CREATE OR REPLACE TRIGGER accounts_audit_trigger
AFTER INSERT OR UPDATE
ON public.accounts
FOR EACH ROW
EXECUTE FUNCTION public.accounts_audit();

View File

@@ -0,0 +1,22 @@
package ltd.hlaeja.controller
import java.util.UUID
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.RestController
import reactor.core.publisher.Mono
@RestController
class AccountController(
private val accountService: AccountService,
) {
@GetMapping("/account-{uuid}")
fun getAccount(
@PathVariable uuid: UUID,
): Mono<Account.Response> = accountService.getUserById(uuid)
.map { it.toAccountResponse() }
}

View File

@@ -0,0 +1,18 @@
package ltd.hlaeja.entity
import java.time.ZonedDateTime
import java.util.UUID
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Table
@Table("accounts")
data class AccountEntity(
@Id
val id: UUID? = null,
val createdAt: ZonedDateTime,
val updatedAt: ZonedDateTime,
val enabled: Boolean,
val username: String,
val password: String,
val roles: String,
)

View File

@@ -0,0 +1,9 @@
package ltd.hlaeja.repository
import java.util.UUID
import ltd.hlaeja.entity.AccountEntity
import org.springframework.data.repository.reactive.ReactiveCrudRepository
import org.springframework.stereotype.Repository
@Repository
interface AccountRepository : ReactiveCrudRepository<AccountEntity, UUID>

View File

@@ -0,0 +1,24 @@
package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.UUID
import ltd.hlaeja.entity.AccountEntity
import ltd.hlaeja.repository.AccountRepository
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException
import reactor.core.publisher.Mono
private val log = KotlinLogging.logger {}
@Service
class AccountService(
private val accountRepository: AccountRepository,
) {
fun getUserById(
uuid: UUID,
): Mono<AccountEntity> = accountRepository.findById(uuid)
.doOnNext { log.debug { "Get account ${it.id}" } }
.switchIfEmpty(Mono.error(ResponseStatusException(HttpStatus.NOT_FOUND)))
}

View File

@@ -0,0 +1,15 @@
package ltd.hlaeja.util
import ltd.hlaeja.entity.AccountEntity
import ltd.hlaeja.library.accountRegistry.Account
import org.springframework.http.HttpStatus.EXPECTATION_FAILED
import org.springframework.web.server.ResponseStatusException
fun AccountEntity.toAccountResponse(): Account.Response = Account.Response(
id ?: throw ResponseStatusException(EXPECTATION_FAILED),
updatedAt,
enabled,
username,
roles.split(","),
)

View File

@@ -0,0 +1,67 @@
package ltd.hlaeja.service
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import java.time.ZonedDateTime
import java.util.UUID
import ltd.hlaeja.entity.AccountEntity
import ltd.hlaeja.repository.AccountRepository
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.web.server.ResponseStatusException
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
class AccountServiceTest {
companion object {
val account = UUID.fromString("00000000-0000-0000-0000-000000000002")
}
private lateinit var accountRepository: AccountRepository
private lateinit var accountService: AccountService
@BeforeEach
fun setup() {
accountRepository = mockk()
accountService = AccountService(accountRepository)
}
@Test
fun `get account by id - success`() {
// given
val accountEntity = AccountEntity(
account,
ZonedDateTime.now(),
ZonedDateTime.now(),
true,
"username",
"password",
"ROLE_TEST",
)
every { accountRepository.findById(any(UUID::class)) } returns Mono.just(accountEntity)
// when
StepVerifier.create(accountService.getUserById(account))
.expectNext(accountEntity)
.verifyComplete()
// then
verify { accountRepository.findById(any(UUID::class)) }
}
@Test
fun `get account by id - fail does not exist`() {
// given
every { accountRepository.findById(any(UUID::class)) } returns Mono.empty()
// when
StepVerifier.create(accountService.getUserById(account))
.expectError(ResponseStatusException::class.java)
.verify()
// then
verify { accountRepository.findById(any(UUID::class)) }
}
}

View File

@@ -0,0 +1,82 @@
package ltd.hlaeja.util
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.UUID
import kotlin.test.assertFailsWith
import ltd.hlaeja.entity.AccountEntity
import org.assertj.core.api.Assertions.assertThat
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.springframework.web.server.ResponseStatusException
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"))
}
@BeforeEach
fun setUp() {
mockkStatic(ZonedDateTime::class)
every { ZonedDateTime.now() } returns timestamp
}
@AfterEach
fun tearDown() {
unmockkStatic(ZonedDateTime::class)
}
@Nested
inner class AccountMapping {
@Test
fun `test toAccountResponse when id is not null`() {
// Arrange
val accountEntity = AccountEntity(
id = account,
createdAt = timestamp,
updatedAt = timestamp,
enabled = true,
username = "username",
password = "password",
roles = "ROLE_ADMIN,ROLE_USER",
)
// Act
val result = accountEntity.toAccountResponse()
// Assert
assertThat(result.id).isEqualTo(accountEntity.id)
assertThat(result.timestamp).isEqualTo(accountEntity.updatedAt)
assertThat(result.enabled).isEqualTo(accountEntity.enabled)
assertThat(result.username).isEqualTo(accountEntity.username)
assertThat(result.roles).contains("ROLE_ADMIN", "ROLE_USER")
}
@Test
fun `test toAccountResponse when id is null`() {
// Arrange
val accountEntity = AccountEntity(
id = null,
createdAt = timestamp,
updatedAt = timestamp,
enabled = true,
username = "username",
password = "password",
roles = "ROLE_ADMIN,ROLE_USER",
)
// Act and Assert
assertFailsWith<ResponseStatusException> {
accountEntity.toAccountResponse()
}
}
}
}