Get Account
- add AccountController - add AccountEntity toAccountResponse in Mapping.kt - add AccountService - add AccountRepository - add AccountEntity
This commit is contained in:
@@ -10,6 +10,7 @@ dependencies {
|
|||||||
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.springboot.starter.actuator)
|
implementation(hlaeja.springboot.starter.actuator)
|
||||||
implementation(hlaeja.springboot.starter.r2dbc)
|
implementation(hlaeja.springboot.starter.r2dbc)
|
||||||
implementation(hlaeja.springboot.starter.webflux)
|
implementation(hlaeja.springboot.starter.webflux)
|
||||||
|
|||||||
2
http/account.http
Normal file
2
http/account.http
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
### get user by id
|
||||||
|
GET {{hostname}}/account-00000000-0000-7000-0000-000000000001
|
||||||
35
sql/002-account.sql
Normal file
35
sql/002-account.sql
Normal 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
26
sql/003-account_audit.sql
Normal 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;
|
||||||
29
sql/004-account_audit_function.sql
Normal file
29
sql/004-account_audit_function.sql
Normal 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();
|
||||||
22
src/main/kotlin/ltd/hlaeja/controller/AccountController.kt
Normal file
22
src/main/kotlin/ltd/hlaeja/controller/AccountController.kt
Normal 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() }
|
||||||
|
}
|
||||||
18
src/main/kotlin/ltd/hlaeja/entity/AccountEntity.kt
Normal file
18
src/main/kotlin/ltd/hlaeja/entity/AccountEntity.kt
Normal 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,
|
||||||
|
)
|
||||||
@@ -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>
|
||||||
24
src/main/kotlin/ltd/hlaeja/service/AccountService.kt
Normal file
24
src/main/kotlin/ltd/hlaeja/service/AccountService.kt
Normal 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)))
|
||||||
|
}
|
||||||
15
src/main/kotlin/ltd/hlaeja/util/Mapping.kt
Normal file
15
src/main/kotlin/ltd/hlaeja/util/Mapping.kt
Normal 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(","),
|
||||||
|
)
|
||||||
|
|
||||||
67
src/test/kotlin/ltd/hlaeja/service/AccountServiceTest.kt
Normal file
67
src/test/kotlin/ltd/hlaeja/service/AccountServiceTest.kt
Normal 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)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/test/kotlin/ltd/hlaeja/util/MappingKtTest.kt
Normal file
82
src/test/kotlin/ltd/hlaeja/util/MappingKtTest.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user