Files
buzz/beehive/test/credential-store.test.ts
2026-09-10 13:17:31 -04:00

51 lines
3.4 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, existsSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createCredential, readCredential, removeCredential, systemCredentials, type CredentialBackend } from '../src/credential-store.ts';
import { bootstrapHostIdentity, readHostIdentity } from '../src/host-identity.ts';
import { newKey } from '../src/protocol.ts';
import { memoryCredentials } from './credential-fixture.ts';
test('explicit isolated credential lifecycle; absence, locked/denied and bad roundtrip fail closed', () => {
const backend = memoryCredentials(), secret = newKey();
const reference = createCredential('agent', secret, backend);
assert.equal(readCredential(reference, backend), secret);
assert.throws(() => createCredential('agent', secret, backend), /exists/);
removeCredential(reference, backend);
assert.throws(() => readCredential(reference, backend), /missing or intentionally removed/);
for (const reason of ['locked', 'denied', 'unavailable']) {
const failed: CredentialBackend = { read() { throw Error(reason); }, create() { assert.fail('must not write after unavailable read'); }, remove() { assert.fail('must not delete after unavailable read'); } };
assert.throws(() => createCredential('host', secret, failed), new RegExp(reason));
assert.throws(() => removeCredential(reference, failed), new RegExp(reason));
}
const bad: CredentialBackend = { read: () => null, create() {}, remove() {} };
assert.throws(() => createCredential('host', secret, bad), /missing/);
// Never invoke the native backend in automated tests: it cannot suppress OS UI.
assert.equal(typeof systemCredentials.read, 'function');
const again = createCredential('agent', secret, backend);
const noDelete = { ...backend, remove() {} };
assert.throws(() => removeCredential(again, noDelete), /not verified/);
});
test('host state persists public references only; removed keys never regenerate; default does not touch OS or write plaintext', () => {
const root = mkdtempSync(join(tmpdir(), 'beehive-key-reference-'));
try {
const backend = memoryCredentials();
const owner = 'a'.repeat(64);
const unavailable: CredentialBackend = { read() { throw Error('OS credential store unavailable'); }, create() { assert.fail('no fallback'); }, remove() { assert.fail('no removal'); } };
assert.throws(() => bootstrapHostIdentity(join(root, 'production'), 'host', owner, 'wss://example.invalid', unavailable), /OS credential store unavailable/);
assert.equal(existsSync(join(root, 'production', 'host-identity.json')), false);
const directory = join(root, 'fixture');
const identity = bootstrapHostIdentity(directory, 'host', owner, 'wss://example.invalid', backend);
const content = readFileSync(join(directory, 'host-identity.json'), 'utf8');
assert.ok(!content.includes(identity.secret));
const reference = JSON.parse(content).key;
removeCredential(reference, backend);
assert.throws(() => readHostIdentity(directory, backend), /missing or intentionally removed/);
assert.throws(() => bootstrapHostIdentity(directory, 'host', owner, 'wss://example.invalid', backend), /already exists/);
assert.equal(readFileSync(join(directory, 'host-identity.json'), 'utf8'), content);
} finally { rmSync(root, { recursive: true, force: true }); }
});