Files
TREK/server/tests/unit/nest/airtrail.service.test.ts
T
Maurice 894a5ee781 test(server): seed the small leftover specs through the repositories
Converts the raw prepare() reads and writes in the share, airtrail, files,
todo, dev-link, vacay, tours, bucket list, unit-of-work and plugin guard
specs to the ORM factories and readers, so they go through the entities the
app uses instead of hand-written SQLite.

The vacay e2e moves from hand-rolled tables to the migrated snapshot, with
its user pinned to the id its session cookie is signed for. Statements that
are raw by nature keep the allow marker with the reason: the legacy oracle
of the bucket list parity spec, the PRAGMA probe of the trek_photos
migration, the unmapped probe table of the route import rollback, the
behind-the-ORM reads of the entity insert probes, and seedUser, which also
serves the hand-rolled users table of createTempDb. Comments that quoted a
prepare() call are reworded so the ratchet no longer counts them.
2026-10-08 18:46:47 +02:00

86 lines
3.8 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 { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
import { createSnapshotTestDb } from '../../helpers/db-mock';
const { checkSsrf } = vi.hoisted(() => ({ checkSsrf: vi.fn() }));
vi.mock('../../../src/utils/ssrfGuard', () => ({
checkSsrf,
safeFetch: vi.fn(),
}));
import { resetTestDb } from '../../helpers/test-db';
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
import { createUser } from '../../helpers/factories';
import { readUser } from '../../helpers/factories/users';
import { Users } from '../../../src/db/entities/Users.entity';
import { AuditLog } from '../../../src/db/entities/AuditLog.entity';
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
import { AuditService } from '../../../src/nest/audit/audit.service';
import { AirtrailService } from '../../../src/nest/integrations/airtrail.service';
import type { AirtrailClient } from '../../../src/nest/integrations/airtrail.client';
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(usersRepo, 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');
});
});