fix(quick-261005-d5d): Kalender-Test bricht bei unerreichbarem Exchange nach 15 s ab
EWS-Aufrufe ueber httpntlm bekommen eine eigene 15-s-Zeitgrenze; httpreqs timeout greift waehrend des Verbindungsaufbaus nicht (gemessen 134 s). Netzfehler liefern im Test den Schluessel 'unreachable', das Formular meldet 'nicht erreichbar' statt 'Zugangsdaten pruefen'. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+3
-2
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-07-17)
|
||||
Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden)
|
||||
Plan: 6 of 6
|
||||
Status: Alle 18 Phasen abgeschlossen; Version 1.2.0 freigegeben. Kein laufender Meilenstein. Nach 1.2.0 auf main (Beta): Bildmarke in Akzentfarbe, CI-Desktop-Skip, Favoriten-Symbol/-Sortierung, Desktop-Server-Adresse, Update in der App (signiert), Versionszeile auf der Setup-Seite — alles verifiziert und auf VM/CI nachgewiesen
|
||||
Last activity: 2026-10-05 - Quick 261005-bt1 Proxmox Autostart-Warnung
|
||||
Last activity: 2026-10-05 - Quick 261005-d5d Kalender-Test Zeitgrenze
|
||||
|
||||
Progress: [██████████] 99%
|
||||
|
||||
@@ -493,7 +493,8 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
|
||||
| 261002-kxc | Nextcloud-Status: Benachrichtigung bei Rot je Benutzer (Mail + Desktop-Hinweis), Klartext-Fehler | 2026-10-02 | faed0d7..6c4bff6 | [261002-kxc-nextcloud-status-benachrichtigung-bei-ro](.planning/quick/261002-kxc-nextcloud-status-benachrichtigung-bei-ro/) |
|
||||
| 261003-387 | Kategorien durch Admins bearbeitbar (anlegen, umbenennen, sortieren, loeschen mit Verschieben, Module zuordnen) | 2026-10-03 | 8ec116c..f2c0a89 | [261003-387-kategorien-durch-admins-bearbeitbar-umbe](.planning/quick/261003-387-kategorien-durch-admins-bearbeitbar-umbe/) |
|
||||
| 261005-blw | Links in Notiz-Kacheln oeffnen in neuem Tab | 2026-10-05 | 781bc9f | [261005-blw-notiz-links-in-neuem-tab](.planning/quick/261005-blw-notiz-links-in-neuem-tab/) |
|
||||
| 261005-bt1 | Proxmox: Warnung je gestopptem Gast mit aktivem Autostart (Karte, Kachel) | 2026-10-05 | (dieser Commit) | [261005-bt1-proxmox-autostart-warnung](.planning/quick/261005-bt1-proxmox-autostart-warnung/) |
|
||||
| 261005-bt1 | Proxmox: Warnung je gestopptem Gast mit aktivem Autostart (Karte, Kachel) | 2026-10-05 | 873d15d | [261005-bt1-proxmox-autostart-warnung](.planning/quick/261005-bt1-proxmox-autostart-warnung/) |
|
||||
| 261005-d5d | Kalender-Test: 15-s-Zeitgrenze fuer Exchange (EWS), Meldung „nicht erreichbar“ | 2026-10-05 | (dieser Commit) | [261005-d5d-kalender-test-zeitgrenze](.planning/quick/261005-d5d-kalender-test-zeitgrenze/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
quick_id: 261005-d5d
|
||||
slug: kalender-test-zeitgrenze
|
||||
date: 2026-10-05
|
||||
---
|
||||
|
||||
# Kalender-Test: Zeitgrenze + Meldung „nicht erreichbar“
|
||||
|
||||
Anlass 05.10.: Exchange auf alpha/live nicht erreichbar (interner DNS -> 172.16.0.3), „Verbindung testen“ hing ~2 min auf „wird geprueft“, danach „Adresse und Zugangsdaten pruefen“.
|
||||
|
||||
## Task 1 — API
|
||||
- EWS-Aufrufe (httpntlm) mit 15 s Zeitgrenze.
|
||||
- Netzfehler (TIMEOUT, ETIMEDOUT, ECONNREFUSED, ...) im Test als `CalendarSourceUnreachableError`; Service liefert Schluessel `unreachable`.
|
||||
|
||||
## Task 2 — Web
|
||||
- Formular zeigt bei `unreachable` eigene Meldung (de/en).
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
quick_id: 261005-d5d
|
||||
status: complete
|
||||
date: 2026-10-05
|
||||
---
|
||||
|
||||
# 261005-d5d: Kalender-Test Zeitgrenze — SUMMARY
|
||||
|
||||
- `ntlmPost` (exchange.provider.ts): eigene 15-s-Zeitgrenze per Timer, zusaetzlich `timeout` an httpntlm. httpreqs `timeout` allein greift NICHT waehrend des Verbindungsaufbaus ueber den Keep-alive-Agenten — echter Lauf gegen 172.16.0.3 dauerte damit noch 134,5 s; erst der eigene Timer brachte 15,0 s. Unit-Test haette das nicht gefunden (Stub), deshalb echter Lauf im lokalen Container.
|
||||
- `CalendarSourceUnreachableError` + `isNetworkUnreachableError` (calendar.service.ts); `testConnection`/`testConnectionFromConfig` liefern `error: 'unreachable'`, lastSyncError „Server not reachable“.
|
||||
- Formular: `formTestUnreachable` statt `formTestFailed`.
|
||||
- Nachweis (lokaler Container, kompilierter Provider): 172.16.0.3 -> nicht erreichbar nach 15,0 s; owa.ctl.de mit falschem Passwort -> erreichbar/abgelehnt 0,0 s. Tests api 2219+, web 1372 gruen.
|
||||
- Gleiches Muster steckt in `inbox/exchange-inbox.provider.ts` (eigenes httpntlm ohne Zeitgrenze) — nicht angefasst.
|
||||
@@ -18,6 +18,7 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
|
||||
|
||||
### Behoben
|
||||
|
||||
- Kalender (Exchange): Ist der Exchange-Server nicht erreichbar, bricht „Verbindung testen“ jetzt nach 15 Sekunden ab, statt rund zwei Minuten auf „Verbindung wird geprüft …“ zu stehen, und meldet „Der Kalender-Server ist nicht erreichbar“ statt des Hinweises auf Adresse und Zugangsdaten. Auch der regelmäßige Abruf im Hintergrund wartet nicht mehr länger als 15 Sekunden.
|
||||
- Links in Notiz-Kacheln öffnen jetzt immer in einem neuen Tab, statt das Dashboard im selben Fenster zu verlassen. In der Desktop-App öffnen sie wie gewohnt im Browser.
|
||||
|
||||
## 1.9.2 – 2026-10-02
|
||||
|
||||
@@ -27,7 +27,7 @@ vi.mock('../prisma/prisma-tenant.extension', () => ({
|
||||
}));
|
||||
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import { CalendarService } from './calendar.service';
|
||||
import { CalendarService, CalendarSourceUnreachableError } from './calendar.service';
|
||||
|
||||
function _applySelect(row: any, select: Record<string, boolean> | undefined) {
|
||||
if (!select) return { ...row };
|
||||
@@ -533,6 +533,45 @@ describe('CalendarService — Bindung an forTenant() (260911-cwh)', () => {
|
||||
expect(vi.mocked(prisma.calendarSource.create)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ─── quick-261005: Kalender-Server nicht erreichbar ──────────────────────
|
||||
|
||||
it('testConnectionFromConfig: nicht erreichbarer Server liefert den Schluessel "unreachable"', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
const exchange = {
|
||||
fetchEvents: vi.fn(async () => []),
|
||||
testConnection: vi.fn(async () => {
|
||||
throw new CalendarSourceUnreachableError();
|
||||
}),
|
||||
};
|
||||
const { service } = makeCalendarService(prisma, { exchangeProvider: exchange });
|
||||
|
||||
const result = await service.testConnectionFromConfig({
|
||||
type: 'exchange',
|
||||
url: 'https://owa.example.invalid/EWS/Exchange.asmx',
|
||||
exchangeMode: 'ews',
|
||||
} as any);
|
||||
|
||||
expect(result).toEqual({ success: false, error: 'unreachable' });
|
||||
});
|
||||
|
||||
it('testConnection (gespeicherte Quelle): nicht erreichbar -> "unreachable", lastSyncError ohne Details', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedSource({ id: 'src-a1', userId: 'user-a1', tenantId: 't1', type: 'exchange' });
|
||||
const exchange = {
|
||||
fetchEvents: vi.fn(async () => []),
|
||||
testConnection: vi.fn(async () => {
|
||||
throw new CalendarSourceUnreachableError();
|
||||
}),
|
||||
};
|
||||
const { service } = makeCalendarService(prisma, { exchangeProvider: exchange });
|
||||
|
||||
const result = await service.testConnection('src-a1', 'user-a1', 't1');
|
||||
|
||||
expect(result).toEqual({ success: false, error: 'unreachable' });
|
||||
const updateCall = vi.mocked(prisma.calendarSource.update).mock.calls.at(-1)?.[0] as any;
|
||||
expect(updateCall.data).toEqual({ lastSyncError: 'Server not reachable' });
|
||||
});
|
||||
|
||||
// ─── Wachhund ────────────────────────────────────────────────────────
|
||||
|
||||
it('keine Methode dieses Bereichs erzeugt mehr als EINEN gebundenen Klienten je Aufruf', async () => {
|
||||
|
||||
@@ -30,6 +30,43 @@ export interface CalendarEvent {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* quick-261005: Ein Provider wirft diesen Fehler aus `testConnection`, wenn
|
||||
* der Kalender-Server gar nicht erreichbar ist (Zeitueberschreitung,
|
||||
* abgewiesene Verbindung, Name unbekannt) — im Unterschied zu „erreichbar,
|
||||
* aber Anmeldung abgelehnt“ (`false`). Traegt bewusst keine Details (T-05-13).
|
||||
*/
|
||||
export class CalendarSourceUnreachableError extends Error {
|
||||
constructor() {
|
||||
super('Calendar server not reachable');
|
||||
this.name = 'CalendarSourceUnreachableError';
|
||||
}
|
||||
}
|
||||
|
||||
/** Fehlerkennungen von Node/httpreq, die „Server nicht erreichbar“ bedeuten. */
|
||||
const NETWORK_UNREACHABLE_CODES = new Set([
|
||||
'TIMEOUT', // httpreq bei Ablauf von `timeout`
|
||||
'ETIMEDOUT',
|
||||
'ECONNREFUSED',
|
||||
'ECONNRESET',
|
||||
'EHOSTUNREACH',
|
||||
'ENETUNREACH',
|
||||
'ENOTFOUND',
|
||||
'EAI_AGAIN',
|
||||
]);
|
||||
|
||||
export function isNetworkUnreachableError(error: unknown): boolean {
|
||||
const code = (error as { code?: unknown; cause?: { code?: unknown } } | null)?.code;
|
||||
const causeCode = (error as { cause?: { code?: unknown } } | null)?.cause?.code;
|
||||
return (
|
||||
(typeof code === 'string' && NETWORK_UNREACHABLE_CODES.has(code)) ||
|
||||
(typeof causeCode === 'string' && NETWORK_UNREACHABLE_CODES.has(causeCode))
|
||||
);
|
||||
}
|
||||
|
||||
/** Stabiler Fehlerschluessel fuer die Oberflaeche (dort uebersetzt). */
|
||||
export const CALENDAR_TEST_UNREACHABLE = 'unreachable';
|
||||
|
||||
/**
|
||||
* Provider interface for calendar source integrations.
|
||||
* Each provider (ICS, CalDAV, Exchange) implements this contract.
|
||||
@@ -319,13 +356,15 @@ export class CalendarService {
|
||||
});
|
||||
|
||||
return { success };
|
||||
} catch {
|
||||
const errorMsg = 'Connection failed'; // T-05-13: generic error, no credentials
|
||||
} catch (error) {
|
||||
const unreachable = error instanceof CalendarSourceUnreachableError;
|
||||
// T-05-13: generic error, no credentials
|
||||
const errorMsg = unreachable ? 'Server not reachable' : 'Connection failed';
|
||||
await tenantPrisma.calendarSource.update({
|
||||
where: { id },
|
||||
data: { lastSyncError: errorMsg },
|
||||
});
|
||||
return { success: false, error: errorMsg };
|
||||
return { success: false, error: unreachable ? CALENDAR_TEST_UNREACHABLE : errorMsg };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +403,10 @@ export class CalendarService {
|
||||
try {
|
||||
const success = await provider.testConnection(tempSource);
|
||||
return { success };
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (error instanceof CalendarSourceUnreachableError) {
|
||||
return { success: false, error: CALENDAR_TEST_UNREACHABLE };
|
||||
}
|
||||
return { success: false, error: 'Connection failed' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import type { AuthProviderCallback } from '@microsoft/microsoft-graph-client';
|
||||
import { CalendarEvent, CalendarProvider } from '../calendar.service';
|
||||
import {
|
||||
CalendarEvent,
|
||||
CalendarProvider,
|
||||
CalendarSourceUnreachableError,
|
||||
isNetworkUnreachableError,
|
||||
} from '../calendar.service';
|
||||
|
||||
/** Optionen, die ntlmPost() unten uebergibt — nichts darueber hinaus. */
|
||||
interface NtlmOptions {
|
||||
@@ -11,6 +16,8 @@ interface NtlmOptions {
|
||||
workstation: string;
|
||||
body: string;
|
||||
headers: Record<string, string>;
|
||||
/** Millisekunden bis zum Abbruch mit `code: 'TIMEOUT'` (siehe ntlmPost). */
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,6 +38,13 @@ const httpntlm = require('httpntlm') as {
|
||||
post: (opts: NtlmOptions, cb: (err: Error | null, res: NtlmResponse) => void) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* quick-261005: Ohne Grenze wartete ein EWS-Aufruf auf eine nicht
|
||||
* erreichbare Adresse rund zwei Minuten (TCP-Verbindungsaufbau des
|
||||
* Betriebssystems), der Test-Knopf hing so lange auf „wird geprueft“.
|
||||
*/
|
||||
const EWS_TIMEOUT_MS = 15_000;
|
||||
|
||||
const NS_SOAP = 'http://schemas.xmlsoap.org/soap/envelope/';
|
||||
const NS_TYPES = 'http://schemas.microsoft.com/exchange/services/2006/types';
|
||||
const NS_MESSAGES = 'http://schemas.microsoft.com/exchange/services/2006/messages';
|
||||
@@ -74,7 +88,15 @@ function extractAttr(xml: string, tag: string, attr: string): string {
|
||||
|
||||
function ntlmPost(opts: NtlmOptions): Promise<{ statusCode: number; body: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Eigene Zeitgrenze zusaetzlich zu `opts.timeout`: httpreq setzt seine
|
||||
// nur als Leerlaufgrenze am Socket, die waehrend des Verbindungsaufbaus
|
||||
// ueber den Keep-alive-Agenten von httpntlm NICHT greift — gemessen
|
||||
// 05.10.: 134 s bis zum Fehler trotz `timeout: 15000`.
|
||||
const timer = setTimeout(() => {
|
||||
reject(Object.assign(new Error('EWS request timed out'), { code: 'TIMEOUT' }));
|
||||
}, opts.timeout);
|
||||
httpntlm.post(opts, (err, res) => {
|
||||
clearTimeout(timer);
|
||||
if (err) return reject(err);
|
||||
resolve({
|
||||
statusCode: res.statusCode,
|
||||
@@ -152,7 +174,10 @@ export class ExchangeProvider implements CalendarProvider {
|
||||
} else {
|
||||
return await this.testEwsConnection(source);
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// quick-261005: „nicht erreichbar“ getrennt melden, damit die
|
||||
// Oberflaeche nicht „Zugangsdaten pruefen“ sagt, wenn das Netz fehlt.
|
||||
if (isNetworkUnreachableError(error)) throw new CalendarSourceUnreachableError();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -316,6 +341,7 @@ export class ExchangeProvider implements CalendarProvider {
|
||||
domain: source.domain ?? '',
|
||||
workstation: '',
|
||||
body: soap,
|
||||
timeout: EWS_TIMEOUT_MS,
|
||||
headers: {
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'SOAPAction': `"http://schemas.microsoft.com/exchange/services/2006/messages/${action}"`,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { testSourceConfig } from '@/lib/calendar-api';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import de from '@/messages/de.json';
|
||||
import { CalendarSourceForm } from './calendar-source-form';
|
||||
@@ -111,3 +112,29 @@ describe('CalendarSourceForm — URL-Platzhalter je Typ + EWS-Hinweis (quick-260
|
||||
expect(fehler.compareDocumentPosition(hinweis) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CalendarSourceForm — Verbindungstest: nicht erreichbar (quick-261005)', () => {
|
||||
function fillAndTest() {
|
||||
const { urlInput, typeSelect } = renderForm();
|
||||
fireEvent.change(screen.getByLabelText(/^Name/), { target: { value: 'Firma' } });
|
||||
fireEvent.change(typeSelect, { target: { value: 'exchange' } });
|
||||
fireEvent.change(urlInput, { target: { value: 'https://owa.example.invalid/EWS/Exchange.asmx' } });
|
||||
fireEvent.click(screen.getByTestId('source-form-test'));
|
||||
}
|
||||
|
||||
it('Schluessel "unreachable" zeigt die Nicht-erreichbar-Meldung statt „Zugangsdaten pruefen“', async () => {
|
||||
vi.mocked(testSourceConfig).mockResolvedValueOnce({ success: false, error: 'unreachable' });
|
||||
fillAndTest();
|
||||
|
||||
expect(await screen.findByText(t.formTestUnreachable)).toBeInTheDocument();
|
||||
expect(screen.queryByText(t.formTestFailed)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('sonstiger Fehlschlag zeigt weiter die bisherige Meldung', async () => {
|
||||
vi.mocked(testSourceConfig).mockResolvedValueOnce({ success: false, error: 'Connection failed' });
|
||||
fillAndTest();
|
||||
|
||||
expect(await screen.findByText(t.formTestFailed)).toBeInTheDocument();
|
||||
expect(screen.queryByText(t.formTestUnreachable)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +58,16 @@ interface CalendarSourceFormProps {
|
||||
* (CalDAV / ICS / Exchange Graph / Exchange EWS), and Exchange EWS shows a
|
||||
* hint below the field explaining that the full EWS address is required.
|
||||
*/
|
||||
/**
|
||||
* quick-261005: „nicht erreichbar“ (Schluessel `unreachable` vom Server)
|
||||
* getrennt von „Anmeldung fehlgeschlagen“ — sonst schickt die Meldung den
|
||||
* Benutzer zu den Zugangsdaten, obwohl das Netz fehlt.
|
||||
*/
|
||||
function testOutcome(result: { success: boolean; error?: string }): 'success' | 'error' | 'unreachable' {
|
||||
if (result.success) return 'success';
|
||||
return result.error === 'unreachable' ? 'unreachable' : 'error';
|
||||
}
|
||||
|
||||
export function CalendarSourceForm({
|
||||
onSave,
|
||||
onCancel,
|
||||
@@ -75,7 +85,9 @@ export function CalendarSourceForm({
|
||||
initialValues?.exchangeMode ?? 'graph',
|
||||
);
|
||||
const [domain, setDomain] = useState(initialValues?.domain ?? '');
|
||||
const [testStatus, setTestStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
const [testStatus, setTestStatus] = useState<
|
||||
'idle' | 'loading' | 'success' | 'error' | 'unreachable'
|
||||
>('idle');
|
||||
const [color, setColor] = useState(
|
||||
initialValues?.color ?? SOURCE_COLOR_PALETTE[0].value,
|
||||
);
|
||||
@@ -127,7 +139,7 @@ export function CalendarSourceForm({
|
||||
// In edit mode with no new password entered, use saved credentials via the source endpoint
|
||||
if (initialValues?.id && !password) {
|
||||
const result = await testSource(initialValues.id);
|
||||
setTestStatus(result.success ? 'success' : 'error');
|
||||
setTestStatus(testOutcome(result));
|
||||
return;
|
||||
}
|
||||
const result = await testSourceConfig({
|
||||
@@ -138,7 +150,7 @@ export function CalendarSourceForm({
|
||||
exchangeMode: isExchange ? exchangeMode : undefined,
|
||||
domain: isExchange && domain.trim() ? domain.trim() : undefined,
|
||||
});
|
||||
setTestStatus(result.success ? 'success' : 'error');
|
||||
setTestStatus(testOutcome(result));
|
||||
} catch {
|
||||
setTestStatus('error');
|
||||
}
|
||||
@@ -382,6 +394,9 @@ export function CalendarSourceForm({
|
||||
{testStatus === 'error' && (
|
||||
<p className="text-sm text-destructive">{t('calendar.formTestFailed')}</p>
|
||||
)}
|
||||
{testStatus === 'unreachable' && (
|
||||
<p className="text-sm text-destructive">{t('calendar.formTestUnreachable')}</p>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ export async function testSourceConfig(config: {
|
||||
*/
|
||||
export async function testSource(
|
||||
id: string,
|
||||
): Promise<{ success: boolean }> {
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const res = await fetch(`${API_URL}/calendar/sources/${id}/test`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
|
||||
@@ -332,6 +332,7 @@
|
||||
"formTesting": "Verbindung wird geprüft …",
|
||||
"formTestSuccess": "Verbindung erfolgreich.",
|
||||
"formTestFailed": "Verbindung fehlgeschlagen. Bitte prüfen Sie Adresse und Zugangsdaten.",
|
||||
"formTestUnreachable": "Der Kalender-Server ist nicht erreichbar. Bitte prüfen Sie die Adresse und ob der Server vom Tessera-Server aus erreichbar ist.",
|
||||
"formCancel": "Abbrechen",
|
||||
"formUrlErrorHttps": "Die Adresse muss mit https:// beginnen.",
|
||||
"formUrlErrorInvalid": "Bitte geben Sie eine gültige Adresse ein.",
|
||||
|
||||
@@ -332,6 +332,7 @@
|
||||
"formTesting": "Checking connection …",
|
||||
"formTestSuccess": "Connection successful.",
|
||||
"formTestFailed": "Connection failed. Please check the address and credentials.",
|
||||
"formTestUnreachable": "The calendar server is not reachable. Please check the address and whether the server can be reached from the Tessera server.",
|
||||
"formCancel": "Cancel",
|
||||
"formUrlErrorHttps": "The address must start with https://.",
|
||||
"formUrlErrorInvalid": "Please enter a valid address.",
|
||||
|
||||
Reference in New Issue
Block a user