feat(quick-260907-let): Verbindungstest fuer Postfach-Endpunkt im API

- TenderEmailConfigService.testConnection(userId, dto) mit Rueckfall auf
  gespeicherte, entschluesselte Zugangsdaten bei leeren Feldern
- TendersController: POST email-config/test, userId aus Auth-Kontext,
  deklariert vor @Get(':id')
- Beide Provider (ImapProvider/ExchangeInboxProvider) optional angehaengt,
  bestehende 2-Arg-Konstruktoraufrufe bleiben typkorrekt
- Reihenfolge-Waechter und IDOR-Testfall ergaenzt

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
This commit is contained in:
2026-09-07 15:40:28 +02:00
parent 5da58ad182
commit 3bf550bc65
4 changed files with 219 additions and 2 deletions
@@ -136,6 +136,7 @@ function makeFakeEmailConfigService() {
id: 'ec-1',
...dto,
})),
testConnection: vi.fn(async (_userId: string, _dto: any) => ({ success: true }) as any),
};
}
@@ -408,17 +409,20 @@ describe('TendersController — route declaration order (static route before :id
expect(removeIdx).toBeLessThan(idIdx);
});
it('declares getEmailConfig/saveEmailConfig before getTender so GET /:id cannot shadow "email-config" (Plan 14-03, Pitfall 5)', () => {
it('declares getEmailConfig/saveEmailConfig/testEmailConnection before getTender so GET /:id cannot shadow "email-config" (Plan 14-03, Pitfall 5; testEmailConnection added Quick 260907-let)', () => {
const methods = Object.getOwnPropertyNames(TendersController.prototype);
const getIdx = methods.indexOf('getEmailConfig');
const saveIdx = methods.indexOf('saveEmailConfig');
const testIdx = methods.indexOf('testEmailConnection');
const idIdx = methods.indexOf('getTender');
expect(getIdx).toBeGreaterThanOrEqual(0);
expect(saveIdx).toBeGreaterThanOrEqual(0);
expect(testIdx).toBeGreaterThanOrEqual(0);
expect(idIdx).toBeGreaterThanOrEqual(0);
expect(getIdx).toBeLessThan(idIdx);
expect(saveIdx).toBeLessThan(idIdx);
expect(testIdx).toBeLessThan(idIdx);
});
it('declares listTriage and setTriage before getTender so GET /:id cannot shadow "triage" (Plan 11-05, Pitfall 5)', () => {
@@ -1129,6 +1133,30 @@ describe('TendersController — email-config (Plan 14-03, per-user since Phase 1
expect(emailConfigService.getConfigForApi).toHaveBeenNthCalledWith(1, 'user-a');
expect(emailConfigService.getConfigForApi).toHaveBeenNthCalledWith(2, 'user-b');
});
it('POST /email-config/test resolves userId from the auth context, even when the body carries a different identity field (T-QT16-01, IDOR)', async () => {
const prisma = makeFakePrisma();
const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any;
const emailConfigService = makeFakeEmailConfigService();
const controller = new TendersController(
prisma as any,
scheduler,
makeFakeTriageService() as any,
makeFakeSavedSearchService() as any,
makeFakeNotificationPrefService() as any,
makeFakeRssFeedService() as any,
emailConfigService as any,
);
const dto = {
protocol: 'imap',
encryption: 'ssl-tls',
userId: 'attacker-supplied-id',
} as any;
await controller.testEmailConnection(dto, makeFakeRequest('u1', 'tenant1'));
expect(emailConfigService.testConnection).toHaveBeenCalledWith('u1', dto);
});
});
describe('TendersController — D-13 private (email-alert) tender visibility (Phase 14, Plan 03)', () => {