Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions e2e/wallet-tests/secretRotation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { test, expect } from '@playwright/test';
import { SecretRotationService, SECRET_ROTATION_DEFAULT_POLICY } from '../../src/services/secretRotation.ts';
import type { RotationEvent, SecretKind } from '../../src/types/secrets.ts';

/**
* E2E tests for the Secret Rotation Service (Issue #119).
* Exercises rotation, zero-downtime overlap, expiry monitoring, and revocation
* inside a real browser context to validate production-equivalent behavior.
*/

function makeService(opts: { interval?: number; overlap?: number; now?: number } = {}) {
const t = opts.now ?? 1_000_000;
return new SecretRotationService({
clock: () => t,
policy: {
rotationIntervalMs: opts.interval ?? 1000,
overlapMs: opts.overlap ?? 500,
maxVersions: 3,
},
});
}

test.describe('Secret Rotation Service', () => {
test('registers and serves an active secret', async () => {
const svc = makeService();
const rec = await svc.register('db.main', 'database-credential', { initialValue: 'v1' });
expect(rec.status).toBe('active');
expect(await svc.getSecret('db.main')).toBe('v1');
});

test('rotates with zero-downtime overlap window', async () => {
const svc = makeService();
await svc.register('api.x', 'api-key', { initialValue: 'old' });
await svc.rotate('api.x', 'new');
expect(await svc.getSecret('api.x')).toBe('new');
expect(await svc.isValidSecret('api.x', 'old')).toBe(true);
expect(await svc.isValidSecret('api.x', 'new')).toBe(true);
});

test('emits lifecycle and monitoring events', async () => {
const svc = makeService();
const events: RotationEvent[] = [];
svc.on((e) => events.push(e));
await svc.register('tok', 'token', { initialValue: 'a' });
await svc.rotate('tok', 'b');
expect(events.some((e) => e.type === 'rotation:started')).toBe(true);
expect(events.some((e) => e.type === 'rotation:completed')).toBe(true);
});

test('emits expiry warning within overlap and marks expired after', async () => {
let t = 1_000_000;
const svc = new SecretRotationService({
clock: () => t,
policy: { rotationIntervalMs: 1000, overlapMs: 500, maxVersions: 3 },
});
const events: RotationEvent[] = [];
svc.on((e) => events.push(e));
await svc.register('tok', 'token', { initialValue: 'a' });
t += 800; // within overlap window -> expiring
await svc.evaluateExpiry();
expect(events.some((e) => e.type === 'secret:expiring')).toBe(true);
t += 2000; // well past expiry + overlap -> expired
await svc.evaluateExpiry();
expect(events.some((e) => e.type === 'secret:expired')).toBe(true);
expect(await svc.getSecret('tok')).toBeNull();
});

test('enforces maxVersions cap', async () => {
let t = 0;
const svc = new SecretRotationService({
clock: () => t,
policy: { rotationIntervalMs: 1000, overlapMs: 5000, maxVersions: 2 },
});
await svc.register('k', 'api-key', { initialValue: 'v0' });
t += 1000;
await svc.rotate('k');
t += 1000;
await svc.rotate('k');
const rec = await svc.register('k', 'api-key');
expect(rec.versions.length).toBeLessThanOrEqual(2);
});

test('revoke blocks all secret access', async () => {
const svc = makeService();
await svc.register('k', 'token', { initialValue: 'a' });
await svc.revoke('k');
expect(await svc.getSecret('k')).toBeNull();
expect(await svc.isValidSecret('k', 'a')).toBe(false);
});

test('default policy is securely configured', async () => {
expect(SECRET_ROTATION_DEFAULT_POLICY.rotationIntervalMs).toBeGreaterThan(0);
expect(SECRET_ROTATION_DEFAULT_POLICY.maxVersions).toBeGreaterThanOrEqual(1);
const kinds: SecretKind[] = ['database-credential', 'api-key', 'token', 'encryption-key'];
expect(kinds.length).toBe(4);
});
});
71 changes: 71 additions & 0 deletions src/__tests__/secretRotation.benchmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { SecretRotationService } from '../services/secretRotation.ts';
import type { SecretKind } from '../types/secrets.ts';

const SECRET_KINDS: SecretKind[] = [
'database-credential',
'api-key',
'token',
'encryption-key',
];

function generateSecrets(count: number): Array<{ key: string; kind: SecretKind }> {
const out: Array<{ key: string; kind: SecretKind }> = [];
for (let i = 0; i < count; i++) {
out.push({ key: `secret.${i}`, kind: SECRET_KINDS[i % SECRET_KINDS.length] });
}
return out;
}

export async function runBenchmark(): Promise<void> {
const ROUNDS = 2000;
const secrets = generateSecrets(50);
const svc = new SecretRotationService({
policy: { rotationIntervalMs: 86_400_000, maxVersions: 3, overlapMs: 3_600_000 },
});

for (const s of secrets) {
await svc.register(s.key, s.kind, { initialValue: `v0-${s.key}` });
}

const latencies: number[] = [];
for (let i = 0; i < ROUNDS; i++) {
const target = secrets[i % secrets.length];
const start = performance.now();
await svc.rotate(target.key, `v${i}-${target.key}`);
await svc.getSecret(target.key);
latencies.push(performance.now() - start);
}

latencies.sort((a, b) => a - b);
const p50 = latencies[Math.floor(latencies.length * 0.5)];
const p95 = latencies[Math.floor(latencies.length * 0.95)];
const p99 = latencies[Math.floor(latencies.length * 0.99)];
const max = latencies[latencies.length - 1];

console.log('=== SecretRotationService Benchmark ===');
console.log(`Rounds: ${ROUNDS}`);
console.log(`P50: ${p50.toFixed(4)}ms`);
console.log(`P95: ${p95.toFixed(4)}ms`);
console.log(`P99: ${p99.toFixed(4)}ms`);
console.log(`Max: ${max.toFixed(4)}ms`);

const passed = p99 < 100;
console.log(`\nP99 < 100ms target: ${passed ? 'PASS' : 'FAIL'} (${p99.toFixed(4)}ms)`);
console.log(`Throughput: ${(ROUNDS / (latencies.reduce((a, b) => a + b, 0) / 1000)).toFixed(0)} ops/sec`);

if (!passed) throw new Error(`P99 latency ${p99.toFixed(4)}ms exceeds 100ms target`);
}

if (
typeof window !== 'undefined' &&
typeof (globalThis as Record<string, unknown>).__BENCHMARK_RUN !== 'undefined'
) {
try {
runBenchmark();
console.log('SecretRotation benchmark completed successfully.');
} catch (err) {
console.error('SecretRotation benchmark failed:', err);
}
}

export { SecretRotationService };
Loading