extract accounts and add integration test
- add end-to-end test AccountsEndpoint - extract get accounts from account.http to accounts.http - extract get accounts from AccountController to AccountsController - move spring boot test file to integration test - add schema.sql - add dependencies for integration test
This commit is contained in:
@@ -16,6 +16,7 @@ dependencies {
|
||||
implementation(hlaeja.springboot.starter.actuator)
|
||||
implementation(hlaeja.springboot.starter.r2dbc)
|
||||
implementation(hlaeja.springboot.starter.security)
|
||||
implementation(hlaeja.springboot.starter.validation)
|
||||
implementation(hlaeja.springboot.starter.webflux)
|
||||
|
||||
runtimeOnly(hlaeja.postgresql)
|
||||
@@ -29,6 +30,15 @@ dependencies {
|
||||
testImplementation(hlaeja.springboot.starter.test)
|
||||
|
||||
testRuntimeOnly(hlaeja.junit.platform.launcher)
|
||||
|
||||
integrationTestImplementation(hlaeja.assertj.core)
|
||||
integrationTestImplementation(hlaeja.library.hlaeja.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"
|
||||
|
||||
@@ -15,15 +15,6 @@ Content-Type: application/json
|
||||
]
|
||||
}
|
||||
|
||||
### 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-5
|
||||
|
||||
### update user all information
|
||||
PUT {{hostname}}/account-00000000-0000-7000-0000-000000000002
|
||||
Content-Type: application/json
|
||||
|
||||
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,118 @@
|
||||
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.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.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.test.web.reactive.server.WebTestClient
|
||||
import org.springframework.test.web.reactive.server.expectBody
|
||||
|
||||
@PostgresContainer
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT)
|
||||
@ExtendWith(SoftAssertionsExtension::class)
|
||||
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
|
||||
}
|
||||
}
|
||||
12
src/integration-test/resources/application.yml
Normal file
12
src/integration-test/resources/application.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
jwt:
|
||||
private-key: cert/valid-private-key.pem
|
||||
|
||||
spring:
|
||||
r2dbc:
|
||||
url: r2dbc:pool:postgresql://localhost:5432/test
|
||||
username: test
|
||||
password: test
|
||||
|
||||
test:
|
||||
postgres:
|
||||
init-script: postgres/schema.sql
|
||||
80
src/integration-test/resources/postgres/schema.sql
Normal file
80
src/integration-test/resources/postgres/schema.sql
Normal file
@@ -0,0 +1,80 @@
|
||||
-- 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');
|
||||
@@ -16,7 +16,6 @@ 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
|
||||
|
||||
@RestController
|
||||
@@ -24,10 +23,6 @@ class AccountController(
|
||||
private val accountService: AccountService,
|
||||
private val passwordEncoder: PasswordEncoder,
|
||||
) {
|
||||
companion object {
|
||||
const val DEFAULT_PAGE: Int = 1
|
||||
const val DEFAULT_SIZE: Int = 25
|
||||
}
|
||||
|
||||
@GetMapping("/account-{uuid}")
|
||||
fun getAccount(
|
||||
@@ -53,26 +48,6 @@ class AccountController(
|
||||
): Mono<Account.Response> = accountService.addAccount(request.toAccountEntity(passwordEncoder))
|
||||
.map { it.toAccountResponse() }
|
||||
|
||||
@GetMapping("/accounts")
|
||||
fun getDefaultAccounts(): Flux<Account.Response> = getAccounts(DEFAULT_PAGE, DEFAULT_SIZE)
|
||||
|
||||
@GetMapping("/accounts/page-{page}")
|
||||
fun getAccountsPage(
|
||||
@PathVariable page: Int,
|
||||
): Flux<Account.Response> = getAccounts(page, DEFAULT_SIZE)
|
||||
|
||||
@GetMapping("/accounts/page-{page}/show-{size}")
|
||||
fun getAccountsPageSize(
|
||||
@PathVariable page: Int,
|
||||
@PathVariable 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() }
|
||||
|
||||
private fun hasChange(
|
||||
user: AccountEntity,
|
||||
update: AccountEntity,
|
||||
|
||||
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() }
|
||||
}
|
||||
@@ -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