maestro/src/vapid-store.test.ts
2026-06-03 05:08:00 +00:00

124 lines
4.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { VapidKeyStore } from './vapid-store.js';
describe('VapidKeyStore', () => {
let tempDir = '';
const SUBJECT = 'https://aao.example/';
beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), 'maestro-vapid-'));
});
afterEach(() => {
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true });
tempDir = '';
}
});
function makeStore() {
return new VapidKeyStore(join(tempDir, 'vapid.json'), join(tempDir, 'vapid-history'));
}
it('generates a new key file on first load', () => {
const store = makeStore();
const k = store.loadOrGenerate(SUBJECT);
expect(k.keyId).toMatch(/^v1-\d{4}-\d{2}-\d{2}-[a-z0-9]+$/);
expect(k.publicKey).toBeTruthy();
expect(k.privateKey).toBeTruthy();
expect(k.subject).toBe(SUBJECT);
expect(existsSync(join(tempDir, 'vapid.json'))).toBe(true);
});
it('writes the key file with mode 0600', () => {
const store = makeStore();
store.loadOrGenerate(SUBJECT);
const mode = statSync(join(tempDir, 'vapid.json')).mode & 0o777;
expect(mode).toBe(0o600);
});
it('returns the same key on repeated load (idempotent)', () => {
const store1 = makeStore();
const a = store1.loadOrGenerate(SUBJECT);
const store2 = makeStore();
const b = store2.loadOrGenerate(SUBJECT);
expect(b.keyId).toBe(a.keyId);
expect(b.publicKey).toBe(a.publicKey);
expect(b.privateKey).toBe(a.privateKey);
});
it('getKey returns current key by id', () => {
const store = makeStore();
const k = store.loadOrGenerate(SUBJECT);
expect(store.getKey(k.keyId)).toEqual(k);
});
it('getKey returns null for unknown keyId', () => {
const store = makeStore();
store.loadOrGenerate(SUBJECT);
expect(store.getKey('nonexistent-key-id')).toBeNull();
});
it('rotate moves the old key to history and creates a new current', () => {
const store = makeStore();
const oldKey = store.loadOrGenerate(SUBJECT);
const newKey = store.rotate(SUBJECT);
expect(newKey.keyId).not.toBe(oldKey.keyId);
expect(newKey.publicKey).not.toBe(oldKey.publicKey);
expect(existsSync(join(tempDir, 'vapid.json'))).toBe(true);
expect(existsSync(join(tempDir, 'vapid-history', `${oldKey.keyId}.json`))).toBe(true);
});
it('rotate: old key is still retrievable via getKey from history', () => {
const store = makeStore();
const oldKey = store.loadOrGenerate(SUBJECT);
store.rotate(SUBJECT);
const recovered = store.getKey(oldKey.keyId);
expect(recovered).not.toBeNull();
expect(recovered?.publicKey).toBe(oldKey.publicKey);
expect(recovered?.privateKey).toBe(oldKey.privateKey);
});
it('history file load works across process restart (fresh store instance)', () => {
const s1 = makeStore();
const oldKey = s1.loadOrGenerate(SUBJECT);
s1.rotate(SUBJECT);
// Simulate restart: a fresh VapidKeyStore reads the same on-disk state.
const s2 = makeStore();
s2.loadOrGenerate(SUBJECT);
const recovered = s2.getKey(oldKey.keyId);
expect(recovered?.publicKey).toBe(oldKey.publicKey);
});
it('throws on malformed key file', () => {
writeFileSync(join(tempDir, 'vapid.json'), '{"keyId":"only-id"}', { mode: 0o600 });
const store = makeStore();
expect(() => store.loadOrGenerate(SUBJECT)).toThrow(/malformed/);
});
it('getCurrent throws if loadOrGenerate was never called', () => {
const store = makeStore();
expect(() => store.getCurrent()).toThrow(/loadOrGenerate/);
});
it('keyId reflects the date prefix', () => {
const store = makeStore();
const k = store.loadOrGenerate(SUBJECT);
const today = new Date().toISOString().slice(0, 10);
expect(k.keyId).toContain(today);
});
it('readKeyFile preserves the original key material verbatim', () => {
const store = makeStore();
const k = store.loadOrGenerate(SUBJECT);
const raw = JSON.parse(readFileSync(join(tempDir, 'vapid.json'), 'utf-8'));
expect(raw.publicKey).toBe(k.publicKey);
expect(raw.privateKey).toBe(k.privateKey);
expect(raw.subject).toBe(k.subject);
});
});