Files
SRCmail/lib/__tests__/caldav-discover-route.test.ts
T
Stefan HildebrandtandLinus Rath 2fac6ebfb8 test: add characterisation tests for untested integration seams
Golden-master tests pinning the CURRENT behavior of high-value modules
that had no coverage — integration seams, security helpers, two API
route handlers, and complex pure utils. 111 tests across 12 files.

New tests:
- auth-crypto / session-cookie: AES-256-GCM session encryption roundtrip,
  tamper/version/missing-secret handling; cookie-slot naming.
- unified-mailbox: multi-account fan-out, sort, totals, per-account error
  isolation, personal-vs-shared JMAP target resolution, counts/roles.
- account-state-manager: snapshot/restore across the six real Zustand
  stores; clearAllStores reset shape; evict.
- mdn: RFC 5322 MDN assembly (CRLF, RFC2047, base64 wrap, headers).
- tnef: winmail.dat binary parsing from hand-built fixtures.
- download-filename / subject-prefix / birthday-calendar / eml-import:
  filename templating, multilingual prefix stripping, birthday event
  generation, .eml/.zip import.
- webdav / caldav-discover route handlers: auth guards, path validation,
  upstream URL construction, candidate probing.
- helpers/factories.ts: shared makeEmail/makeMailbox/makeFakeJmapClient.

Tests follow the repo's existing patterns (route-import, fake IJMAPClient,
fetch spy, real store singletons). Where current behavior looks buggy it
is pinned and flagged with a // CHARACTERISATION: comment (see PR for the
suspected-bugs list); no production code is changed.
2026-06-19 23:52:42 +02:00

86 lines
3.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
vi.mock('next/server', () => ({
NextResponse: {
json: (data: unknown, init?: { status?: number }) => ({ status: init?.status ?? 200, json: async () => data }),
},
NextRequest: class {},
}));
vi.mock('@/lib/logger', () => ({ logger: { warn: () => {}, error: () => {} } }));
vi.mock('@/lib/stalwart/credentials', () => ({ getStalwartCredentials: vi.fn() }));
import { POST } from '@/app/api/caldav/discover/route';
import { getStalwartCredentials } from '@/lib/stalwart/credentials';
const mockCreds = getStalwartCredentials as unknown as Mock;
const CREDS = { serverUrl: 'https://mail.example.com', username: 'u', authHeader: 'Basic abc' };
let fetchSpy: Mock;
function makeReq(body: unknown): Parameters<typeof POST>[0] {
return { headers: { get: () => null }, json: async () => body } as unknown as Parameters<typeof POST>[0];
}
function read(res: unknown) {
return res as { status: number; json: () => Promise<{ wellKnownUrl: string; accounts: Record<string, { url: string | null; resolvedAccount: string | null }> }> };
}
const target = (c: string) => `https://mail.example.com/dav/cal/${c}`;
beforeEach(() => {
mockCreds.mockResolvedValue(CREDS);
fetchSpy = vi.fn();
vi.stubGlobal('fetch', fetchSpy);
});
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
describe('POST /api/caldav/discover', () => {
it('401 without credentials', async () => {
mockCreds.mockResolvedValue(null);
const res = read(await POST(makeReq({ accounts: [] })));
expect(res.status).toBe(401);
});
it('returns the .well-known url and resolves the first 207 candidate, skipping the rest', async () => {
fetchSpy.mockResolvedValue({ status: 207, headers: new Headers() });
const res = read(await POST(makeReq({ accounts: [{ key: 'A', candidates: ['c1', 'c2'] }] })));
const data = await res.json();
expect(data.wellKnownUrl).toBe('https://mail.example.com/.well-known/caldav');
expect(data.accounts.A).toEqual({ url: target('c1'), resolvedAccount: 'c1' });
expect(fetchSpy).toHaveBeenCalledTimes(1); // c2 never probed
expect(fetchSpy).toHaveBeenCalledWith(target('c1'), expect.objectContaining({ method: 'PROPFIND' }));
});
it('resolves a redirect Location relative to the probe URL', async () => {
fetchSpy.mockResolvedValue({ status: 302, headers: new Headers({ Location: '/dav/cal/real-home' }) });
const res = read(await POST(makeReq({ accounts: [{ key: 'A', candidates: ['c1'] }] })));
const data = await res.json();
expect(data.accounts.A).toEqual({ url: 'https://mail.example.com/dav/cal/real-home', resolvedAccount: 'c1' });
});
it('returns null url when every candidate fails, but still 200', async () => {
fetchSpy.mockResolvedValue({ status: 404, headers: new Headers() });
const res = read(await POST(makeReq({ accounts: [{ key: 'A', candidates: ['c1', 'c2'] }] })));
expect(res.status).toBe(200);
expect((await res.json()).accounts.A).toEqual({ url: null, resolvedAccount: null });
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('catches a probe error and continues to the next candidate', async () => {
fetchSpy
.mockRejectedValueOnce(new Error('boom'))
.mockResolvedValueOnce({ status: 207, headers: new Headers() });
const res = read(await POST(makeReq({ accounts: [{ key: 'A', candidates: ['c1', 'c2'] }] })));
expect((await res.json()).accounts.A).toEqual({ url: target('c2'), resolvedAccount: 'c2' });
});
it('de-duplicates and trims candidates before probing', async () => {
fetchSpy.mockResolvedValue({ status: 404, headers: new Headers() });
await POST(makeReq({ accounts: [{ key: 'A', candidates: [' c1 ', 'c1', '', 'c1'] }] }));
expect(fetchSpy).toHaveBeenCalledTimes(1); // collapsed to a single "c1"
expect(fetchSpy).toHaveBeenCalledWith(target('c1'), expect.anything());
});
});