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 type { 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'; import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest'; // Avoid any real DNS/network from the SSRF guard during saveSettings and the probe. vi.mock('../../../../src/utils/ssrfGuard', () => ({ checkSsrf: vi.fn(async () => ({ allowed: true, isPrivate: false })), safeFetch: vi.fn(), })); // The probe is the only call that would leave the process, so the client is a // stub; the credential handling around it runs against the real row. const listFlights = vi.fn(); let svc: AirtrailService; 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)), { listFlights, } as unknown as AirtrailClient); }); afterAll(async () => { await t.close(); }); const MASK = '••••••••'; beforeEach(() => { listFlights.mockReset(); listFlights.mockResolvedValue([]); }); describe('airtrail test connection keeps the stored key with its host', () => { it('refuses a blank key field against a different host than the stored one before anything is sent', async () => { // The form prefills the address and never the key, so retyping the URL and // pressing Test is the ordinary way to move an instance. Falling back to the // stored key here would carry it to whatever host was just typed. const { user } = createUser(db); await svc.saveSettings(user.id, 'https://old.example', 'stored-key', false, false, null); const out = await svc.testConnection(user.id, 'https://someone-elses.example', undefined, false); expect(out.connected).toBe(false); expect(out.error).toContain('https://old.example'); expect(listFlights).not.toHaveBeenCalled(); }); it('treats the mask against a different port as a blank field, not a key', async () => { const { user } = createUser(db); await svc.saveSettings(user.id, 'https://old.example', 'stored-key', false, false, null); const out = await svc.testConnection(user.id, 'https://old.example:8443', MASK, false); expect(out.connected).toBe(false); expect(listFlights).not.toHaveBeenCalled(); }); it('still tests a path edit on the same origin with the stored key', async () => { // The same instance under a corrected path is where the key was issued, and // refusing it would make every trailing-slash edit demand the key again. const { user } = createUser(db); await svc.saveSettings(user.id, 'https://at.example/api', 'stored-key', false, false, null); const out = await svc.testConnection(user.id, 'https://at.example/', undefined, false); expect(out.connected).toBe(true); expect(listFlights).toHaveBeenCalledWith({ baseUrl: 'https://at.example/', apiKey: 'stored-key', allowInsecureTls: false, }); }); it('uses a key typed for the new host as typed, so moving an instance and testing it first still works', async () => { const { user } = createUser(db); await svc.saveSettings(user.id, 'https://old.example', 'stored-key', false, false, null); const out = await svc.testConnection(user.id, 'https://new.example', 'minted-for-new', false); expect(out.connected).toBe(true); expect(listFlights).toHaveBeenCalledWith({ baseUrl: 'https://new.example', apiKey: 'minted-for-new', allowInsecureTls: false, }); }); });