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:
2026-10-05 09:29:25 +02:00
parent 94bd2a4a04
commit e49d4c7c9d
13 changed files with 305 additions and 13 deletions
@@ -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>
);
}
+1 -1
View File
@@ -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',
+1
View File
@@ -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.",
+1
View File
@@ -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.",