update project

This commit is contained in:
2025-07-28 22:28:24 +02:00
committed by swordsteel
parent 1ee306c151
commit 3849fa8676
17 changed files with 42 additions and 37 deletions

View File

@@ -0,0 +1,13 @@
package ltd.hlaeja
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class ApplicationTests {
@Test
fun contextLoads() {
// place holder
}
}

View File

@@ -0,0 +1,306 @@
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.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.http.HttpStatus.ACCEPTED
import org.springframework.http.HttpStatus.CONFLICT
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 AccountEndpoint {
@InjectSoftAssertions
lateinit var softly: SoftAssertions
@LocalServerPort
var port: Int = 0
lateinit var webClient: WebTestClient
@BeforeEach
fun setup() {
webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
@Nested
inner class GetAccount {
@Test
fun `get account with valid uuid`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000001")
// when
val result = webClient.get().uri("/account-$uuid").exchange()
// then
result.expectStatus().isOk()
.expectBody<Account.Response>()
.consumeWith {
softly.assertThat(it.responseBody?.id).isEqualTo(uuid)
softly.assertThat(it.responseBody?.username).isEqualTo("admin")
softly.assertThat(it.responseBody?.enabled).isTrue
softly.assertThat(it.responseBody?.roles?.size).isEqualTo(1)
softly.assertThat(it.responseBody?.roles?.get(0)).isEqualTo("ROLE_ADMIN")
}
}
@Test
fun `get account with invalid uuid`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000000")
// when
val result = webClient.get().uri("/account-$uuid").exchange()
// then
result.expectStatus().isNotFound
}
@Test
fun `get account with bad uuid`() {
// given
val uuidInvalid = "000000000001"
// when
val result = webClient.get().uri("/account-$uuidInvalid").exchange()
// then
result.expectStatus().isBadRequest
}
}
@Nested
inner class PutAccount {
@Test
fun `success account with all changes`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000003")
val request = Account.Request(
username = "usernameA",
password = "abc123",
enabled = true,
roles = listOf("ROLE_USER", "ROLE_TEST"),
)
// when
val result = webClient.put().uri("/account-$uuid").bodyValue(request).exchange()
// then
result.expectStatus().isOk()
.expectBody<Account.Response>()
.consumeWith {
softly.assertThat(it.responseBody?.id).isEqualTo(uuid)
softly.assertThat(it.responseBody?.username).isEqualTo("usernameA")
softly.assertThat(it.responseBody?.enabled).isTrue
softly.assertThat(it.responseBody?.roles?.size).isEqualTo(2)
softly.assertThat(it.responseBody?.roles).contains("ROLE_USER")
softly.assertThat(it.responseBody?.roles).contains("ROLE_TEST")
}
}
@Test
fun `success account with null password changes`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000003")
val request = Account.Request(
username = "usernameB",
password = null,
enabled = false,
roles = listOf("ROLE_TEST"),
)
// when
val result = webClient.put().uri("/account-$uuid").bodyValue(request).exchange()
// then
result.expectStatus().isOk()
.expectBody<Account.Response>()
.consumeWith {
softly.assertThat(it.responseBody?.id).isEqualTo(uuid)
softly.assertThat(it.responseBody?.username).isEqualTo("usernameB")
softly.assertThat(it.responseBody?.enabled).isFalse
softly.assertThat(it.responseBody?.roles?.size).isEqualTo(1)
softly.assertThat(it.responseBody?.roles).contains("ROLE_TEST")
}
}
@Test
fun `success account with no changes`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000002")
val request = Account.Request(
username = "user",
password = null,
enabled = true,
roles = listOf("ROLE_USER"),
)
// when
val result = webClient.put().uri("/account-$uuid").bodyValue(request).exchange()
// then
result.expectStatus().isEqualTo(ACCEPTED)
}
@Test
fun `failed username duplicate`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000002")
val request = Account.Request(
username = "admin",
password = null,
enabled = true,
roles = listOf("ROLE_USER"),
)
// when
val result = webClient.put().uri("/account-$uuid").bodyValue(request).exchange()
// then
result.expectStatus().isEqualTo(CONFLICT)
}
@Test
fun `failed account not found`() {
// given
val uuid = UUID.fromString("00000000-0000-7000-0000-000000000000")
val request = Account.Request(
username = "admin",
password = null,
enabled = true,
roles = listOf("ROLE_USER"),
)
// when
val result = webClient.put().uri("/account-$uuid").bodyValue(request).exchange()
// then
result.expectStatus().isNotFound
}
}
@Nested
inner class PostAccount {
@ParameterizedTest
@CsvSource(
"new-user, new-pass, true, 2, ROLE_USER;ROLE_TEST",
"admin-user, admin-pass, false, 1, ROLE_ADMIN",
"test-user, test-pass, true, 1, ROLE_USER",
)
fun `success added account`(
username: String,
password: String,
enabled: Boolean,
size: Int,
roleList: String,
) {
// given
val roles: List<String> = roleList.split(";")
val request = Account.Request(
username = username,
password = password,
enabled = enabled,
roles = roles,
)
// when
val result = webClient.post().uri("/account").bodyValue(request).exchange()
// then
result.expectStatus().isCreated
.expectBody<Account.Response>()
.consumeWith {
softly.assertThat(it.responseBody?.id?.version()).isEqualTo(7)
softly.assertThat(it.responseBody?.username).isEqualTo(username)
softly.assertThat(it.responseBody?.enabled).isEqualTo(enabled)
softly.assertThat(it.responseBody?.roles?.size).isEqualTo(size)
for (role in roles) {
softly.assertThat(it.responseBody?.roles).contains(role)
}
}
}
@ParameterizedTest
@CsvSource(
"'', new-pass, ROLE_TEST",
"new-user, '', ROLE_ADMIN",
"new-user, new-pass, ''",
)
fun `validation fail on empty values`(
username: String,
password: String,
roleList: String,
) {
// given
val request = Account.Request(
username = username,
password = password,
enabled = true,
roles = when {
roleList.isEmpty() -> emptyList()
else -> listOf(roleList)
},
)
// when
val result = webClient.post().uri("/account").bodyValue(request).exchange()
// then
result.expectStatus().isBadRequest
}
@Test
fun `fail username take`() {
// given
val request = Account.Request(
username = "user",
password = "new-pass",
enabled = true,
roles = listOf("ROLE_USER", "ROLE_TEST"),
)
// when
val result = webClient.post().uri("/account").bodyValue(request).exchange()
// then
result.expectStatus().isEqualTo(CONFLICT)
}
@Test
fun `fail password null`() {
// given
val request = Account.Request(
username = "user",
password = null,
enabled = true,
roles = listOf("ROLE_USER", "ROLE_TEST"),
)
// when
val result = webClient.post().uri("/account").bodyValue(request).exchange()
// then
result.expectStatus().isBadRequest
}
}
}

View File

@@ -0,0 +1,111 @@
package ltd.hlaeja.controller
import ltd.hlaeja.library.accountRegistry.Account
import ltd.hlaeja.test.container.PostgresContainer
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
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)
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
}
}

View File

@@ -0,0 +1,116 @@
package ltd.hlaeja.controller
import org.assertj.core.api.Assertions.assertThat
import ltd.hlaeja.library.accountRegistry.Authentication
import ltd.hlaeja.test.compareToFile
import ltd.hlaeja.test.container.PostgresContainer
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.Test
import org.junit.jupiter.api.extension.ExtendWith
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.http.HttpStatus
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 AuthenticationEndpoint {
@InjectSoftAssertions
lateinit var softly: SoftAssertions
@LocalServerPort
var port: Int = 0
lateinit var webClient: WebTestClient
@BeforeEach
fun setup() {
webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
@Test
fun `login as admin`() {
// given
val request = Authentication.Request(
username = "admin",
password = "pass",
)
// when
val result = webClient.post().uri("/authenticate").bodyValue(request).exchange()
// then
result.expectStatus().isOk()
.expectBody<Authentication.Response>()
.consumeWith { assertThat(it.responseBody?.token).compareToFile("authenticate/admin-token.data") }
}
@Test
fun `login as user`() {
// given
val request = Authentication.Request(
username = "user",
password = "pass",
)
// when
val result = webClient.post().uri("/authenticate").bodyValue(request).exchange()
// then
result.expectStatus().isOk()
.expectBody<Authentication.Response>()
.consumeWith { assertThat(it.responseBody?.token).compareToFile("authenticate/user-token.data") }
}
@Test
fun `login as disabled user`() {
// given
val request = Authentication.Request(
username = "disabled",
password = "pass",
)
// when
val result = webClient.post().uri("/authenticate").bodyValue(request).exchange()
// then
result.expectStatus().isEqualTo(HttpStatus.LOCKED)
}
@Test
fun `login as non-existent `() {
// given
val request = Authentication.Request(
username = "username",
password = "pass",
)
// when
val result = webClient.post().uri("/authenticate").bodyValue(request).exchange()
// then
result.expectStatus().isNotFound
}
@Test
fun `login as user bad password`() {
// given
val request = Authentication.Request(
username = "user",
password = "password",
)
// when
val result = webClient.post().uri("/authenticate").bodyValue(request).exchange()
// then
result.expectStatus().isUnauthorized
}
}

View File

@@ -0,0 +1,15 @@
jwt:
private-key: cert/valid-private-key.pem
spring:
r2dbc:
url: r2dbc:postgresql://placeholder
username: placeholder
password: placeholder
container:
postgres:
version: postgres:17
init: postgres/schema.sql
before: postgres/data.sql
after: postgres/reset.sql

View File

@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMSIsInVzZXJuYW1lIjoiYWRtaW4iLCJyb2xlIjoiUk9MRV9BRE1JTiJ9.D6pK86XPWcdu1imV_y_4nAM6R4WEZvJpQ7oGaPAYe0_rg3UWdmVMa8Iw7L21bRgFoyIa7FQBwb_0AXojFVdb2mdOVDeGOwxQZAx23dwqeicOGd8yUMnuBaRSnd7z4P65KPMbbf0NOTQtho0Iv5mBAwFMJoF67sw-yntfx3cD_bfrI-Rf4oZaZsVn38Y2HJBe2sO2QI4e5_7s82ikxac416OX7PcIEgaf3IeEK1fSzSjRG_dyBGT_Jq_vAzVURsSu4ep976kI-k5ZXNE9EMxKu1S-n5c5eiaqo96ObnaSl4eWFik5q8vLhNLYIYO-bQi1xlJKnStwZqtUwlR763Gd5w

View File

@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6IjAwMDAwMDAwLTAwMDAtNzAwMC0wMDAwLTAwMDAwMDAwMDAwMiIsInVzZXJuYW1lIjoidXNlciIsInJvbGUiOiJST0xFX1VTRVIifQ.GvZIq0VF9xB8UY3PUGdnc6JNeUXtv4LzHJ56hWSeqUS6BXH0M_QJ5Lu9ndh9_P85CECp3eKrW4fKymGYe-NUXCtrzhr9-SSZLF6D7GRzAJ4yZjVRCOa_dgqe1RGuIZyZpli36z4NPqeBFqtHJ3Cs5rAI-WdvxGfWPgtM2kzpSJ_0zFihp9mVcZBlWP57HlN7-oKzDJWVpO2E17fWZTy-y4pdrIUsff63c256Cy8NhiAgux9aqZTdzaqp9TsXw59bRsS5d0YH7-gJuBd4xctZwgy_41BOcRk2q-nLyLZgWJs1wmCa_zaW0Fj6fjAsYvpdPNegkpIqrHJcQpGd7nE0KQ

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj
MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu
NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ
qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg
p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR
ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi
VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV
laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8
sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H
mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY
dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw
ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ
DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T
N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t
0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv
t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU
AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk
48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL
DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK
xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA
mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh
2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz
et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr
VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD
TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc
dn/RsYEONbwQSjIfMPkvxF+8HQ==
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,5 @@
-- 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');

View File

@@ -0,0 +1,11 @@
-- Disable triggers on the tables
ALTER TABLE accounts DISABLE TRIGGER ALL;
ALTER TABLE accounts_audit DISABLE TRIGGER ALL;
-- Truncate tables
TRUNCATE TABLE accounts_audit CASCADE;
TRUNCATE TABLE accounts CASCADE;
-- Enable triggers on the account table
ALTER TABLE accounts ENABLE TRIGGER ALL;
ALTER TABLE accounts_audit ENABLE TRIGGER ALL;

View File

@@ -0,0 +1,74 @@
-- 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();