Testing¶
Three kinds of test, from cheapest to most real: your declarations on their own, your Spring wiring without a database, and the whole thing against Postgres.
Your declarations, in plain Java¶
A charter, a MemoryStorage and an identity you control are all it takes. No Spring, no database,
no keys.
class InvoiceChecksTest {
private final MemoryStorage storage = new MemoryStorage();
private final Map<String, String> asking = new HashMap<>(Map.of("tenant", "acme"));
private final DefaultCharter charter = new DefaultCharter(Axes.of(TENANT, SENSITIVITY));
// Your own declarations -- the same methods your configuration calls, so the test exercises the
// real ceilings rather than copies of them.
private final Occlude<Invoice> invoices = InvoicePortals.source(charter);
private final Reveal<Invoice> billingUi = InvoicePortals.billingUi(charter);
{
charter.bind(Bindings.of(storage).withIdentity(() -> AccessContext.of(asking)));
}
@Test
void another_tenant_cannot_read_it() {
Occluded<Invoice> held = invoices.occlude(anInvoice());
asking.put("tenant", "globex");
assertThat(billingUi.reveal(held).succeeded()).isFalse();
}
}
InvoicePortals stands for wherever your application declares its portals: keep the declarations
in methods that take the Charter — a configuration class, or static methods like these — so a test
calls exactly what production does.
Refusals are results, so assert on them directly. reveal(...).succeeded(), derive(...).value(),
ask(...) returning Answer.Refused with a reason. Nothing needs to be caught, and the reason tells
you which rule said no.
The trail is there to read. MemoryStorage.audit() returns every line, and
audit(Operation.REVEAL) one operation's. A test that a refusal was recorded, not just returned,
is one line:
assertThat(storage.audit(AuditRecord.Operation.REVEAL))
.anySatisfy(line -> assertThat(line.outcome()).isEqualTo(AuditRecord.Outcome.REFUSED));
The manifest is a test too. A charter's findings are provable from its declarations alone, so assert there are none, or only the ones you have accepted:
assertThat(charter.manifest().findings()).isEmpty();
That catches a portal reading a type nothing produces — the usual shape of a rename that went half-applied — before anything runs.
Your Spring wiring, without a database¶
The starter will not build a JDBC store without keys and a root, and says so at startup. A test
that is about your beans rather than about storage does not need one: supply a MemoryStorage
under the store's name, and the JDBC store backs off entirely.
@TestConfiguration
class InMemoryStorage {
@Bean(name = CharterAutoConfiguration.STORAGE, defaultCandidate = false)
Storage storage() {
return new MemoryStorage();
}
}
For identity, contribute an AccessContextProvider a test can steer — a ThreadLocal it sets, or
the same request headers production reads. The starter binds with whichever AccessContextProvider
bean exists, and warns at startup only when there is none.
The whole thing, against Postgres¶
Run a real Postgres with Testcontainers, and give the store keys and a root generated for the run. Nothing about them needs to be stable across runs, and nothing should be committed:
@Testcontainers
@SpringBootTest
class BillingIT {
@Container
static final PostgreSQLContainer POSTGRES = new PostgreSQLContainer("postgres:17-alpine");
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
registry.add("spring.datasource.username", POSTGRES::getUsername);
registry.add("spring.datasource.password", POSTGRES::getPassword);
registry.add("occlude.keys.current", () -> "test");
registry.add("occlude.keys.keks.test", () -> random(32));
registry.add("occlude.roots.current", () -> "test");
registry.add("occlude.roots.secrets.test", () -> random(32));
}
private static String random(int bytes) {
byte[] secret = new byte[bytes];
new SecureRandom().nextBytes(secret);
return Base64.getEncoder().encodeToString(secret);
}
}
The example application does the same with a demo profile holding fixed secrets, which is fine
for a demo and exactly what a real application must never check in.
This is also the place to test operations code — MemoryStorage has no integrity() or trail(),
so checking and reading the record back need the real store. Take the StorageIntegrity bean and
assert that check().intact() holds after your test has run its traffic through.