This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { createHmac } from 'node:crypto';
|
||||
import { describe, it } from 'node:test';
|
||||
import type { Database } from '@pig/db';
|
||||
import type { Principal } from '../src/lib/auth';
|
||||
import { AuthError } from '../src/lib/auth';
|
||||
import { executeMutation } from '../src/lib/mutation';
|
||||
import {
|
||||
createSlackLinkMutationDefinition,
|
||||
parseSlashRequirement,
|
||||
verifySlackRequest,
|
||||
} from '../src/routes/slack';
|
||||
import { NotificationDeliveryError } from '../src/services/notifier';
|
||||
import { SlackNotifier, slackClientMessageId } from '../src/services/slack';
|
||||
|
||||
const NOW = 1_786_579_200;
|
||||
const SECRET = 'test-signing-secret';
|
||||
|
||||
function signature(timestamp: number, body: string): string {
|
||||
return `v0=${createHmac('sha256', SECRET).update(`v0:${timestamp}:${body}`).digest('hex')}`;
|
||||
}
|
||||
|
||||
describe('Slack request verification', () => {
|
||||
it('accepts the exact signed bytes and rejects tampering', () => {
|
||||
const body = 'team_id=T1&channel_id=C1&text=8+H100_80GB';
|
||||
const signed = signature(NOW, body);
|
||||
assert.equal(verifySlackRequest(SECRET, String(NOW), signed, body, NOW), true);
|
||||
assert.equal(verifySlackRequest(SECRET, String(NOW), signed, `${body}+fabric`, NOW), false);
|
||||
});
|
||||
|
||||
it('rejects replayed requests outside Slack\'s five-minute window', () => {
|
||||
const body = 'team_id=T1&channel_id=C1&text=8+H100_80GB';
|
||||
const old = NOW - 301;
|
||||
assert.equal(verifySlackRequest(SECRET, String(old), signature(old, body), body, NOW), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Slack capacity command', () => {
|
||||
it('turns transport syntax into a CapacityService requirement without matching in the handler', () => {
|
||||
assert.deepEqual(
|
||||
parseSlashRequirement('8 H100_80GB hours=640 max=2.50 fabric tier=secure_cloud'),
|
||||
{
|
||||
gpuCount: 8,
|
||||
gpuType: 'H100_80GB',
|
||||
totalGpuHours: 640,
|
||||
maxPricePerGpuHourCents: 250,
|
||||
requiresHighSpeedInterconnect: true,
|
||||
minSecurityTier: 'secure_cloud',
|
||||
},
|
||||
);
|
||||
assert.equal(parseSlashRequirement('eight H100_80GB'), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Slack channel link authorization', () => {
|
||||
it('denies non-admins before reading link input or opening a transaction', async () => {
|
||||
const events: string[] = [];
|
||||
const principal: Principal = {
|
||||
userId: '00000000-0000-4000-8000-000000000001',
|
||||
email: 'seller@example.com',
|
||||
name: 'Seller',
|
||||
isPlatformAdmin: false,
|
||||
teams: [{ team: 'demand', role: 'admin' }],
|
||||
via: 'jwt',
|
||||
scopes: ['read', 'write'],
|
||||
};
|
||||
const db = {
|
||||
transaction: async () => {
|
||||
events.push('transaction');
|
||||
},
|
||||
} as unknown as Database;
|
||||
|
||||
await assert.rejects(
|
||||
executeMutation(
|
||||
db,
|
||||
principal,
|
||||
async () => {
|
||||
events.push('body');
|
||||
return {};
|
||||
},
|
||||
createSlackLinkMutationDefinition(),
|
||||
),
|
||||
(error: unknown) => error instanceof AuthError && error.code === 'insufficient_permission',
|
||||
);
|
||||
assert.deepEqual(events, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Slack delivery decisions', () => {
|
||||
it('reuses a deterministic client message id across retries', async () => {
|
||||
const bodies: Record<string, unknown>[] = [];
|
||||
const notifier = new SlackNotifier({
|
||||
botToken: 'xoxb-test',
|
||||
fetchImpl: async (_input, init) => {
|
||||
bodies.push(JSON.parse(String(init?.body)) as Record<string, unknown>);
|
||||
return Response.json({ ok: true, ts: '123.456' });
|
||||
},
|
||||
});
|
||||
const envelope = {
|
||||
idempotencyKey: 'slack:link:event',
|
||||
destination: 'C123',
|
||||
notification: {
|
||||
kind: 'stage_change' as const,
|
||||
accountId: '10000000-0000-4000-8000-000000000001',
|
||||
dealId: '20000000-0000-4000-8000-000000000002',
|
||||
dealSide: 'demand' as const,
|
||||
dealName: 'Reserved H100 cluster',
|
||||
fromStage: 'proposal',
|
||||
toStage: 'procurement',
|
||||
changedAt: '2026-08-12T12:00:00.000Z',
|
||||
},
|
||||
};
|
||||
await notifier.send(envelope);
|
||||
await notifier.send(envelope);
|
||||
assert.equal(bodies[0]?.client_msg_id, slackClientMessageId(envelope.idempotencyKey));
|
||||
assert.equal(bodies[1]?.client_msg_id, bodies[0]?.client_msg_id);
|
||||
});
|
||||
|
||||
it('marks rate limits retryable and honours Slack retry-after', async () => {
|
||||
const notifier = new SlackNotifier({
|
||||
botToken: 'xoxb-test',
|
||||
fetchImpl: async () =>
|
||||
new Response('', { status: 429, headers: { 'retry-after': '7' } }),
|
||||
});
|
||||
await assert.rejects(
|
||||
notifier.send({
|
||||
idempotencyKey: 'one',
|
||||
destination: 'C1',
|
||||
notification: {
|
||||
kind: 'idle_capacity',
|
||||
accountId: '10000000-0000-4000-8000-000000000001',
|
||||
commitmentId: '20000000-0000-4000-8000-000000000002',
|
||||
commitmentName: 'Eight H100s',
|
||||
gpuType: 'H100_80GB',
|
||||
idleGpuHours: 640,
|
||||
idleCostCents: 120_000,
|
||||
utilisation: 0,
|
||||
observedAt: '2026-08-12T12:00:00.000Z',
|
||||
},
|
||||
}),
|
||||
(error: unknown) =>
|
||||
error instanceof NotificationDeliveryError &&
|
||||
error.retryable &&
|
||||
error.retryAfterMs === 7_000,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user