maestro/src/db/repository-local-auth.test.ts
oss-sync 2ec9853655
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (88dd58b)
2026-06-09 10:50:27 +00:00

127 lines
6.0 KiB
TypeScript

/**
* Local-auth repository layer: password hashing, local account creation,
* the shared `local` system admin, and the user-deletion guards.
*
* See docs/superpowers/plans/2026-06-09-local-auth.md.
*/
import { afterEach, describe, it, expect, beforeEach } from 'vitest';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { Repository } from './repository.js';
import { runMigrations } from './migrate.js';
describe('Repository local-auth', () => {
let tempDir = '';
let repo: Repository;
beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), 'maestro-localauth-'));
repo = new Repository(join(tempDir, 'orchestrator.db'));
runMigrations(repo.getDb());
});
afterEach(() => {
repo.close();
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true });
tempDir = '';
}
});
// ── password hashing ──────────────────────────────────────────
it('setLocalPassword + verifyLocalPassword round-trips', () => {
const u = repo.createUser({ email: 'a@x.com', name: 'A', role: 'user', status: 'active' });
repo.setLocalPassword(u.id, 'correct horse battery staple');
expect(repo.verifyLocalPassword(u.id, 'correct horse battery staple')).toBe(true);
expect(repo.verifyLocalPassword(u.id, 'wrong')).toBe(false);
});
it('verifyLocalPassword returns false for a user with no credential', () => {
const u = repo.createUser({ email: 'b@x.com', name: 'B', role: 'user', status: 'active' });
expect(repo.verifyLocalPassword(u.id, 'anything')).toBe(false);
});
it('setLocalPassword is idempotent-overwrite (re-set changes the password)', () => {
const u = repo.createUser({ email: 'c@x.com', name: 'C', role: 'user', status: 'active' });
repo.setLocalPassword(u.id, 'first');
repo.setLocalPassword(u.id, 'second');
expect(repo.verifyLocalPassword(u.id, 'first')).toBe(false);
expect(repo.verifyLocalPassword(u.id, 'second')).toBe(true);
});
it('stores a per-user random salt (two users, same password → different hash)', () => {
const u1 = repo.createUser({ email: 'd@x.com', name: 'D', role: 'user', status: 'active' });
const u2 = repo.createUser({ email: 'e@x.com', name: 'E', role: 'user', status: 'active' });
repo.setLocalPassword(u1.id, 'same');
repo.setLocalPassword(u2.id, 'same');
const db = repo.getDb();
const r1 = db.prepare('SELECT password_hash, salt FROM local_credentials WHERE user_id=?').get(u1.id) as { password_hash: string; salt: string };
const r2 = db.prepare('SELECT password_hash, salt FROM local_credentials WHERE user_id=?').get(u2.id) as { password_hash: string; salt: string };
expect(r1.salt).not.toBe(r2.salt);
expect(r1.password_hash).not.toBe(r2.password_hash);
});
// ── createLocalUser ───────────────────────────────────────────
it('createLocalUser creates a pending user with a local identity + password', () => {
const u = repo.createLocalUser({ email: 'new@x.com', password: 'pw12345', role: 'user', status: 'pending' });
expect(u.email).toBe('new@x.com');
expect(u.status).toBe('pending');
expect(repo.verifyLocalPassword(u.id, 'pw12345')).toBe(true);
const idn = repo.getDb().prepare("SELECT provider, provider_id FROM oauth_accounts WHERE user_id=? AND provider='local'").get(u.id) as { provider: string; provider_id: string };
expect(idn.provider).toBe('local');
expect(idn.provider_id).toBe('new@x.com');
});
it('createLocalUser REJECTS an email that already exists (no account takeover)', () => {
repo.createUser({ email: 'taken@x.com', name: 'T', role: 'user', status: 'active' });
expect(() => repo.createLocalUser({ email: 'taken@x.com', password: 'pw', role: 'user', status: 'pending' }))
.toThrow(/exist/i);
});
// ── upsertLocalSystemAdmin (shared 'local' identity) ──────────
it('upsertLocalSystemAdmin creates the shared id=local admin (active)', () => {
const u = repo.upsertLocalSystemAdmin({ email: 'admin@x.com', password: 'adminpw' });
expect(u.id).toBe('local');
expect(u.role).toBe('admin');
expect(u.status).toBe('active');
expect(repo.verifyLocalPassword('local', 'adminpw')).toBe(true);
});
it('upsertLocalSystemAdmin is idempotent and keeps id=local across re-seeds', () => {
repo.upsertLocalSystemAdmin({ email: 'admin@x.com', password: 'pw1' });
const again = repo.upsertLocalSystemAdmin({ email: 'admin@x.com', password: 'pw2' });
expect(again.id).toBe('local');
// password updated on re-seed
expect(repo.verifyLocalPassword('local', 'pw2')).toBe(true);
// exactly one users row with id=local
const cnt = repo.getDb().prepare("SELECT COUNT(*) c FROM users WHERE id='local'").get() as { c: number };
expect(cnt.c).toBe(1);
});
it('local-owned data survives because the admin IS the local owner (id=local)', () => {
// Simulate no-auth data owned by 'local', then seed the admin onto it.
repo.upsertLocalSystemAdmin({ email: 'admin@x.com', password: 'pw' });
expect(repo.getUserById('local')?.role).toBe('admin');
});
// ── deleteUser guards ─────────────────────────────────────────
it('deleteUser REFUSES to delete the local/system user', () => {
repo.upsertLocalSystemAdmin({ email: 'admin@x.com', password: 'pw' });
expect(() => repo.deleteUser('local')).toThrow(/local|system/i);
expect(repo.getUserById('local')).not.toBeNull();
});
it('deleteUser cascades local_credentials (FK ON DELETE CASCADE)', () => {
const u = repo.createLocalUser({ email: 'z@x.com', password: 'pw', role: 'user', status: 'active' });
expect(repo.verifyLocalPassword(u.id, 'pw')).toBe(true);
repo.deleteUser(u.id);
const row = repo.getDb().prepare('SELECT 1 FROM local_credentials WHERE user_id=?').get(u.id);
expect(row).toBeUndefined();
});
});