maestro/src/user-folder/session-loader.test.ts
oss-sync 5502478636
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (5b6df2f)
2026-06-10 08:40:41 +00:00

111 lines
4.1 KiB
TypeScript

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<string, unknown> | 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');
});
});