import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { CalendarSourceUnreachableError, isNetworkUnreachableError } from '../calendar.service'; import type { ExchangeProvider as ExchangeProviderType } from './exchange.provider'; /** * ExchangeProvider.testConnection (quick-261005): Zeitgrenze fuer EWS und * getrennte Meldung „nicht erreichbar“. * * httpntlm wird per CommonJS-`require` geladen — `vi.mock` greift dort nicht. * Wie in `inbox/exchange-inbox.provider.spec.ts` wird deshalb Nodes * `require.cache` vor dem ersten Laden des Providers mit einem Stub belegt. */ const httpntlmPath = require.resolve('httpntlm'); const httpntlmPost = vi.fn((_opts: any, cb: (err: Error | null, res: any) => void) => { cb(new Error('httpntlmPost not configured for this test'), null); }); require.cache[httpntlmPath] = { id: httpntlmPath, filename: httpntlmPath, loaded: true, exports: { post: httpntlmPost }, } as any; let ExchangeProvider: typeof ExchangeProviderType; beforeAll(async () => { ({ ExchangeProvider } = await import('./exchange.provider')); }); beforeEach(() => { httpntlmPost.mockReset(); }); const SOURCE = { id: 'src-1', url: 'https://owa.example.invalid/EWS/Exchange.asmx', username: 'kalender', password: 'geheim', domain: 'CONTOSO', exchangeMode: 'ews', }; function failWith(code: string) { httpntlmPost.mockImplementation((_opts, cb) => { const err = Object.assign(new Error(`fail ${code}`), { code }); cb(err, null); }); } describe('ExchangeProvider.testConnection (EWS)', () => { it('uebergibt httpntlm eine Zeitgrenze von 15 Sekunden', async () => { httpntlmPost.mockImplementation((_opts, cb) => cb(null, { statusCode: 200, body: '' })); await new ExchangeProvider().testConnection(SOURCE); expect(httpntlmPost).toHaveBeenCalled(); expect(httpntlmPost.mock.calls[0][0].timeout).toBe(15_000); }); it.each([ 'TIMEOUT', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND', 'EHOSTUNREACH', ])('Netzfehler %s -> CalendarSourceUnreachableError', async (code) => { failWith(code); await expect(new ExchangeProvider().testConnection(SOURCE)).rejects.toBeInstanceOf( CalendarSourceUnreachableError, ); }); it('keine Rueckmeldung von httpntlm (Verbindungsaufbau haengt) -> nach 15 s nicht erreichbar', async () => { vi.useFakeTimers(); try { httpntlmPost.mockImplementation(() => { /* ruft nie zurueck — wie ein unbeantworteter Verbindungsaufbau */ }); const pending = new ExchangeProvider().testConnection(SOURCE); const assertion = expect(pending).rejects.toBeInstanceOf(CalendarSourceUnreachableError); await vi.advanceTimersByTimeAsync(14_999); await vi.advanceTimersByTimeAsync(1); await assertion; } finally { vi.useRealTimers(); } }); it('erreichbar, aber Anmeldung abgelehnt (401) -> false, kein Wurf', async () => { httpntlmPost.mockImplementation((_opts, cb) => cb(null, { statusCode: 401, body: '' })); await expect(new ExchangeProvider().testConnection(SOURCE)).resolves.toBe(false); }); it('sonstiger Fehler ohne Netzkennung -> false', async () => { httpntlmPost.mockImplementation((_opts, cb) => cb(new Error('kaputt'), null)); await expect(new ExchangeProvider().testConnection(SOURCE)).resolves.toBe(false); }); }); describe('isNetworkUnreachableError', () => { it('erkennt Kennung direkt und in cause, sonst nicht', () => { expect(isNetworkUnreachableError({ code: 'ETIMEDOUT' })).toBe(true); expect(isNetworkUnreachableError({ cause: { code: 'ECONNREFUSED' } })).toBe(true); expect(isNetworkUnreachableError(new Error('x'))).toBe(false); expect(isNetworkUnreachableError(null)).toBe(false); }); });