111 lines
4.1 KiB
TypeScript
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');
|
|
});
|
|
});
|