This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { describe, it } from 'node:test';
|
||||
import { z } from 'zod';
|
||||
import type { Database } from '@pig/db';
|
||||
import type { Principal } from '../src/lib/auth';
|
||||
import { AuthError } from '../src/lib/auth';
|
||||
import { apiError, executeMutation, MutationError } from '../src/lib/mutation';
|
||||
|
||||
const principal: Principal = {
|
||||
userId: '00000000-0000-0000-0000-000000000001',
|
||||
email: 'seller@example.com',
|
||||
name: 'Seller',
|
||||
isPlatformAdmin: false,
|
||||
teams: [{ team: 'demand', role: 'member' }],
|
||||
via: 'jwt',
|
||||
scopes: ['read', 'write'],
|
||||
};
|
||||
|
||||
function fakeDatabase(events: string[], activityRows: unknown[]): Database {
|
||||
const tx = {
|
||||
insert: () => ({
|
||||
values: async (row: unknown) => {
|
||||
events.push('activity');
|
||||
activityRows.push(row);
|
||||
},
|
||||
}),
|
||||
};
|
||||
return {
|
||||
transaction: async (work: (transaction: unknown) => Promise<unknown>) => {
|
||||
events.push('transaction');
|
||||
return work(tx);
|
||||
},
|
||||
} as unknown as Database;
|
||||
}
|
||||
|
||||
describe('mutation convention', () => {
|
||||
it('checks capability before reading attacker-controlled input', async () => {
|
||||
const events: string[] = [];
|
||||
const forbidden = { ...principal, teams: [{ team: 'supply', role: 'admin' }] } as Principal;
|
||||
|
||||
await assert.rejects(
|
||||
executeMutation(fakeDatabase(events, []), forbidden, async () => {
|
||||
events.push('body');
|
||||
return {};
|
||||
}, {
|
||||
schema: z.object({ name: z.string() }),
|
||||
permission: { capability: 'deal:write', team: 'demand' },
|
||||
invalidMessage: 'Invalid deal.',
|
||||
async mutate() {
|
||||
events.push('mutate');
|
||||
return { data: {}, activity: { type: 'note', subject: 'Changed' } };
|
||||
},
|
||||
}),
|
||||
(error: unknown) => error instanceof AuthError && error.code === 'insufficient_permission',
|
||||
);
|
||||
assert.deepEqual(events, []);
|
||||
});
|
||||
|
||||
it('rejects invalid ontology input before opening a transaction', async () => {
|
||||
const events: string[] = [];
|
||||
const stages = ['qualification', 'legal'] as const;
|
||||
|
||||
await assert.rejects(
|
||||
executeMutation(fakeDatabase(events, []), principal, async () => ({ stage: 'invented' }), {
|
||||
schema: z.object({ stage: z.enum(stages) }),
|
||||
permission: { capability: 'deal:write', team: 'demand' },
|
||||
invalidMessage: 'Invalid transition.',
|
||||
async mutate() {
|
||||
events.push('mutate');
|
||||
return { data: {}, activity: { type: 'stage_change', subject: 'Changed' } };
|
||||
},
|
||||
}),
|
||||
(error: unknown) =>
|
||||
error instanceof MutationError &&
|
||||
error.code === 'invalid_request' &&
|
||||
apiError(error.code, error.message, error.issues).issues?.length === 1,
|
||||
);
|
||||
assert.deepEqual(events, []);
|
||||
});
|
||||
|
||||
it('writes mutation evidence in the same transaction with framework attribution', async () => {
|
||||
const events: string[] = [];
|
||||
const rows: unknown[] = [];
|
||||
const result = await executeMutation(
|
||||
fakeDatabase(events, rows),
|
||||
principal,
|
||||
async () => ({ stage: 'legal' }),
|
||||
{
|
||||
schema: z.object({ stage: z.literal('legal') }),
|
||||
permission: { capability: 'deal:write', team: 'demand' },
|
||||
invalidMessage: 'Invalid transition.',
|
||||
async mutate() {
|
||||
events.push('mutate');
|
||||
const data = { id: 'deal-1' };
|
||||
return {
|
||||
data,
|
||||
activity: {
|
||||
type: 'stage_change',
|
||||
subject: 'Moved to legal',
|
||||
demandDealId: data.id,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(result, { id: 'deal-1' });
|
||||
assert.deepEqual(events, ['transaction', 'mutate', 'activity']);
|
||||
assert.deepEqual(rows, [
|
||||
{
|
||||
type: 'stage_change',
|
||||
subject: 'Moved to legal',
|
||||
demandDealId: 'deal-1',
|
||||
actorUserId: principal.userId,
|
||||
actorAgent: null,
|
||||
source: 'manual',
|
||||
occurredAt: (rows[0] as { occurredAt: Date }).occurredAt,
|
||||
},
|
||||
]);
|
||||
assert.ok((rows[0] as { occurredAt: unknown }).occurredAt instanceof Date);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user