gstack/ios-qa/daemon/test/allowlist.test.ts

147 lines
5.7 KiB
TypeScript

// Allowlist tests — codex flagged identity canonicalization gaps.
import { describe, test, expect, beforeEach } from 'bun:test';
import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import {
loadAllowlist,
findEntry,
hasCapability,
grantIdentity,
revokeIdentity,
saveAllowlist,
} from '../src/allowlist';
let tmpDir: string;
let listPath: string;
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), 'ios-qa-allowlist-'));
listPath = join(tmpDir, 'allowlist.json');
});
describe('Allowlist', () => {
test('loadAllowlist returns empty on missing file', async () => {
const list = await loadAllowlist(listPath);
expect(list).toEqual({ version: 1, entries: [] });
});
test('saveAllowlist writes mode 0600 JSON', async () => {
await saveAllowlist({
version: 1,
entries: [{ identity: 'user@example.com', capabilities: ['observe'], expires_at: null }],
}, listPath);
expect(existsSync(listPath)).toBe(true);
const raw = readFileSync(listPath, 'utf-8');
expect(JSON.parse(raw).entries[0].identity).toBe('user@example.com');
});
test('findEntry matches exact identity', async () => {
const list = {
version: 1 as const,
entries: [{ identity: 'user@example.com', capabilities: ['mutate' as const], expires_at: null }],
};
expect(findEntry(list, 'user@example.com')?.identity).toBe('user@example.com');
expect(findEntry(list, 'USER@example.com')).toBeNull(); // exact-match only
expect(findEntry(list, 'unknown@example.com')).toBeNull();
});
test('findEntry skips expired entries', async () => {
const list = {
version: 1 as const,
entries: [
{ identity: 'expired', capabilities: ['observe' as const], expires_at: new Date(Date.now() - 60_000).toISOString() },
],
};
expect(findEntry(list, 'expired')).toBeNull();
});
test('findEntry accepts future expiry', async () => {
const list = {
version: 1 as const,
entries: [
{ identity: 'future', capabilities: ['observe' as const], expires_at: new Date(Date.now() + 60_000).toISOString() },
],
};
expect(findEntry(list, 'future')?.identity).toBe('future');
});
test('hasCapability is tier-aware', async () => {
const list = {
version: 1 as const,
entries: [
{ identity: 'restore-user', capabilities: ['restore' as const], expires_at: null },
{ identity: 'observe-user', capabilities: ['observe' as const], expires_at: null },
],
};
expect(hasCapability(list, 'restore-user', 'observe')).toBe(true);
expect(hasCapability(list, 'restore-user', 'interact')).toBe(true);
expect(hasCapability(list, 'restore-user', 'mutate')).toBe(true);
expect(hasCapability(list, 'restore-user', 'restore')).toBe(true);
expect(hasCapability(list, 'observe-user', 'observe')).toBe(true);
expect(hasCapability(list, 'observe-user', 'interact')).toBe(false);
expect(hasCapability(list, 'observe-user', 'mutate')).toBe(false);
expect(hasCapability(list, 'observe-user', 'restore')).toBe(false);
});
test('grantIdentity adds a new entry', async () => {
await grantIdentity({
identity: 'new@example.com',
capability: 'interact',
path: listPath,
});
const list = await loadAllowlist(listPath);
expect(list.entries).toHaveLength(1);
expect(list.entries[0]!.identity).toBe('new@example.com');
expect(list.entries[0]!.capabilities).toContain('interact');
});
test('grantIdentity upgrades an existing entry', async () => {
await grantIdentity({ identity: 'u', capability: 'observe', path: listPath });
await grantIdentity({ identity: 'u', capability: 'restore', path: listPath });
const list = await loadAllowlist(listPath);
expect(list.entries).toHaveLength(1);
expect(list.entries[0]!.capabilities).toContain('restore');
});
test('grantIdentity with ttl sets expires_at', async () => {
await grantIdentity({ identity: 'u', capability: 'observe', ttlSeconds: 3600, path: listPath });
const list = await loadAllowlist(listPath);
const exp = Date.parse(list.entries[0]!.expires_at!);
expect(exp).toBeGreaterThan(Date.now());
expect(exp).toBeLessThan(Date.now() + 3700 * 1000);
});
test('revokeIdentity removes the entry', async () => {
await grantIdentity({ identity: 'u', capability: 'observe', path: listPath });
await revokeIdentity('u', listPath);
const list = await loadAllowlist(listPath);
expect(list.entries).toHaveLength(0);
});
// Codex-flagged identity canonicalization variants — verify the matcher
// works for each.
test('user identity, tagged node, node key, expired node all canonicalize distinctly', async () => {
const list = {
version: 1 as const,
entries: [
{ identity: 'alice@example.com', capabilities: ['observe' as const], expires_at: null },
{ identity: 'tag:ci', capabilities: ['mutate' as const], expires_at: null },
{ identity: 'node:abcdef0123', capabilities: ['observe' as const], expires_at: null },
{ identity: 'bob@example.com', capabilities: ['observe' as const], expires_at: new Date(Date.now() - 1000).toISOString() },
],
};
expect(hasCapability(list, 'alice@example.com', 'observe')).toBe(true);
expect(hasCapability(list, 'tag:ci', 'mutate')).toBe(true);
expect(hasCapability(list, 'node:abcdef0123', 'observe')).toBe(true);
expect(hasCapability(list, 'bob@example.com', 'observe')).toBe(false); // expired
expect(hasCapability(list, 'tag:CI', 'mutate')).toBe(false); // case-sensitive — canonicalize before lookup
});
});
import { afterEach } from 'bun:test';
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});