import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, rmSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import type { BrowserSessionRepo } from '../db/browser-session-repo.js'; import { initMasterKey, generateUserDek, encryptUserDek, encryptStateBlob, } from '../crypto/sessions.js'; import { loadSessionStateForUser } from './session-loader.js'; const OWNER = 'user-1'; const STATE = { cookies: [{ name: 'sid', value: 'abc' }], origins: [] }; let dir: string; let masterKeyPath: string; beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'session-loader-test-')); masterKeyPath = join(dir, 'master.key'); }); afterEach(() => { rmSync(dir, { recursive: true, force: true }); }); /** Build a stub repo + a valid encrypted blob for OWNER. */ function makeFixture(overrides: { profile?: Record | null; encDek?: Buffer | null; } = {}) { const master = initMasterKey(masterKeyPath); const dek = generateUserDek(); const encDek = overrides.encDek !== undefined ? overrides.encDek : encryptUserDek(master, dek); const blob = encryptStateBlob(dek, JSON.stringify(STATE)); const profile = overrides.profile !== undefined ? overrides.profile : { id: 7, ownerId: OWNER, status: 'active', encryptedStateBlob: blob }; const sessRepo = { getProfileById: (id: number, ownerId: string) => profile && id === 7 && ownerId === OWNER ? profile : null, getUserDek: (userId: string) => (userId === OWNER ? encDek : null), } as unknown as BrowserSessionRepo; return { sessRepo, blob }; } describe('loadSessionStateForUser', () => { it('decrypts and parses the storageState for an active profile', async () => { const { sessRepo } = makeFixture(); const res = await loadSessionStateForUser({ sessRepo, masterKeyPath }, OWNER, 7); expect(res).toEqual({ ok: true, storageState: STATE }); }); it('reports profile_not_found for an unknown id', async () => { const { sessRepo } = makeFixture(); const res = await loadSessionStateForUser({ sessRepo, masterKeyPath }, OWNER, 999); expect(res.ok).toBe(false); if (!res.ok) expect(res.error.kind).toBe('profile_not_found'); }); it('reports profile_not_found when the profile belongs to another user', async () => { const { sessRepo } = makeFixture(); const res = await loadSessionStateForUser({ sessRepo, masterKeyPath }, 'other-user', 7); expect(res.ok).toBe(false); if (!res.ok) expect(res.error.kind).toBe('profile_not_found'); }); it('reports profile_not_active for a non-active status', async () => { const base = makeFixture(); const { sessRepo } = makeFixture({ profile: { id: 7, ownerId: OWNER, status: 'pending', encryptedStateBlob: base.blob }, }); const res = await loadSessionStateForUser({ sessRepo, masterKeyPath }, OWNER, 7); expect(res.ok).toBe(false); if (!res.ok) { expect(res.error.kind).toBe('profile_not_active'); expect(res.error.message).toContain('status=pending'); } }); it('reports profile_not_active when the blob is missing', async () => { const { sessRepo } = makeFixture({ profile: { id: 7, ownerId: OWNER, status: 'active', encryptedStateBlob: null }, }); const res = await loadSessionStateForUser({ sessRepo, masterKeyPath }, OWNER, 7); expect(res.ok).toBe(false); if (!res.ok) expect(res.error.kind).toBe('profile_not_active'); }); it('reports dek_not_found when the user has no stored DEK', async () => { const { sessRepo } = makeFixture({ encDek: null }); const res = await loadSessionStateForUser({ sessRepo, masterKeyPath }, OWNER, 7); expect(res.ok).toBe(false); if (!res.ok) expect(res.error.kind).toBe('dek_not_found'); }); it('reports decrypt_error for a corrupted blob and never throws', async () => { const { sessRepo } = makeFixture({ profile: { id: 7, ownerId: OWNER, status: 'active', encryptedStateBlob: Buffer.from('garbage') }, }); const res = await loadSessionStateForUser({ sessRepo, masterKeyPath }, OWNER, 7); expect(res.ok).toBe(false); if (!res.ok) expect(res.error.kind).toBe('decrypt_error'); }); });