test(integration): audit log, outbox atomicity and transaction minor units tests

This commit is contained in:
admin-valentin 2026-07-31 14:40:30 +00:00
parent 458217a0ec
commit c7e09ed647

View 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);
});
});