// Credentials git (P12) : round-trip SecretBox, résumé SANS secret, NULLification à la suppression. import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { openDb, type Db } from '../src/db/index.js'; import { SecretBox } from '../src/core/secret-box.js'; import { GitCredentialsManager } from '../src/core/git-credentials.js'; let db: Db; let mgr: GitCredentialsManager; const SECRET = 'pat-super-secret-1234'; beforeEach(() => { db = openDb(':memory:'); mgr = new GitCredentialsManager(db, new SecretBox(Buffer.alloc(32, 7))); }); afterEach(() => db.close()); describe('GitCredentialsManager', () => { it('crée un credential : secret chiffré en DB, résumé sans secret', () => { const cred = mgr.create({ label: 'gh', service: 'github', authType: 'pat', secret: SECRET }); expect(cred.hasSecret).toBe(true); expect(cred.secretLast4).toBe('1234'); // le résumé sérialisé ne contient JAMAIS le secret en clair. expect(JSON.stringify(cred)).not.toContain(SECRET); // en DB : valeur chiffrée (préfixe v1:), jamais le clair. const row = db.prepare('SELECT secret_encrypted FROM git_credentials WHERE id = ?').get(cred.id) as { secret_encrypted: string }; expect(row.secret_encrypted.startsWith('v1:')).toBe(true); expect(row.secret_encrypted).not.toContain(SECRET); }); it('getSecret déchiffre (round-trip)', () => { const cred = mgr.create({ label: 'gl', service: 'gitlab', authType: 'pat', secret: SECRET }); expect(mgr.getSecret(cred.id)).toBe(SECRET); }); it('exige un base_url pour Gitea', () => { expect(() => mgr.create({ label: 'x', service: 'gitea', authType: 'pat', secret: SECRET })).toThrow(); expect(mgr.create({ label: 'x', service: 'gitea', authType: 'pat', baseUrl: 'https://git.example.com', secret: SECRET }).baseUrl).toBe('https://git.example.com'); }); it('refuse ssh_key/oauth pour l’instant (P12a)', () => { expect(() => mgr.create({ label: 'x', service: 'github', authType: 'ssh_key', secret: SECRET })).toThrow(); expect(() => mgr.create({ label: 'x', service: 'github', authType: 'oauth', secret: SECRET })).toThrow(); }); it('remove() NULLifie repos.credential_id (pas de FK)', () => { const cred = mgr.create({ label: 'gh', service: 'github', authType: 'pat', secret: SECRET }); db.prepare( "INSERT INTO repos (id, path, label, post_create_hooks, pre_trust, created_at, hidden, credential_id) VALUES ('r1', '/tmp/r1', 'r1', '[]', 0, '2020-01-01T00:00:00Z', 0, ?)", ).run(cred.id); expect(mgr.remove(cred.id)).toBe(true); const repo = db.prepare("SELECT credential_id FROM repos WHERE id = 'r1'").get() as { credential_id: string | null }; expect(repo.credential_id).toBeNull(); }); });