/** * DC-161 (judge r1 blocker 5): end-to-end auto-wiring contract test. * * Proves the FULL chain the product requirement depends on: * registerAutoWiredSource() writes credentials at `calendar:source:` * → GET /api/v1/calendar/events resolves THOSE credentials * → authenticated CalDAV REPORT against a real HTTP server (this process) * → the server checks Basic auth matches the provisioned password * → returned ICS is parsed and events reach the API response. * * If the credential-key convention or the context wiring regresses, this * test fails — the two defects the judge caught are exactly its target. */ const express = require('express'); const http = require('http'); const fs = require('fs'); const os = require('os'); const path = require('path'); const ICS_BODY = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//DC161-E2E//EN', 'BEGIN:VEVENT', 'UID:e2e-1', 'DTSTART:20300101T100000Z', 'DTEND:20300101T110000Z', 'SUMMARY:Auto-wired dentist appointment', 'END:VEVENT', 'END:VCALENDAR', ].join('\r\n'); /** Minimal CalDAV-ish server: verifies Basic auth, answers REPORT with ICS. */ function startFakeCalDAV(expectedUser, expectedPass) { const server = http.createServer((req, res) => { if (req.method === 'OPTIONS') { // Anonymous capability probe — real CalDAV servers allow this unauthenticated res.writeHead(200, { DAV: '1, 2, calendar-access' }); res.end('ok'); return; } const auth = req.headers.authorization || ''; const m = /^Basic (.+)$/.exec(auth); let user = '', pass = ''; if (m) { try { [user, pass] = Buffer.from(m[1], 'base64').toString('utf8').split(':'); } catch (_) {} } if (user !== expectedUser || pass !== expectedPass) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="dc161-e2e"' }); res.end('unauthorized'); return; } if (req.method === 'REPORT') { const body = ` /dashcaddy/calendar/ ${ICS_BODY}HTTP/1.1 200 OK `; res.writeHead(207, { 'Content-Type': 'application/xml' }); res.end(body); return; } if (req.method === 'OPTIONS') { // Anonymous capability probe — real CalDAV servers allow this unauthenticated res.writeHead(200, { DAV: '1, 2, calendar-access' }); res.end('ok'); return; } // Any other method requires auth (the REPORT path above is the one the // client uses with credentials; PROPFIND etc. stay auth-guarded). res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="dc161-e2e"' }); res.end('unauthorized'); }); return new Promise((resolve) => { // CALDAV_ALLOW_HTTP=1 is required for the client to speak http:// to // localhost in this test; set by the suite below. server.listen(0, '127.0.0.1', () => resolve({ server, port: server.address().port })); }); } describe('DC-161 end-to-end auto-wiring → authenticated event fetch', () => { let calServer, calPort, app, deps, tmpConfigRoot, savedAllowHttp; beforeAll(async () => { savedAllowHttp = process.env.CALDAV_ALLOW_HTTP; process.env.CALDAV_ALLOW_HTTP = '1'; // localhost-only fake server tmpConfigRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'dc161-e2e-')); process.env.DC_RADICALE_CONFIG_ROOT = tmpConfigRoot; }); afterAll(async () => { if (savedAllowHttp === undefined) delete process.env.CALDAV_ALLOW_HTTP; else process.env.CALDAV_ALLOW_HTTP = savedAllowHttp; if (calServer) await new Promise((r) => calServer.close(r)); if (tmpConfigRoot) fs.rmSync(tmpConfigRoot, { recursive: true, force: true }); }); function buildApp() { jest.resetModules(); const createRoutes = require('../../routes/calendar'); const stored = {}; const items = []; deps = { calendarStateManager: { read: jest.fn(async () => items), addItem: jest.fn(async (item) => { items.push(item); return item; }), updateItem: jest.fn(async (id, updates) => { const it = items.find(x => x.id === id); if (it) Object.assign(it, updates); return it; }), removeItem: jest.fn(async () => {}), findItem: jest.fn(async (id) => items.find(x => x.id === id) || null), }, credentialManager: { store: jest.fn(async (key, value) => { stored[key] = value; }), retrieve: jest.fn(async (key) => stored[key] ?? null), delete: jest.fn(async (key) => { delete stored[key]; }), }, docker: null, asyncHandler: (fn) => async (req, res, next) => { try { await fn(req, res, next); } catch (e) { next(e); } }, ok: (res, body) => res.json({ success: true, ...body }), errorResponse: (res, code, msg) => res.status(code).json({ success: false, error: msg }), log: { info: jest.fn(), warn: jest.fn(), error: jest.fn() }, fetchT: jest.fn(), buildServiceUrl: (sub) => `http://127.0.0.1:${calPort}`, _stored: stored, _items: items, }; app = express(); app.use(express.json()); app.use('/api/v1/calendar', createRoutes(deps)); app.use((err, req, res, next) => res.status(err.statusCode || 500).json({ success: false, error: err.message })); } test('provision → source registered with fetchable credentials → events flow', async () => { // 1. The provisioner writes config + creds (pre-start contract) const { writeRadicaleConfig } = require('../../src/calendar/radicale-provisioner'); // Credentials come from the same generator the deploy hook uses const { generatePassword } = require('../../src/calendar/radicale-provisioner'); const pass = generatePassword(); // 2. Fake Radicale expects exactly those credentials const started = await startFakeCalDAV('dashcaddy', pass); calServer = started.server; calPort = started.port; // 3. Provision (config write + source registration) — same calls the deploy hook makes await writeRadicaleConfig(tmpConfigRoot, { user: 'dashcaddy', pass }); buildApp(); const provRes = await fetch(`http://127.0.0.1:${calPort}`, { method: 'OPTIONS' }); // warm the fake server expect(provRes.status).toBe(200); const reg = require('../../src/calendar/radicale-provisioner'); const source = await reg.registerAutoWiredSource({ calendarStateManager: deps.calendarStateManager, credentialManager: deps.credentialManager, subdomain: 'calendar', serviceUrl: `http://127.0.0.1:${calPort}`, creds: { user: 'dashcaddy', pass }, }); // 4. The source is registered and enabled expect(source.autoProvisioned).toBe(true); expect(deps._items).toHaveLength(1); // 5. THE CONTRACT: GET /events resolves the auto-wired source's stored // credentials and the fake Radicale accepts them (401 would mean the // key mismatch the judge caught). const server = http.createServer(app); await new Promise((r) => server.listen(0, '127.0.0.1', r)); try { const port = server.address().port; // Explicit window covering the fixture event (2030-01-01) const res = await fetch(`http://127.0.0.1:${port}/api/v1/calendar/events?start=2029-12-25T00:00:00Z&end=2030-01-10T00:00:00Z`); const body = await res.json(); expect(res.status).toBe(200); expect(body.perSource).toEqual([expect.objectContaining({ sourceId: source.id, ok: true })]); expect(body.events).toHaveLength(1); expect(body.events[0].title).toBe('Auto-wired dentist appointment'); // And the exact credential key the fetcher used is the one provisioner stored: expect(Object.keys(deps._stored)).toEqual([`calendar:source:${source.id}`]); } finally { await new Promise((r) => server.close(r)); } }); test('re-provisioning the same subdomain updates instead of duplicating', async () => { buildApp(); const reg = require('../../src/calendar/radicale-provisioner'); const first = await reg.registerAutoWiredSource({ calendarStateManager: deps.calendarStateManager, credentialManager: deps.credentialManager, subdomain: 'calendar', serviceUrl: 'https://calendar.sami', creds: { user: 'dashcaddy', pass: 'first-pass' }, }); const second = await reg.registerAutoWiredSource({ calendarStateManager: deps.calendarStateManager, credentialManager: deps.credentialManager, subdomain: 'calendar', serviceUrl: 'https://calendar.sami', creds: { user: 'dashcaddy', pass: 'second-pass' }, }); expect(second.id).toBe(first.id); expect(deps._items).toHaveLength(1); const saved = JSON.parse(deps._stored[`calendar:source:${first.id}`]); expect(saved.pass).toBe('second-pass'); }); });