test(integration): audit log, outbox atomicity and transaction minor units tests
This commit is contained in:
parent
458217a0ec
commit
c7e09ed647
1 changed files with 118 additions and 0 deletions
118
src/test/audit-outbox.spec.ts
Normal file
118
src/test/audit-outbox.spec.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* Audit, Outbox and Transaction Integration Tests
|
||||
*
|
||||
* Verifies that:
|
||||
* 1. audit_log rows are created for mutations
|
||||
* 2. outbox_events rows are created atomically
|
||||
* 3. Transactions store amounts in minor units (no float drift)
|
||||
*
|
||||
* Runs against real DB via DATABASE_URL. Skip if not set.
|
||||
*/
|
||||
import { eq, desc } from 'drizzle-orm';
|
||||
import { db } from '../db/client';
|
||||
import { auditLog, outboxEvents, transactions } from '../db/schema';
|
||||
|
||||
const HAS_DB = !!process.env.DATABASE_URL;
|
||||
|
||||
const TENANT = '00000000-0000-0000-0000-000000000c01';
|
||||
const USER = '00000000-0000-0000-0000-000000000c02';
|
||||
const ORG = '00000000-0000-0000-0000-000000000c03';
|
||||
|
||||
(HAS_DB ? describe : describe.skip)('Audit Log', () => {
|
||||
it('audit record is written with correct tenant and actor', async () => {
|
||||
// Arrange
|
||||
const resource = `test:audit-${Date.now()}`;
|
||||
|
||||
// Act
|
||||
await db.insert(auditLog).values({
|
||||
tenantId: TENANT,
|
||||
actorId: USER,
|
||||
action: 'test.audit_integration',
|
||||
resource,
|
||||
});
|
||||
|
||||
// Assert
|
||||
const row = await db.query.auditLog.findFirst({
|
||||
where: eq(auditLog.resource, resource),
|
||||
});
|
||||
expect(row).toBeDefined();
|
||||
expect(row!.tenantId).toBe(TENANT);
|
||||
expect(row!.actorId).toBe(USER);
|
||||
expect(row!.action).toBe('test.audit_integration');
|
||||
|
||||
// Cleanup
|
||||
await db.delete(auditLog).where(eq(auditLog.resource, resource));
|
||||
});
|
||||
});
|
||||
|
||||
(HAS_DB ? describe : describe.skip)('Outbox Events', () => {
|
||||
it('outbox event is persisted with correct aggregate info', async () => {
|
||||
// Arrange
|
||||
const subjectId = '00000000-0000-0000-0000-aabbccddeeff';
|
||||
const correlationId = `test-${Date.now()}`;
|
||||
|
||||
// Act
|
||||
const [row] = await db
|
||||
.insert(outboxEvents)
|
||||
.values({
|
||||
tenantId: TENANT,
|
||||
actorId: USER,
|
||||
eventType: 'test.integration_event',
|
||||
aggregateType: 'test',
|
||||
subjectId,
|
||||
correlationId,
|
||||
payload: { test: true } as unknown,
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Assert
|
||||
expect(row.tenantId).toBe(TENANT);
|
||||
expect(row.eventType).toBe('test.integration_event');
|
||||
expect(row.processedAt).toBeNull();
|
||||
|
||||
// Cleanup
|
||||
await db.delete(outboxEvents).where(eq(outboxEvents.id, row.id));
|
||||
});
|
||||
});
|
||||
|
||||
(HAS_DB ? describe : describe.skip)('Transactions - minor units integrity', () => {
|
||||
let txId: string;
|
||||
|
||||
afterAll(async () => {
|
||||
if (txId) await db.delete(transactions).where(eq(transactions.id, txId));
|
||||
});
|
||||
|
||||
it('stores amount as integer string (no float drift)', async () => {
|
||||
// Arrange — 1234 EUR = 123400 eurocenti
|
||||
const amountCents = 123400;
|
||||
|
||||
// Act
|
||||
const [row] = await db
|
||||
.insert(transactions)
|
||||
.values({
|
||||
tenantId: TENANT,
|
||||
organizationId: ORG,
|
||||
type: 'invoice',
|
||||
amountMinorUnits: String(amountCents),
|
||||
currency: 'EUR',
|
||||
transactionDate: new Date('2026-07-31'),
|
||||
evidenceStatus: 'missing',
|
||||
})
|
||||
.returning();
|
||||
txId = row.id;
|
||||
|
||||
// Assert — no float conversion artifacts
|
||||
expect(row.amountMinorUnits).toBe('123400');
|
||||
expect(parseFloat(row.amountMinorUnits)).toBe(123400);
|
||||
expect(Number.isInteger(parseFloat(row.amountMinorUnits))).toBe(true);
|
||||
});
|
||||
|
||||
it('tenant isolation: other tenant cannot see this transaction', async () => {
|
||||
const OTHER_TENANT = '00000000-0000-0000-0000-000000000d99';
|
||||
const rows = await db.query.transactions.findMany({
|
||||
where: eq(transactions.tenantId, OTHER_TENANT),
|
||||
});
|
||||
const leaked = rows.filter((t) => t.id === txId);
|
||||
expect(leaked).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue