import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { closeDb } from '@/db/client'; import { getPrincipalByName, grantPrincipal, grantsAllow, isAuthorized, listPrincipals, renderAuthorizedKeys, revokePrincipal, validateGrant, validatePrincipalName, validatePublicKey, } from './api-access'; const VALID_KEY = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI test@laptop'; describe('grantsAllow (pure, deny-by-default)', () => { test('exact command:subcommand', () => { expect(grantsAllow(['module:deploy'], 'module', 'deploy')).toBe(true); expect(grantsAllow(['module:deploy'], 'module', 'remove')).toBe(false); }); test('command:* wildcard', () => { expect(grantsAllow(['service:*'], 'service', 'add')).toBe(true); expect(grantsAllow(['service:*'], 'service', 'remove')).toBe(true); expect(grantsAllow(['service:*'], 'module', 'deploy')).toBe(false); }); test('global *', () => { expect(grantsAllow(['*'], 'anything', 'goes')).toBe(true); }); test('bare command for a subcommand-less command', () => { expect(grantsAllow(['status'], 'status')).toBe(true); expect(grantsAllow(['status'], 'module', 'deploy')).toBe(false); }); test('empty grants deny everything', () => { expect(grantsAllow([], 'module', 'deploy')).toBe(false); }); }); describe('validators', () => { test('principal name must be kebab-case', () => { expect(() => validatePrincipalName('alice')).not.toThrow(); expect(() => validatePrincipalName('ci-deployer')).not.toThrow(); expect(() => validatePrincipalName('Alice')).toThrow(); expect(() => validatePrincipalName('a b')).toThrow(); expect(() => validatePrincipalName('under_score')).toThrow(); }); test('public key must look like an SSH key', () => { expect(() => validatePublicKey(VALID_KEY)).not.toThrow(); expect(() => validatePublicKey('ssh-ed25519 AAAAC3NzaC1lZDI1NTE5')).not.toThrow(); expect(() => validatePublicKey('not a key')).toThrow(); expect(() => validatePublicKey('ssh-ed25519')).toThrow(); expect(() => validatePublicKey('rsa-bogus AAAA')).toThrow(); }); test('grant syntax', () => { expect(() => validateGrant('module:deploy')).not.toThrow(); expect(() => validateGrant('service:*')).not.toThrow(); expect(() => validateGrant('*')).not.toThrow(); expect(() => validateGrant('status')).not.toThrow(); expect(() => validateGrant('Module:Deploy')).toThrow(); expect(() => validateGrant('a:b:c')).toThrow(); }); }); describe('api-access (DB-backed)', () => { let dir: string; beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'celilo-api-access-')); process.env.CELILO_DB_PATH = join(dir, 'test.db'); closeDb(); // drop any cached handle so getDb() reopens at the new path }); afterEach(() => { closeDb(); rmSync(dir, { recursive: true, force: true }); }); test('grant → list → getByName → isAuthorized → render → revoke', async () => { const { created } = await grantPrincipal({ name: 'alice', publicKey: VALID_KEY, grants: ['module:deploy', 'service:*'], }); expect(created).toBe(true); expect((await listPrincipals()).map((p) => p.name)).toEqual(['alice']); const alice = await getPrincipalByName('alice'); expect(alice?.grants).toEqual(['module:deploy', 'service:*']); expect(await isAuthorized('alice', 'module', 'deploy')).toBe(true); expect(await isAuthorized('alice', 'service', 'add')).toBe(true); expect(await isAuthorized('alice', 'secret', 'read')).toBe(false); expect(await isAuthorized('nobody', 'module', 'deploy')).toBe(false); const authKeys = await renderAuthorizedKeys(); expect(authKeys).toContain('command="celilo api-serve --principal=alice"'); expect(authKeys).toContain('no-pty,no-port-forwarding'); expect(authKeys).toContain(VALID_KEY); expect(await revokePrincipal('alice')).toBe(true); expect(await listPrincipals()).toHaveLength(0); expect(await revokePrincipal('alice')).toBe(false); expect(await renderAuthorizedKeys()).toBe(''); }); test('grant is an upsert (replaces key + grants)', async () => { await grantPrincipal({ name: 'bob', publicKey: VALID_KEY, grants: ['status'] }); const second = await grantPrincipal({ name: 'bob', publicKey: VALID_KEY, grants: ['module:deploy'], }); expect(second.created).toBe(false); expect((await getPrincipalByName('bob'))?.grants).toEqual(['module:deploy']); expect(await listPrincipals()).toHaveLength(1); }); test('invalid inputs are rejected before write', async () => { await expect( grantPrincipal({ name: 'Bad Name', publicKey: VALID_KEY, grants: ['status'] }), ).rejects.toThrow(); await expect( grantPrincipal({ name: 'good', publicKey: 'garbage', grants: ['status'] }), ).rejects.toThrow(); expect(await listPrincipals()).toHaveLength(0); }); });