Skip to content

Commit 4f4f242

Browse files
committed
feat(archetype): Add example ITs for the nine new core audits
One <Audit>AuditIT template per new audit, under its family directory (catalog/jpa/runtime), following the existing pattern: extends AbstractDatabaseAuditIT (or the user's parentClass), @Autowired the assertion bean, and demonstrate the exclusion constant. UnusedIndexAuditIT is plan-based (PostgreSQL-only), so it joins Join/OrderBy/WhereClause in archetype-post-generate.groovy's deletion list for non-PostgreSQL engines. Fixing the demo schema to pass all three new catalog/JPA audits cleanly surfaced two real gaps: * Liquibase's own bookkeeping tables (databasechangelog, databasechangeloglock) are unmapped by design, and databasechangeloglock's PK is a narrow INT - both genuine findings PrimaryKeyTypeAuditIT and UnmappedDatabaseObjectAuditIT now exclude, the same way PrimaryKeyPresenceAuditAssertion already special-cases them for the presence audit. * The demo's Parent/Child entities had no @Version attribute. Rather than excluding them, added a genuine version column (Liquibase + entity + getter) to both, since a versioned entity is worth demonstrating rather than suppressing - Spring Data's save() handles it transparently, so RepositoryWorkloadIT needed no change. Full reactor `clean install` (which drives archetype:integration-test against Testcontainers PostgreSQL/MySQL) is green: 20/20 in the full and targeted-datasource projects, 16/16 in full-mysql.
1 parent 29807c9 commit 4f4f242

13 files changed

Lines changed: 441 additions & 5 deletions

archetype/src/main/resources/META-INF/archetype-post-generate.groovy

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,16 @@ if (parentClass != 'none') {
6464
new File(destRoot, "src/test/java/${packagePath}/AbstractDatabaseAuditIT.java").delete()
6565
}
6666

67-
// The plan-based runtime audits (Join/OrderBy/WhereClause index) are PostgreSQL-only, so remove their ITs for any
68-
// other engine. The catalog, JPA, and unconditional-mutation audits run on every engine; RepositoryWorkloadIT stays
69-
// too, because the unconditional-mutation audit throws on an empty capture, so its priming workload must still run.
67+
// The plan-based runtime audits (Join/OrderBy/Unused/WhereClause index) are PostgreSQL-only, so remove their ITs
68+
// for any other engine. The catalog, JPA, and capture-scan (offset-pagination/repeated-statement/
69+
// unconditional-mutation) audits run on every engine; RepositoryWorkloadIT stays too, because the
70+
// unconditional-mutation audit throws on an empty capture, so its priming workload must still run.
7071
def databasePlatform = request.properties.getProperty('databasePlatform', 'postgresql')
7172
if (databasePlatform != 'postgresql') {
7273
[
7374
"src/test/java/${packagePath}/runtime/JoinIndexAuditIT.java",
7475
"src/test/java/${packagePath}/runtime/OrderByIndexAuditIT.java",
76+
"src/test/java/${packagePath}/runtime/UnusedIndexAuditIT.java",
7577
"src/test/java/${packagePath}/runtime/WhereClauseIndexAuditIT.java"
7678
].each { path ->
7779
new File(destRoot, path).delete()

archetype/src/main/resources/archetype-resources/src/test/java/app/Child.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
import jakarta.persistence.JoinColumn;
1010
import jakarta.persistence.ManyToOne;
1111
import jakarta.persistence.Table;
12+
import jakarta.persistence.Version;
1213

1314
/**
1415
* Demo child entity whose {@code parent_id} foreign key is indexed, not null, and type-matched to
1516
* {@link Parent}'s primary key. Its {@code name} column is indexed so the example WHERE/ORDER BY audits see an
16-
* indexed access path.
17+
* indexed access path. Carries a {@code @Version} attribute so concurrent updates are detected rather than
18+
* silently overwritten.
1719
*/
1820
@Entity
1921
@Table(name = "child")
@@ -30,6 +32,10 @@ public class Child {
3032
@JoinColumn(name = "parent_id", nullable = false)
3133
private Parent parent;
3234

35+
@Version
36+
@Column(name = "version", nullable = false)
37+
private int version;
38+
3339
/**
3440
* Constructs an empty instance for JPA.
3541
*/
@@ -73,4 +79,13 @@ public String getName() {
7379
public Parent getParent() {
7480
return parent;
7581
}
82+
83+
/**
84+
* Returns the optimistic-locking version.
85+
*
86+
* @return the version.
87+
*/
88+
public int getVersion() {
89+
return version;
90+
}
7691
}

archetype/src/main/resources/archetype-resources/src/test/java/app/Parent.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import jakarta.persistence.GenerationType;
77
import jakarta.persistence.Id;
88
import jakarta.persistence.Table;
9+
import jakarta.persistence.Version;
910

1011
/**
11-
* Demo parent entity referenced by {@link Child} through an indexed, not-null, type-matched foreign key.
12+
* Demo parent entity referenced by {@link Child} through an indexed, not-null, type-matched foreign key. Carries
13+
* a {@code @Version} attribute so concurrent updates are detected rather than silently overwritten.
1214
*/
1315
@Entity
1416
@Table(name = "parent")
@@ -21,6 +23,10 @@ public class Parent {
2123
@Column(name = "code", nullable = false)
2224
private String code;
2325

26+
@Version
27+
@Column(name = "version", nullable = false)
28+
private int version;
29+
2430
/**
2531
* Constructs an empty instance for JPA.
2632
*/
@@ -53,4 +59,13 @@ public Long getId() {
5359
public String getCode() {
5460
return code;
5561
}
62+
63+
/**
64+
* Returns the optimistic-locking version.
65+
*
66+
* @return the version.
67+
*/
68+
public int getVersion() {
69+
return version;
70+
}
5671
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package ${package}.catalog;
2+
3+
import java.util.Set;
4+
5+
#if($disabledTests == 'true')
6+
import org.junit.jupiter.api.Disabled;
7+
#end
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.beans.factory.annotation.Value;
11+
12+
#if($parentClass && $parentClass != '' && $parentClass != 'none')
13+
#set($simpleParentClass = $parentClass.replaceAll('.*\.', ''))
14+
import ${parentClass};
15+
#else
16+
import ${package}.AbstractDatabaseAuditIT;
17+
#end
18+
import io.github.databaseaudits.spring.boot.assertion.DuplicateForeignKeyAuditAssertion;
19+
20+
/**
21+
* Asserts that no relationship in the schema is enforced by more than one foreign key constraint, with a place
22+
* to exclude a deliberate duplicate.
23+
*/
24+
#if($parentClass && $parentClass != '' && $parentClass != 'none')
25+
public class DuplicateForeignKeyAuditIT extends ${simpleParentClass} {
26+
#else
27+
public class DuplicateForeignKeyAuditIT extends AbstractDatabaseAuditIT {
28+
#end
29+
/** Exclude a deliberate duplicate constraint, e.g. Set.of("fk_orders_customer_legacy"). */
30+
private static final Set<String> EXCLUDED_CONSTRAINTS = Set.of();
31+
32+
@Autowired
33+
private DuplicateForeignKeyAuditAssertion duplicateForeignKeyAuditAssertion;
34+
35+
@Value("#[[${]]#${schemaPropertyName}#[[}]]#")
36+
private String schema;
37+
38+
@Test
39+
#if($disabledTests == 'true')
40+
@Disabled("Generated as disabled; remove @Disabled to enable")
41+
#end
42+
void testNoDuplicateForeignKeyConstraints() {
43+
duplicateForeignKeyAuditAssertion.assertClean(schema, EXCLUDED_CONSTRAINTS);
44+
}
45+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package ${package}.catalog;
2+
3+
import java.util.Set;
4+
5+
#if($disabledTests == 'true')
6+
import org.junit.jupiter.api.Disabled;
7+
#end
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.beans.factory.annotation.Value;
11+
12+
#if($parentClass && $parentClass != '' && $parentClass != 'none')
13+
#set($simpleParentClass = $parentClass.replaceAll('.*\.', ''))
14+
import ${parentClass};
15+
#else
16+
import ${package}.AbstractDatabaseAuditIT;
17+
#end
18+
import io.github.databaseaudits.spring.boot.assertion.PrimaryKeyTypeAuditAssertion;
19+
20+
/**
21+
* Asserts that every primary key in the schema is at least bigint wide, with a place to exclude a genuinely
22+
* bounded table.
23+
*/
24+
#if($parentClass && $parentClass != '' && $parentClass != 'none')
25+
public class PrimaryKeyTypeAuditIT extends ${simpleParentClass} {
26+
#else
27+
public class PrimaryKeyTypeAuditIT extends AbstractDatabaseAuditIT {
28+
#end
29+
/** Liquibase's own bookkeeping table declares a narrow INT primary key; every application table here is
30+
* genuinely BIGINT. Add your own genuinely-bounded tables the same way, e.g. "lookup_values.id". */
31+
private static final Set<String> EXCLUDED_COLUMNS = Set.of("databasechangeloglock.id");
32+
33+
@Autowired
34+
private PrimaryKeyTypeAuditAssertion primaryKeyTypeAuditAssertion;
35+
36+
@Value("#[[${]]#${schemaPropertyName}#[[}]]#")
37+
private String schema;
38+
39+
@Test
40+
#if($disabledTests == 'true')
41+
@Disabled("Generated as disabled; remove @Disabled to enable")
42+
#end
43+
void testEveryPrimaryKeyIsAtLeastBigint() {
44+
primaryKeyTypeAuditAssertion.assertClean(schema, EXCLUDED_COLUMNS);
45+
}
46+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package ${package}.catalog;
2+
3+
import java.util.Set;
4+
5+
#if($disabledTests == 'true')
6+
import org.junit.jupiter.api.Disabled;
7+
#end
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.beans.factory.annotation.Value;
11+
12+
#if($parentClass && $parentClass != '' && $parentClass != 'none')
13+
#set($simpleParentClass = $parentClass.replaceAll('.*\.', ''))
14+
import ${parentClass};
15+
#else
16+
import ${package}.AbstractDatabaseAuditIT;
17+
#end
18+
import io.github.databaseaudits.spring.boot.assertion.UniqueIndexNotNullAuditAssertion;
19+
20+
/**
21+
* Asserts that no UNIQUE index in the schema includes a nullable column, with a place to exclude a deliberate
22+
* partial-uniqueness design.
23+
*/
24+
#if($parentClass && $parentClass != '' && $parentClass != 'none')
25+
public class UniqueIndexNotNullAuditIT extends ${simpleParentClass} {
26+
#else
27+
public class UniqueIndexNotNullAuditIT extends AbstractDatabaseAuditIT {
28+
#end
29+
/** Exclude a deliberate partial-uniqueness index, e.g. Set.of("uq_orders_active_customer"). */
30+
private static final Set<String> EXCLUDED_INDEXES = Set.of();
31+
32+
@Autowired
33+
private UniqueIndexNotNullAuditAssertion uniqueIndexNotNullAuditAssertion;
34+
35+
@Value("#[[${]]#${schemaPropertyName}#[[}]]#")
36+
private String schema;
37+
38+
@Test
39+
#if($disabledTests == 'true')
40+
@Disabled("Generated as disabled; remove @Disabled to enable")
41+
#end
42+
void testNoUniqueIndexOverNullableColumn() {
43+
uniqueIndexNotNullAuditAssertion.assertClean(schema, EXCLUDED_INDEXES);
44+
}
45+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package ${package}.jpa;
2+
3+
import java.util.Set;
4+
5+
#if($disabledTests == 'true')
6+
import org.junit.jupiter.api.Disabled;
7+
#end
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
11+
#if($parentClass && $parentClass != '' && $parentClass != 'none')
12+
#set($simpleParentClass = $parentClass.replaceAll('.*\.', ''))
13+
import ${parentClass};
14+
#else
15+
import ${package}.AbstractDatabaseAuditIT;
16+
#end
17+
import io.github.databaseaudits.spring.boot.assertion.EagerCollectionFetchAuditAssertion;
18+
19+
/**
20+
* Asserts that no mapped collection is fetched eagerly, with a place to exclude a deliberately eager one.
21+
*/
22+
#if($parentClass && $parentClass != '' && $parentClass != 'none')
23+
public class EagerCollectionFetchAuditIT extends ${simpleParentClass} {
24+
#else
25+
public class EagerCollectionFetchAuditIT extends AbstractDatabaseAuditIT {
26+
#end
27+
/** Exclude a deliberately eager collection role, e.g. Set.of("com.example.demo.Order.items"). */
28+
private static final Set<String> EXCLUDED_ROLES = Set.of();
29+
30+
@Autowired
31+
private EagerCollectionFetchAuditAssertion eagerCollectionFetchAuditAssertion;
32+
33+
@Test
34+
#if($disabledTests == 'true')
35+
@Disabled("Generated as disabled; remove @Disabled to enable")
36+
#end
37+
void testNoCollectionIsFetchedEagerly() {
38+
eagerCollectionFetchAuditAssertion.assertClean(EXCLUDED_ROLES);
39+
}
40+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package ${package}.jpa;
2+
3+
import java.util.Set;
4+
5+
#if($disabledTests == 'true')
6+
import org.junit.jupiter.api.Disabled;
7+
#end
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
11+
#if($parentClass && $parentClass != '' && $parentClass != 'none')
12+
#set($simpleParentClass = $parentClass.replaceAll('.*\.', ''))
13+
import ${parentClass};
14+
#else
15+
import ${package}.AbstractDatabaseAuditIT;
16+
#end
17+
import io.github.databaseaudits.spring.boot.assertion.MissingVersionAttributeAuditAssertion;
18+
19+
/**
20+
* Asserts that every mutable root entity carries a {@code @Version} attribute, with a place to exclude a
21+
* genuinely append-only or single-writer entity.
22+
*/
23+
#if($parentClass && $parentClass != '' && $parentClass != 'none')
24+
public class MissingVersionAttributeAuditIT extends ${simpleParentClass} {
25+
#else
26+
public class MissingVersionAttributeAuditIT extends AbstractDatabaseAuditIT {
27+
#end
28+
/** Exclude an append-only or single-writer entity, e.g. Set.of("com.example.demo.AuditLogEntry"). */
29+
private static final Set<String> EXCLUDED_ENTITIES = Set.of();
30+
31+
@Autowired
32+
private MissingVersionAttributeAuditAssertion missingVersionAttributeAuditAssertion;
33+
34+
@Test
35+
#if($disabledTests == 'true')
36+
@Disabled("Generated as disabled; remove @Disabled to enable")
37+
#end
38+
void testEveryMutableEntityIsVersioned() {
39+
missingVersionAttributeAuditAssertion.assertClean(EXCLUDED_ENTITIES);
40+
}
41+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package ${package}.jpa;
2+
3+
import java.util.Set;
4+
5+
#if($disabledTests == 'true')
6+
import org.junit.jupiter.api.Disabled;
7+
#end
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
11+
#if($parentClass && $parentClass != '' && $parentClass != 'none')
12+
#set($simpleParentClass = $parentClass.replaceAll('.*\.', ''))
13+
import ${parentClass};
14+
#else
15+
import ${package}.AbstractDatabaseAuditIT;
16+
#end
17+
import io.github.databaseaudits.spring.boot.assertion.UnmappedDatabaseObjectAuditAssertion;
18+
19+
/**
20+
* Asserts that every physical base table and column in the live schema is mapped by a JPA entity, with a place
21+
* to exclude a known, acceptable unmapped relation (e.g. a migration-tool bookkeeping table).
22+
*/
23+
#if($parentClass && $parentClass != '' && $parentClass != 'none')
24+
public class UnmappedDatabaseObjectAuditIT extends ${simpleParentClass} {
25+
#else
26+
public class UnmappedDatabaseObjectAuditIT extends AbstractDatabaseAuditIT {
27+
#end
28+
/** Liquibase's own bookkeeping tables track migrations, not application data, so no entity should map them.
29+
* Add your own known, acceptable unmapped relations the same way. */
30+
private static final Set<String> EXCLUDED_RELATIONS =
31+
Set.of("databasechangelog", "databasechangeloglock");
32+
33+
@Autowired
34+
private UnmappedDatabaseObjectAuditAssertion unmappedDatabaseObjectAuditAssertion;
35+
36+
@Test
37+
#if($disabledTests == 'true')
38+
@Disabled("Generated as disabled; remove @Disabled to enable")
39+
#end
40+
void testEveryDatabaseObjectIsMapped() {
41+
unmappedDatabaseObjectAuditAssertion.assertClean(EXCLUDED_RELATIONS);
42+
}
43+
}

0 commit comments

Comments
 (0)