/** * 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(); }); });