Files
TREK/server/tests/unit/nest/airtrail.service.test.ts
T
Maurice 724c88c201 style: format server and shared with prettier
A mechanical prettier --write over server/src, server/tests, server/scripts
and shared/src with the workspaces' .prettierrc; nothing but layout and
import order changes, and every file keeps the same set of imports. shared
is formatted throughout and its format baseline is empty. The server
baseline keeps the 35 files that Prettier would grow past their lint:size
limit (they need a split first, which is not a formatting change), and the
generated entity files stay as their generator writes them.
2026-10-10 21:23:37 +02:00

92 lines
4.0 KiB
TypeScript

/**
* `AirtrailService` — 3h L3 carry (`phase3h-reports/task-7-review.md`'s L3:
* "`users.airtrail_api_key` has no at-rest pin"). The credential lives in a
* plain `users` column, not a dedicated secrets table, so this is the
* service-level pin `dawarich.service.test.ts`'s DAWARICH-SVC-050 already
* gives the Dawarich connection: save through the real service, read the raw
* column, assert it is `enc:v1:`-prefixed ciphertext (never the plaintext),
* and assert `getAirtrailCredentials` round-trips it back to the original
* value. The database is real in-memory SQLite (`createSnapshotTestDb()`) —
* a stubbed repository would only prove some string was handed along, and
* "the key is never in the column as typed" is the one thing worth pinning.
* `checkSsrf` and the `AirtrailClient` are the only two fakes: both would
* otherwise leave the process.
*/
import { AuditLog } from '../../../src/db/entities/AuditLog.entity';
import { Users } from '../../../src/db/entities/Users.entity';
import { UserAirtrailRepository } from '../../../src/db/repositories/UserAirtrail.repository';
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
import { AuditService } from '../../../src/nest/audit/audit.service';
import type { AirtrailClient } from '../../../src/nest/integrations/airtrail.client';
import { AirtrailService } from '../../../src/nest/integrations/airtrail.service';
import { createSnapshotTestDb } from '../../helpers/db-mock';
import { createUser } from '../../helpers/factories';
import { readUser } from '../../helpers/factories/users';
import { resetTestDb } from '../../helpers/test-db';
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
const { checkSsrf } = vi.hoisted(() => ({ checkSsrf: vi.fn() }));
vi.mock('../../../src/utils/ssrfGuard', () => ({
checkSsrf,
safeFetch: vi.fn(),
}));
const testDb = createSnapshotTestDb();
let t: TestOrm;
let usersRepo: UsersRepository;
let svc: AirtrailService;
const listFlights = vi.fn();
beforeAll(async () => {
t = await createTestOrm(testDb);
usersRepo = t.repo(Users);
svc = new AirtrailService(new UserAirtrailRepository(t.orm.em), new AuditService(t.repo(AuditLog), usersRepo), {
listFlights,
} as unknown as AirtrailClient);
});
beforeEach(() => {
resetTestDb(testDb);
t.clear();
checkSsrf.mockReset();
checkSsrf.mockResolvedValue({ allowed: true, isPrivate: false });
});
afterAll(async () => {
await t.close();
testDb.close();
});
async function rawAirtrailKey(userId: number): Promise<string | null> {
return (await readUser(t, userId)).airtrail_api_key ?? null;
}
describe('AirtrailService — at-rest encryption (3h L3)', () => {
it('AIRTRAIL-SVC-ENC-001: saveSettings stores the key enc:v1:-prefixed, never as typed, and round-trips through getAirtrailCredentials', async () => {
const { user } = createUser(testDb);
await svc.saveSettings(user.id, 'https://airtrail.example.test', 'super-secret-key', false, false, null);
const stored = await rawAirtrailKey(user.id);
expect(stored).toMatch(/^enc:v1:/);
expect(stored).not.toContain('super-secret-key');
const creds = await svc.getAirtrailCredentials(user.id);
expect(creds?.apiKey).toBe('super-secret-key');
});
it('AIRTRAIL-SVC-ENC-002: a second save with a new key re-encrypts to a different ciphertext that still round-trips', async () => {
const { user } = createUser(testDb);
await svc.saveSettings(user.id, 'https://airtrail.example.test', 'first-key', false, false, null);
const first = await rawAirtrailKey(user.id);
await svc.saveSettings(user.id, 'https://airtrail.example.test', 'second-key', false, false, null);
const second = await rawAirtrailKey(user.id);
expect(second).toMatch(/^enc:v1:/);
expect(second).not.toBe(first);
expect(second).not.toContain('second-key');
expect((await svc.getAirtrailCredentials(user.id))?.apiKey).toBe('second-key');
});
});