From c76bbf279195cc967627dcf06ed5668bf183963c Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 20 Jun 2026 21:51:05 +0200 Subject: [PATCH] test(acl)(#28): integration cases for windowed + stacked usage caps 9 cases: under/at limit, signings outside the window excluded, uncapped, lifetime (null window) all-time count, kind-specific counting, both stacked-cap directions (hourly binds vs daily binds), and the record->count->deny loop via recordSigning. 22 integration + 7 unit green. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/acl.integration.test.ts | 120 +++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/tests/acl.integration.test.ts b/tests/acl.integration.test.ts index d81418f..6907f70 100644 --- a/tests/acl.integration.test.ts +++ b/tests/acl.integration.test.ts @@ -13,7 +13,7 @@ import fs from 'node:fs'; import path from 'node:path'; import prisma from '../src/db'; -import { checkIfPubkeyAllowed } from '../src/daemon/lib/acl/index'; +import { checkIfPubkeyAllowed, recordSigning } from '../src/daemon/lib/acl/index'; const KEY = 'test-key'; const PUB = 'client-pubkey-1'; @@ -74,6 +74,38 @@ async function seedCondition( }); } +type RuleSeed = { method: string; kind: string | null; maxUsageCount?: number | null; windowSeconds?: number | null }; + +async function seedPolicyRules(rules: RuleSeed[]): Promise { + const policy = await prisma.policy.create({ + data: { + name: 'p', + rules: { + create: rules.map((r) => ({ + method: r.method, + kind: r.kind ?? undefined, + maxUsageCount: r.maxUsageCount ?? null, + windowSeconds: r.windowSeconds ?? null, + })), + }, + }, + }); + return policy.id; +} + +async function seedSigningLog( + keyUserId: number, + method: string, + kind: string | null, + count: number, + ageMs = 0, +): Promise { + const createdAt = new Date(Date.now() - ageMs); + for (let i = 0; i < count; i++) { + await prisma.signingLog.create({ data: { keyUserId, method, kind, createdAt } }); + } +} + // ---- lifecycle ------------------------------------------------------------- before(() => { @@ -95,6 +127,7 @@ before(() => { beforeEach(async () => { // FK-safe truncation order. + await prisma.signingLog.deleteMany(); await prisma.signingCondition.deleteMany(); await prisma.request.deleteMany(); await prisma.token.deleteMany(); @@ -193,3 +226,88 @@ test('policy rule kind mismatch -> undefined', async () => { test('no KeyUser -> undefined', async () => { assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), undefined); }); + +// ---- usage caps (#28) ------------------------------------------------------ + +test('usage cap: under the limit -> allowed', async () => { + const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: 3, windowSeconds: 3600 }]); + const ku = await seedKeyUser(); + await seedToken(ku, pid, {}); + await seedSigningLog(ku, 'sign_event', '1', 2); // 2 < 3 + assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), true); +}); + +test('usage cap: at the limit -> denied', async () => { + const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: 3, windowSeconds: 3600 }]); + const ku = await seedKeyUser(); + await seedToken(ku, pid, {}); + await seedSigningLog(ku, 'sign_event', '1', 3); // 3 >= 3 + assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), undefined); +}); + +test('usage cap: signings outside the window do not count', async () => { + const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: 2, windowSeconds: 3600 }]); + const ku = await seedKeyUser(); + await seedToken(ku, pid, {}); + await seedSigningLog(ku, 'sign_event', '1', 5, 2 * 3600 * 1000); // 5, aged 2h (outside 1h) + await seedSigningLog(ku, 'sign_event', '1', 1); // 1 recent + assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), true); // only 1 in window +}); + +test('uncapped rule -> allowed regardless of log volume', async () => { + const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: null }]); + const ku = await seedKeyUser(); + await seedToken(ku, pid, {}); + await seedSigningLog(ku, 'sign_event', '1', 100); + assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), true); +}); + +test('lifetime cap (windowSeconds null) counts all-time', async () => { + const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: 2, windowSeconds: null }]); + const ku = await seedKeyUser(); + await seedToken(ku, pid, {}); + await seedSigningLog(ku, 'sign_event', '1', 2, 10 * 24 * 3600 * 1000); // 2, aged 10 days + assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), undefined); // lifetime: 2 >= 2 +}); + +test('kind-specific cap counts only that kind', async () => { + const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: 2, windowSeconds: 3600 }]); + const ku = await seedKeyUser(); + await seedToken(ku, pid, {}); + await seedSigningLog(ku, 'sign_event', '2', 5); // kind 2 — irrelevant to a kind-1 cap + await seedSigningLog(ku, 'sign_event', '1', 1); + assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), true); // kind-1 count = 1 < 2 +}); + +test('stacked caps: the hourly cap binds even when the daily is fine', async () => { + const pid = await seedPolicyRules([ + { method: 'sign_event', kind: '1', maxUsageCount: 20, windowSeconds: 3600 }, + { method: 'sign_event', kind: '1', maxUsageCount: 200, windowSeconds: 86400 }, + ]); + const ku = await seedKeyUser(); + await seedToken(ku, pid, {}); + await seedSigningLog(ku, 'sign_event', '1', 20); // hits the 20/hr cap; well under 200/day + assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), undefined); +}); + +test('stacked caps: the daily cap binds even when the hourly is fine', async () => { + const pid = await seedPolicyRules([ + { method: 'sign_event', kind: '1', maxUsageCount: 20, windowSeconds: 3600 }, + { method: 'sign_event', kind: '1', maxUsageCount: 200, windowSeconds: 86400 }, + ]); + const ku = await seedKeyUser(); + await seedToken(ku, pid, {}); + await seedSigningLog(ku, 'sign_event', '1', 15); // hourly: 15 < 20 OK + await seedSigningLog(ku, 'sign_event', '1', 190, 2 * 3600 * 1000); // +190 aged 2h: in day, not hour + // hourly = 15 (OK), daily = 205 >= 200 -> denied + assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), undefined); +}); + +test('recordSigning feeds the cap: record then re-check denies at the limit', async () => { + const pid = await seedPolicyRules([{ method: 'sign_event', kind: '1', maxUsageCount: 1, windowSeconds: 3600 }]); + const ku = await seedKeyUser(); + await seedToken(ku, pid, {}); + assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), true); // 0 < 1 + await recordSigning(KEY, PUB, 'sign_event', signEvt); // now 1 recorded + assert.equal(await checkIfPubkeyAllowed(KEY, PUB, 'sign_event', signEvt), undefined); // 1 >= 1 +});