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
@@ -230,4 +230,92 @@ describe('TenderEmailConfigService', () => {
expect(configG2!.host).toBe('imap.user-g2.test');
expect(prisma.__store.size).toBe(2);
});
describe('testConnection (Quick 260907-let, WINDOWS #16)', () => {
function makeFakeProviders() {
return {
imapProvider: { testConnection: vi.fn(async () => ({ success: true })) },
exchangeProvider: { testConnection: vi.fn(async () => ({ success: true })) },
};
}
it('a typed-in password is passed through to the provider unchanged, without reading the database', async () => {
const prisma = makeFakePrisma();
const crypto = makeFakeCrypto();
const { imapProvider, exchangeProvider } = makeFakeProviders();
const service = new TenderEmailConfigService(
prisma as any,
crypto as any,
imapProvider as any,
exchangeProvider as any,
);
const result = await service.testConnection('user-h', {
protocol: 'imap',
encryption: 'ssl-tls',
host: 'imap.example.test',
username: 'typed-user',
password: 'typed-secret',
} as any);
expect(prisma.tenderEmailConfig.findUnique).not.toHaveBeenCalled();
expect(imapProvider.testConnection).toHaveBeenCalledWith(
expect.objectContaining({ username: 'typed-user', password: 'typed-secret' }),
);
expect(result).toEqual({ success: true });
});
it('an empty password falls back to this same user\'s stored, decrypted credentials', async () => {
const prisma = makeFakePrisma();
const crypto = makeFakeCrypto();
const { imapProvider, exchangeProvider } = makeFakeProviders();
const service = new TenderEmailConfigService(
prisma as any,
crypto as any,
imapProvider as any,
exchangeProvider as any,
);
await service.saveConfig(
{ userId: 'user-i', tenantId: 'tenant-i' },
{
protocol: 'imap',
encryption: 'ssl-tls',
username: 'stored-user',
password: 'stored-secret',
} as any,
);
await service.testConnection('user-i', {
protocol: 'imap',
encryption: 'ssl-tls',
} as any);
expect(imapProvider.testConnection).toHaveBeenCalledWith(
expect.objectContaining({ username: 'stored-user', password: 'stored-secret' }),
);
});
it("dto.protocol 'exchange' selects the Exchange provider, not IMAP", async () => {
const prisma = makeFakePrisma();
const crypto = makeFakeCrypto();
const { imapProvider, exchangeProvider } = makeFakeProviders();
const service = new TenderEmailConfigService(
prisma as any,
crypto as any,
imapProvider as any,
exchangeProvider as any,
);
await service.testConnection('user-j', {
protocol: 'exchange',
encryption: 'ssl-tls',
username: 'ews-user',
password: 'ews-secret',
} as any);
expect(exchangeProvider.testConnection).toHaveBeenCalledTimes(1);
expect(imapProvider.testConnection).not.toHaveBeenCalled();
});
});
});