mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
ae26a6c61c
The address and the key it may no longer carry now change together, as they do for Dawarich, so the transaction ratchet drops the AirTrail entry. The tests that pinned the old no-transaction shape now pin the new one.
108 lines
4.7 KiB
TypeScript
108 lines
4.7 KiB
TypeScript
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
|
|
|
// ATC-TX-001 reads Nest's own emitted constructor-param metadata (the same
|
|
// mechanism `discovery.test.ts` uses) — needs the polyfill `src/index.ts`
|
|
// normally provides, absent from the vitest entrypoint.
|
|
import 'reflect-metadata';
|
|
|
|
import { db } from '../../../../src/db/database';
|
|
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 { AuditService } from '../../../../src/nest/audit/audit.service';
|
|
import { UnitOfWork } from '../../../../src/nest/database/unit-of-work';
|
|
import { AirtrailClient } from '../../../../src/nest/integrations/airtrail.client';
|
|
import { AirtrailService } from '../../../../src/nest/integrations/airtrail.service';
|
|
import { createUser } from '../../../helpers/factories';
|
|
import { createTestOrm, type TestOrm } from '../../../helpers/test-orm';
|
|
|
|
// Avoid any real DNS/network from the SSRF guard during saveSettings.
|
|
vi.mock('../../../../src/utils/ssrfGuard', () => ({
|
|
checkSsrf: vi.fn(async () => ({ allowed: true, isPrivate: false })),
|
|
safeFetch: vi.fn(),
|
|
}));
|
|
|
|
// The free functions became methods with the airtrail fold; same SQL, same
|
|
// behaviour, one instance over the same db handle.
|
|
let svc: AirtrailService;
|
|
let getConnectionSettings: (
|
|
...args: Parameters<AirtrailService['getConnectionSettings']>
|
|
) => ReturnType<AirtrailService['getConnectionSettings']>;
|
|
let isAirtrailWriteEnabled: (
|
|
...args: Parameters<AirtrailService['isAirtrailWriteEnabled']>
|
|
) => ReturnType<AirtrailService['isAirtrailWriteEnabled']>;
|
|
let saveSettings: (...args: Parameters<AirtrailService['saveSettings']>) => ReturnType<AirtrailService['saveSettings']>;
|
|
let t: TestOrm;
|
|
|
|
beforeAll(async () => {
|
|
t = await createTestOrm(db);
|
|
svc = new AirtrailService(
|
|
new UserAirtrailRepository(t.orm.em),
|
|
new AuditService(t.repo(AuditLog), t.repo(Users)),
|
|
new AirtrailClient(),
|
|
new UnitOfWork(t.orm.em),
|
|
);
|
|
getConnectionSettings = (...args) => svc.getConnectionSettings(...args);
|
|
isAirtrailWriteEnabled = (...args) => svc.isAirtrailWriteEnabled(...args);
|
|
saveSettings = (...args) => svc.saveSettings(...args);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await t.close();
|
|
});
|
|
|
|
describe('airtrail writeback opt-in persistence (#1240)', () => {
|
|
it('defaults the writeback opt-in to off for a new user', async () => {
|
|
const { user } = createUser(db);
|
|
expect(await isAirtrailWriteEnabled(user.id)).toBe(false);
|
|
expect((await getConnectionSettings(user.id)).writeEnabled).toBe(false);
|
|
});
|
|
|
|
it('persists the opt-in and lets it be toggled back off without dropping the key', async () => {
|
|
const { user } = createUser(db);
|
|
|
|
await saveSettings(user.id, 'https://at.example.com', 'secret-key', false, true, null);
|
|
expect(await isAirtrailWriteEnabled(user.id)).toBe(true);
|
|
const on = await getConnectionSettings(user.id);
|
|
expect(on.writeEnabled).toBe(true);
|
|
expect(on.connected).toBe(true); // key stored
|
|
|
|
// No key supplied keeps the stored key; only the opt-in flips back off.
|
|
await saveSettings(user.id, 'https://at.example.com', undefined, false, false, null);
|
|
expect(await isAirtrailWriteEnabled(user.id)).toBe(false);
|
|
expect((await getConnectionSettings(user.id)).connected).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('airtrail saveSettings writes in one transaction, as Dawarich does', () => {
|
|
it('ATC-TX-001: AirtrailService is constructed with a UnitOfWork to wrap its writes in', () => {
|
|
const paramTypes = Reflect.getMetadata('design:paramtypes', AirtrailService) as unknown[] | undefined;
|
|
expect(paramTypes?.map((p) => (p as { name?: string })?.name)).toEqual([
|
|
'UserAirtrailRepository',
|
|
'AuditService',
|
|
'AirtrailClient',
|
|
'UnitOfWork',
|
|
]);
|
|
});
|
|
|
|
it('ATC-TX-002: clearing the URL drops the key inside the same transaction as the address write', async () => {
|
|
const { user } = createUser(db);
|
|
await saveSettings(user.id, 'https://at.example.com', 'secret-key', false, false, null);
|
|
|
|
const inner = svc as unknown as { airtrailRepo: UserAirtrailRepository; uow: UnitOfWork };
|
|
const txSpy = vi.spyOn(inner.uow, 'transactional');
|
|
const setSpy = vi.spyOn(inner.airtrailRepo, 'setAirtrailSettings');
|
|
const clearSpy = vi.spyOn(inner.airtrailRepo, 'clearAirtrailApiKey');
|
|
try {
|
|
await saveSettings(user.id, '', undefined, false, false, null);
|
|
expect(txSpy).toHaveBeenCalledTimes(1);
|
|
expect(setSpy).toHaveBeenCalledTimes(1);
|
|
expect(clearSpy).toHaveBeenCalledTimes(1);
|
|
} finally {
|
|
txSpy.mockRestore();
|
|
setSpy.mockRestore();
|
|
clearSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|