Files
TREK/server/tests/unit/db/repositories/addons-config-encryption.test.ts
T
Maurice b2f2aef016 test(server): seed the small specs through the repositories
The suites with one to three raw statements left now seed and read their
rows through the ORM factories instead of hand-written SQLite on the
better-sqlite3 handle. A legacy statement that serves as a parity oracle,
the schema introspection and the bare backup file keep their raw SQL and
say why with a test-sql-allow marker. Two database mocks still answered a
getPlaceWithTags the module no longer exports; that dead branch goes too,
and the budget seam suite stubs the reservations repository directly
instead of a fake prepare().
2026-10-08 18:46:13 +02:00

83 lines
3.8 KiB
TypeScript

/**
* AD36 (`admin.service.ts#updateAddon`, the LLM-parsing addon's encrypted
* API key) — Plan 3i Task 1's encryption-boundary proof. Deliberately does
* NOT mock `apiKeyCrypto` (unlike `admin.service.test.ts`, which stubs it
* to identity functions) — this file exercises the REAL
* `maybe_encrypt_api_key`/`decrypt_api_key` so the round trip is genuine,
* not a no-op through a mock. `ENCRYPTION_KEY` comes from
* `tests/global-setup.ts`.
*/
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { createSnapshotTestDb } from '../../../helpers/db-mock';
import { resetTestDb } from '../../../helpers/test-db';
import { createTestOrm, type TestOrm } from '../../../helpers/test-orm';
import { Addons } from '../../../../src/db/entities/Addons.entity';
import { deleteRows, insertRow } from '../../../helpers/factories/rows';
import type { AddonsRepository } from '../../../../src/db/repositories/Addons.repository';
import { prepareLlmAddonConfigForWrite } from '../../../../src/nest/llm-parse/llm-config';
import { decrypt_api_key } from '../../../../src/nest/common/crypto/apiKeyCrypto';
const testDb = createSnapshotTestDb();
let t: TestOrm;
let addons: AddonsRepository;
beforeAll(async () => {
t = await createTestOrm(testDb);
addons = t.repo(Addons);
});
beforeEach(async () => {
resetTestDb(testDb);
await deleteRows(t, Addons);
await insertRow(t, Addons, {
id: 'llm_parsing',
name: 'AI Parsing',
description: null,
type: 'global',
icon: 'Sparkles',
enabled: false,
sort_order: 0,
});
t.clear();
});
afterAll(async () => {
await t.close();
testDb.close();
});
describe('AddonsRepository.setConfig — AD36 encryption boundary', () => {
it('ADDONSREPO-010: the stored apiKey is never the plaintext, but decrypt_api_key always round-trips it back', async () => {
const plaintext = 'sk-live-abc123-a-real-looking-secret';
// Mirrors admin.service.ts#updateAddon's own call: the SERVICE encrypts
// before handing the repository a config object — the repository never
// calls maybe_encrypt_api_key/decrypt_api_key itself.
const configToStore = prepareLlmAddonConfigForWrite({ provider: 'openai', apiKey: plaintext }, undefined);
await addons.setConfig('llm_parsing', configToStore as { apiKey?: string; [k: string]: unknown });
const row = await addons.findById('llm_parsing');
const storedApiKey = (row?.config as { apiKey?: string } | null)?.apiKey;
expect(storedApiKey).toBeDefined();
// Never assert a literal ciphertext string (the IV is random on every
// encrypt call, so the exact bytes are non-deterministic) — only that it
// differs from the plaintext and that the real decrypt function recovers it.
expect(storedApiKey).not.toBe(plaintext);
expect(decrypt_api_key(storedApiKey)).toBe(plaintext);
});
it('ADDONSREPO-011: a second write of the SAME plaintext produces a different stored ciphertext (random IV), both still decrypting to the same plaintext', async () => {
const plaintext = 'sk-live-same-secret-both-times';
const first = prepareLlmAddonConfigForWrite({ apiKey: plaintext }, undefined);
await addons.setConfig('llm_parsing', first as { apiKey?: string; [k: string]: unknown });
const firstStored = ((await addons.findById('llm_parsing'))?.config as { apiKey?: string } | null)?.apiKey;
const second = prepareLlmAddonConfigForWrite({ apiKey: plaintext }, undefined);
await addons.setConfig('llm_parsing', second as { apiKey?: string; [k: string]: unknown });
const secondStored = ((await addons.findById('llm_parsing'))?.config as { apiKey?: string } | null)?.apiKey;
expect(firstStored).not.toBe(secondStored);
expect(decrypt_api_key(firstStored)).toBe(plaintext);
expect(decrypt_api_key(secondStored)).toBe(plaintext);
});
});