feat(audit): add quarkus-audit-tools module
All checks were successful
Build / build (pull_request) Successful in 1m57s
All checks were successful
Build / build (pull_request) Successful in 1m57s
This commit is contained in:
parent
db0026b723
commit
f268c4a27a
18 changed files with 834 additions and 0 deletions
|
@ -0,0 +1,136 @@
|
|||
package ch.phoenix.oss.quarkus.commons.audit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import ch.phoenix.oss.quarkus.commons.tracing.TracingService;
|
||||
import io.quarkus.narayana.jta.QuarkusTransaction;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.quarkus.test.junit.mockito.InjectSpy;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.time.Instant;
|
||||
import org.hibernate.envers.AuditReaderFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@QuarkusTest
|
||||
class AuditedPanacheEntityBaseTest {
|
||||
|
||||
@Inject
|
||||
EntityManager entityManager;
|
||||
|
||||
@InjectSpy
|
||||
TracingService tracingService;
|
||||
|
||||
@SuppressWarnings("Convert2MethodRef")
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
QuarkusTransaction.requiringNew().run(() -> TestEntity2.deleteAll());
|
||||
}
|
||||
|
||||
@Test
|
||||
void persistAndUpdate() {
|
||||
when(tracingService.getRequestId()).thenReturn("00000000-0000-0000-0000-000000000000");
|
||||
when(tracingService.getSpanId()).thenReturn("0000000000000000");
|
||||
when(tracingService.getTraceId()).thenReturn("00000000000000000000000000000000");
|
||||
when(tracingService.getActor()).thenReturn("unknown");
|
||||
when(tracingService.getClientIp()).thenReturn("unknown");
|
||||
|
||||
var now = Instant.now();
|
||||
|
||||
QuarkusTransaction.requiringNew().run(() -> {
|
||||
var entity = new TestEntity2("something");
|
||||
entity.persist();
|
||||
});
|
||||
|
||||
QuarkusTransaction.requiringNew().run(() -> {
|
||||
var entity = TestEntity2.findBySomething("something");
|
||||
assertAll(
|
||||
() -> assertThat(entity.createdAt)
|
||||
.as("createdAt should be after or equal to expected value")
|
||||
.isAfterOrEqualTo(now),
|
||||
() -> assertThat(entity.lastUpdatedAt)
|
||||
.as("lastUpdatedAt should be equal to createdAt")
|
||||
.isEqualTo(entity.createdAt));
|
||||
|
||||
var auditReader = AuditReaderFactory.get(entityManager);
|
||||
|
||||
var revisions = auditReader.getRevisions(TestEntity2.class, entity.id);
|
||||
assertThat(revisions).hasSize(1);
|
||||
|
||||
var revInfo = entityManager.find(Revision.class, revisions.getFirst());
|
||||
assertThat(revInfo).isNotNull();
|
||||
|
||||
assertAll(
|
||||
() -> assertThat(revInfo.timestamp)
|
||||
.as("revision timestamp should be equal to entity's createdAt timestamp")
|
||||
.isEqualTo(entity.createdAt),
|
||||
() -> assertThat(revInfo.actor)
|
||||
.as("actor should match expected value")
|
||||
.isEqualTo("unknown"),
|
||||
() -> assertThat(revInfo.traceId)
|
||||
.as("traceId should match expected value")
|
||||
.isEqualTo("00000000000000000000000000000000"),
|
||||
() -> assertThat(revInfo.spanId)
|
||||
.as("spanId should match expected value")
|
||||
.isEqualTo("0000000000000000"),
|
||||
() -> assertThat(revInfo.requestId)
|
||||
.as("requestId should match expected value")
|
||||
.isEqualTo("00000000-0000-0000-0000-000000000000"),
|
||||
() -> assertThat(revInfo.clientIp)
|
||||
.as("clientIp should match expected value")
|
||||
.isEqualTo("unknown"),
|
||||
() -> assertThat(revInfo.hostName)
|
||||
.as("hostName should not be blank")
|
||||
.isNotBlank());
|
||||
});
|
||||
|
||||
QuarkusTransaction.requiringNew().run(() -> {
|
||||
var entity = TestEntity2.findBySomething("something");
|
||||
entity.something = "else";
|
||||
});
|
||||
|
||||
QuarkusTransaction.requiringNew().run(() -> {
|
||||
var entity = TestEntity2.findBySomething("else");
|
||||
assertAll(() -> assertThat(entity.createdAt)
|
||||
.as("createdAt should be before lastUpdatedAt")
|
||||
.isBefore(entity.lastUpdatedAt));
|
||||
|
||||
var auditReader = AuditReaderFactory.get(entityManager);
|
||||
|
||||
var revisions = auditReader.getRevisions(TestEntity2.class, entity.id);
|
||||
assertThat(revisions).hasSize(2);
|
||||
|
||||
Revision revInfo = Revision.findById(revisions.getLast());
|
||||
assertThat(revInfo).isNotNull();
|
||||
|
||||
assertAll(
|
||||
() -> assertThat(revInfo.timestamp)
|
||||
.as("revision timestamp should not be equal to entity's createdAt")
|
||||
.isNotEqualTo(entity.createdAt),
|
||||
() -> assertThat(revInfo.timestamp)
|
||||
.as("revision timestamp should be equal to entity's lastUpdatedAt")
|
||||
.isEqualTo(entity.lastUpdatedAt),
|
||||
() -> assertThat(revInfo.actor)
|
||||
.as("actor should match expected value")
|
||||
.isEqualTo("unknown"),
|
||||
() -> assertThat(revInfo.traceId)
|
||||
.as("traceId should match expected value")
|
||||
.isEqualTo("00000000000000000000000000000000"),
|
||||
() -> assertThat(revInfo.spanId)
|
||||
.as("spanId should match expected value")
|
||||
.isEqualTo("0000000000000000"),
|
||||
() -> assertThat(revInfo.requestId)
|
||||
.as("requestId should match expected value")
|
||||
.isEqualTo("00000000-0000-0000-0000-000000000000"),
|
||||
() -> assertThat(revInfo.clientIp)
|
||||
.as("clientIp should match expected value")
|
||||
.isEqualTo("unknown"),
|
||||
() -> assertThat(revInfo.hostName)
|
||||
.as("hostName should not be blank")
|
||||
.isNotBlank());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
package ch.phoenix.oss.quarkus.commons.audit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import ch.phoenix.oss.quarkus.commons.tracing.TracingService;
|
||||
import io.quarkus.narayana.jta.QuarkusTransaction;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.quarkus.test.junit.mockito.InjectSpy;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.time.Instant;
|
||||
import org.hibernate.envers.AuditReaderFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@QuarkusTest
|
||||
class AuditedPanacheEntityTest {
|
||||
|
||||
@Inject
|
||||
EntityManager entityManager;
|
||||
|
||||
@InjectSpy
|
||||
TracingService tracingService;
|
||||
|
||||
@SuppressWarnings("Convert2MethodRef")
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
QuarkusTransaction.requiringNew().run(() -> TestEntity.deleteAll());
|
||||
}
|
||||
|
||||
@Test
|
||||
void persistAndUpdate() {
|
||||
when(tracingService.getRequestId()).thenReturn("00000000-0000-0000-0000-000000000000");
|
||||
when(tracingService.getSpanId()).thenReturn("0000000000000000");
|
||||
when(tracingService.getTraceId()).thenReturn("00000000000000000000000000000000");
|
||||
when(tracingService.getActor()).thenReturn("unknown");
|
||||
when(tracingService.getClientIp()).thenReturn("unknown");
|
||||
|
||||
var now = Instant.now();
|
||||
|
||||
QuarkusTransaction.requiringNew().run(() -> {
|
||||
var entity = new TestEntity("something");
|
||||
entity.persist();
|
||||
});
|
||||
|
||||
QuarkusTransaction.requiringNew().run(() -> {
|
||||
var entity = TestEntity.findBySomething("something");
|
||||
assertAll(
|
||||
() -> assertThat(entity.createdAt)
|
||||
.as("createdAt should be after or equal to expected value")
|
||||
.isAfterOrEqualTo(now),
|
||||
() -> assertThat(entity.lastUpdatedAt)
|
||||
.as("lastUpdatedAt should be equal to createdAt")
|
||||
.isEqualTo(entity.createdAt));
|
||||
|
||||
var auditReader = AuditReaderFactory.get(entityManager);
|
||||
|
||||
var revisions = auditReader.getRevisions(TestEntity.class, entity.id);
|
||||
assertThat(revisions).hasSize(1);
|
||||
|
||||
var revInfo = entityManager.find(Revision.class, revisions.getFirst());
|
||||
assertThat(revInfo).isNotNull();
|
||||
|
||||
assertAll(
|
||||
() -> assertThat(revInfo.timestamp)
|
||||
.as("revision timestamp should be equal to entity's createdAt timestamp")
|
||||
.isEqualTo(entity.createdAt),
|
||||
() -> assertThat(revInfo.actor)
|
||||
.as("actor should match expected value")
|
||||
.isEqualTo("unknown"),
|
||||
() -> assertThat(revInfo.traceId)
|
||||
.as("traceId should match expected value")
|
||||
.isEqualTo("00000000000000000000000000000000"),
|
||||
() -> assertThat(revInfo.spanId)
|
||||
.as("spanId should match expected value")
|
||||
.isEqualTo("0000000000000000"),
|
||||
() -> assertThat(revInfo.requestId)
|
||||
.as("requestId should match expected value")
|
||||
.isEqualTo("00000000-0000-0000-0000-000000000000"),
|
||||
() -> assertThat(revInfo.clientIp)
|
||||
.as("clientIp should match expected value")
|
||||
.isEqualTo("unknown"),
|
||||
() -> assertThat(revInfo.hostName)
|
||||
.as("hostName should not be blank")
|
||||
.isNotBlank());
|
||||
});
|
||||
|
||||
QuarkusTransaction.requiringNew().run(() -> {
|
||||
var entity = TestEntity.findBySomething("something");
|
||||
entity.something = "else";
|
||||
});
|
||||
|
||||
QuarkusTransaction.requiringNew().run(() -> {
|
||||
var entity = TestEntity.findBySomething("else");
|
||||
assertAll(() -> assertThat(entity.createdAt)
|
||||
.as("createdAt should be before lastUpdatedAt")
|
||||
.isBefore(entity.lastUpdatedAt));
|
||||
|
||||
var auditReader = AuditReaderFactory.get(entityManager);
|
||||
|
||||
var revisions = auditReader.getRevisions(TestEntity.class, entity.id);
|
||||
assertThat(revisions).hasSize(2);
|
||||
|
||||
Revision revInfo = Revision.findById(revisions.getLast());
|
||||
assertThat(revInfo).isNotNull();
|
||||
|
||||
assertAll(
|
||||
() -> assertThat(revInfo.timestamp)
|
||||
.as("revision timestamp should not be equal to entity's createdAt")
|
||||
.isNotEqualTo(entity.createdAt),
|
||||
() -> assertThat(revInfo.timestamp)
|
||||
.as("revision timestamp should be equal to entity's lastUpdatedAt")
|
||||
.isEqualTo(entity.lastUpdatedAt),
|
||||
() -> assertThat(revInfo.actor)
|
||||
.as("actor should match expected value")
|
||||
.isEqualTo("unknown"),
|
||||
() -> assertThat(revInfo.traceId)
|
||||
.as("traceId should match expected value")
|
||||
.isEqualTo("00000000000000000000000000000000"),
|
||||
() -> assertThat(revInfo.spanId)
|
||||
.as("spanId should match expected value")
|
||||
.isEqualTo("0000000000000000"),
|
||||
() -> assertThat(revInfo.requestId)
|
||||
.as("requestId should match expected value")
|
||||
.isEqualTo("00000000-0000-0000-0000-000000000000"),
|
||||
() -> assertThat(revInfo.clientIp)
|
||||
.as("clientIp should match expected value")
|
||||
.isEqualTo("unknown"),
|
||||
() -> assertThat(revInfo.hostName)
|
||||
.as("hostName should not be blank")
|
||||
.isNotBlank());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package ch.phoenix.oss.quarkus.commons.audit;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import jakarta.inject.Inject;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@QuarkusTest
|
||||
class DefaultRevisionContextProviderTest {
|
||||
|
||||
@Inject
|
||||
DefaultRevisionContextProvider underTest;
|
||||
|
||||
@Test
|
||||
void getHostName() {
|
||||
assertThat(underTest.getHostName()).isNotBlank();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getHostNameWhenUnknown() {
|
||||
try (var inetMock = mockStatic(InetAddress.class)) {
|
||||
inetMock.when(InetAddress::getLocalHost).thenThrow(new UnknownHostException("simulated failure"));
|
||||
|
||||
assertThat(underTest.getHostName()).isEqualTo("unknown");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package ch.phoenix.oss.quarkus.commons.audit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@QuarkusTest
|
||||
class RevisionTest {
|
||||
|
||||
@Test
|
||||
void testEquals() {
|
||||
var r1 = new Revision();
|
||||
r1.rev = 1;
|
||||
|
||||
var r2 = new Revision();
|
||||
r2.rev = 1;
|
||||
|
||||
assertThat(r1).as("Revisions should be equal").isEqualTo(r2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHashCode() {}
|
||||
|
||||
@Test
|
||||
void testToString() {}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package ch.phoenix.oss.quarkus.commons.audit;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import org.hibernate.envers.Audited;
|
||||
|
||||
@Entity
|
||||
@Audited
|
||||
@Table(name = "test_entity")
|
||||
public class TestEntity extends AuditedPanacheEntity {
|
||||
|
||||
public String something;
|
||||
|
||||
public TestEntity() {}
|
||||
|
||||
public TestEntity(String something) {
|
||||
this.something = something;
|
||||
}
|
||||
|
||||
public static TestEntity findBySomething(String something) {
|
||||
return find("something", something).singleResult();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package ch.phoenix.oss.quarkus.commons.audit;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import org.hibernate.envers.Audited;
|
||||
|
||||
@Entity
|
||||
@Audited
|
||||
@Table(name = "test_entity")
|
||||
public class TestEntity2 extends AuditedPanacheEntityBase {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
public Long id;
|
||||
|
||||
public String something;
|
||||
|
||||
public TestEntity2() {}
|
||||
|
||||
public TestEntity2(String something) {
|
||||
this.something = something;
|
||||
}
|
||||
|
||||
public static TestEntity2 findBySomething(String something) {
|
||||
return find("something", something).singleResult();
|
||||
}
|
||||
}
|
22
quarkus-audit-tools/src/test/resources/application.yaml
Normal file
22
quarkus-audit-tools/src/test/resources/application.yaml
Normal file
|
@ -0,0 +1,22 @@
|
|||
quarkus:
|
||||
flyway:
|
||||
migrate-at-start: true
|
||||
datasource:
|
||||
db-kind: postgresql
|
||||
hibernate-orm:
|
||||
sql-load-script: no-file
|
||||
schema-management:
|
||||
strategy: none
|
||||
log:
|
||||
sql: true
|
||||
bind-parameters: true
|
||||
hibernate-envers:
|
||||
audit-strategy: org.hibernate.envers.strategy.internal.ValidityAuditStrategy
|
||||
revision-listener: ch.phoenix.oss.quarkus.commons.audit.AuditRevisionListener
|
||||
security:
|
||||
users:
|
||||
embedded:
|
||||
enabled: true
|
||||
plain-text: true
|
||||
users:
|
||||
jon: doe
|
|
@ -0,0 +1,46 @@
|
|||
create sequence revinfo_seq start with 1 increment by 50;
|
||||
create sequence test_entity_seq start with 1 increment by 50;
|
||||
|
||||
create table revinfo
|
||||
(
|
||||
rev bigint not null,
|
||||
timestamp timestamp(6) with time zone not null,
|
||||
actor varchar(255),
|
||||
spanId varchar(255),
|
||||
traceId varchar(255),
|
||||
requestId varchar(255),
|
||||
clientIp varchar(255),
|
||||
hostName varchar(255),
|
||||
primary key (rev)
|
||||
);
|
||||
|
||||
create table test_entity
|
||||
(
|
||||
|
||||
id bigint primary key not null,
|
||||
something varchar(255),
|
||||
createdAt timestamp(6) with time zone,
|
||||
lastUpdatedAt timestamp(6) with time zone
|
||||
);
|
||||
|
||||
create table test_entity_aud
|
||||
(
|
||||
revtype smallint,
|
||||
rev bigint not null,
|
||||
revend bigint,
|
||||
id bigint not null,
|
||||
something varchar(255),
|
||||
primary key (rev, id)
|
||||
);
|
||||
|
||||
alter table if exists test_entity_aud
|
||||
add constraint fk_rev__revinfo_rev
|
||||
foreign key (rev)
|
||||
references revinfo;
|
||||
|
||||
|
||||
alter table if exists test_entity_aud
|
||||
add constraint fk_revend__revinfo_rev
|
||||
foreign key (revend)
|
||||
references revinfo;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
CREATE OR REPLACE FUNCTION trg_test_entity_aud_apply_rev()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $func$
|
||||
DECLARE
|
||||
ts TIMESTAMP;
|
||||
BEGIN
|
||||
-- fetch the exact revision timestamp from revinfo
|
||||
SELECT r.timestamp
|
||||
INTO ts
|
||||
FROM revinfo r
|
||||
WHERE r.rev = NEW.rev;
|
||||
|
||||
-- only set created_at once, when still NULL
|
||||
UPDATE test_entity
|
||||
SET createdAt = ts
|
||||
WHERE id = NEW.id
|
||||
AND createdAt IS NULL;
|
||||
|
||||
-- always bump last_updated_at
|
||||
UPDATE test_entity
|
||||
SET lastUpdatedAt = ts
|
||||
WHERE id = NEW.id;
|
||||
|
||||
RETURN NULL;
|
||||
END;
|
||||
$func$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_test_entity_aud_after_insert
|
||||
ON test_entity_aud;
|
||||
|
||||
CREATE TRIGGER trg_test_entity_aud_after_insert
|
||||
AFTER INSERT ON test_entity_aud
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trg_test_entity_aud_apply_rev();
|
37
quarkus-audit-tools/src/test/resources/import.sql
Normal file
37
quarkus-audit-tools/src/test/resources/import.sql
Normal file
|
@ -0,0 +1,37 @@
|
|||
-- 1) Create or replace the trigger function
|
||||
CREATE OR REPLACE FUNCTION trg_test_entity_aud_apply_rev()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $func$
|
||||
DECLARE
|
||||
ts TIMESTAMP;
|
||||
BEGIN
|
||||
-- fetch the exact revision timestamp from revinfo
|
||||
SELECT r.timestamp
|
||||
INTO ts
|
||||
FROM revinfo r
|
||||
WHERE r.rev = NEW.rev;
|
||||
|
||||
-- only set created_at once, when still NULL
|
||||
UPDATE test_entity
|
||||
SET created_at = ts
|
||||
WHERE id = NEW.id
|
||||
AND created_at IS NULL;
|
||||
|
||||
-- always bump last_updated_at
|
||||
UPDATE test_entity
|
||||
SET last_updated_at = ts
|
||||
WHERE id = NEW.id;
|
||||
|
||||
RETURN NULL; -- AFTER trigger ignores return value
|
||||
END;
|
||||
$func$;
|
||||
|
||||
-- 2) Drop any existing trigger, then attach the new one
|
||||
DROP TRIGGER IF EXISTS trg_test_entity_aud_after_insert
|
||||
ON test_entity_aud;
|
||||
|
||||
CREATE TRIGGER trg_test_entity_aud_after_insert
|
||||
AFTER INSERT ON test_entity_aud
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trg_test_entity_aud_apply_rev();
|
Loading…
Add table
Add a link
Reference in a new issue