import { applyD1Migrations, type D1Migration } from 'cloudflare:test'; import { env, exports as worker } from 'cloudflare:workers'; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { hashPassword } from '../src/worker/lib/password'; import { resolveFavicon } from '../src/worker/routes/monitors'; const ADMIN_PASSWORD = 'correct-horse-battery-staple'; const VALID_MONITOR = { name: 'Example', url: 'https://example.com/health', method: 'GET', expectedStatus: 200, intervalSeconds: 300, timeoutMs: 10_000, }; async function seedAdmin() { await env.DB.batch([ env.DB.prepare('DELETE FROM ai_events'), env.DB.prepare('DELETE FROM checks'), env.DB.prepare('DELETE FROM incident_updates'), env.DB.prepare('DELETE FROM incident_monitors'), env.DB.prepare('DELETE FROM incidents'), env.DB.prepare('DELETE FROM monitor_daily_stats'), env.DB.prepare('DELETE FROM notification_deliveries'), env.DB.prepare('DELETE FROM notification_channel_monitors'), env.DB.prepare('DELETE FROM notification_channels'), env.DB.prepare('DELETE FROM monitors'), env.DB.prepare('DELETE FROM login_attempts'), env.DB.prepare('DELETE FROM sessions'), env.DB.prepare('DELETE FROM admin_credentials'), ]); const now = Date.now(); await env.DB.prepare('INSERT INTO admin_credentials (id, password_hash, created_at, updated_at) VALUES (1, ?, ?, ?)') .bind(await hashPassword(ADMIN_PASSWORD), now, now) .run(); } async function authenticatedCookie() { const response = await worker.default.fetch('https://example.com/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', 'CF-Connecting-IP': '198.51.100.20', Origin: 'https://example.com', }, body: JSON.stringify({ password: ADMIN_PASSWORD }), }); return response.headers.get('Set-Cookie')?.split(';', 1)[0] ?? ''; } function apiFetch(path: string, method = 'GET', cookie = '', body?: unknown) { return worker.default.fetch(`https://example.com${path}`, { method, headers: { ...(cookie ? { Cookie: cookie } : {}), ...(method !== 'GET' ? { Origin: 'https://example.com' } : {}), ...(body === undefined ? {} : { 'Content-Type': 'application/json' }), }, body: body === undefined ? undefined : JSON.stringify(body), }); } async function createMonitor(cookie: string, overrides: Record = {}) { return apiFetch('/api/monitors', 'POST', cookie, { ...VALID_MONITOR, ...overrides }); } describe('monitor API', () => { beforeAll(async () => { const testEnv = env as Env & { TEST_MIGRATIONS: D1Migration[] }; await applyD1Migrations(testEnv.DB, testEnv.TEST_MIGRATIONS); }); beforeEach(seedAdmin); afterEach(() => vi.unstubAllGlobals()); it('protects every monitor endpoint, including the collection path without a trailing slash', async () => { const requests = [ apiFetch('/api/monitors'), apiFetch('/api/monitors/1/favicon'), apiFetch('/api/monitors', 'POST', '', VALID_MONITOR), apiFetch('/api/monitors/1', 'PATCH', '', { name: 'Changed' }), apiFetch('/api/monitors/1', 'DELETE'), apiFetch('/api/monitors/1/check', 'POST'), ]; const responses = await Promise.all(requests); for (const response of responses) { expect(response.status).toBe(401); expect(await response.json()).toEqual({ message: 'Authentication required' }); } }); it('proxies and caches a monitor favicon response', async () => { const cookie = await authenticatedCookie(); const created = await ( await createMonitor(cookie, { url: 'https://favicon-route.example.test/health' }) ).json<{ monitor: { id: number } }>(); vi.stubGlobal( 'fetch', vi.fn(async (input: RequestInfo | URL) => { const url = new URL(input.toString()); expect(url.href).toBe('https://favicon-route.example.test/favicon.ico'); return new Response(new Uint8Array([0, 0, 1, 0]), { headers: { 'Content-Type': 'image/x-icon' }, }); }), ); const response = await apiFetch(`/api/monitors/${created.monitor.id}/favicon`, 'GET', cookie); expect(response.status).toBe(200); expect(response.headers.get('Content-Type')).toBe('image/x-icon'); expect(response.headers.get('Cache-Control')).toBe('public, max-age=86400'); expect(response.headers.get('X-Content-Type-Options')).toBe('nosniff'); expect(new Uint8Array(await response.arrayBuffer())).toEqual(new Uint8Array([0, 0, 1, 0])); }); it('discovers a favicon declared in the website head', async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = new URL(input.toString()); if (url.pathname === '/favicon.ico') return new Response(null, { status: 404 }); if (url.pathname === '/') { return new Response('', { headers: { 'Content-Type': 'text/html; charset=utf-8' }, }); } return new Response(new Uint8Array([137, 80, 78, 71]), { headers: { 'Content-Type': 'image/png' }, }); }); vi.stubGlobal('fetch', fetchMock); const favicon = await resolveFavicon('https://favicon-head.example.test/status'); expect(favicon?.contentType).toBe('image/png'); expect(new Uint8Array(favicon?.body ?? new ArrayBuffer(0))).toEqual(new Uint8Array([137, 80, 78, 71])); expect(fetchMock).toHaveBeenCalledTimes(3); }); it('does not fetch favicons from private network hosts', async () => { const fetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); await expect(resolveFavicon('http://127.0.0.1/admin')).resolves.toBeNull(); await expect(resolveFavicon('http://[::1]/admin')).resolves.toBeNull(); await expect(resolveFavicon('http://10.0.0.1/admin')).resolves.toBeNull(); expect(fetchMock).not.toHaveBeenCalled(); }); it('creates a valid monitor and returns it in the list', async () => { const cookie = await authenticatedCookie(); const response = await createMonitor(cookie); const created = await response.json<{ monitor: { id: number; name: string; enabled: boolean; alertsEnabled: boolean; retryCount: number; failureThreshold: number; }; }>(); expect(response.status).toBe(200); expect(created.monitor).toMatchObject({ name: 'Example', enabled: true, alertsEnabled: true, retryCount: 1, failureThreshold: 2, }); const listResponse = await apiFetch('/api/monitors', 'GET', cookie); const list = await listResponse.json<{ monitors: Array<{ id: number; url: string }> }>(); expect(list.monitors).toHaveLength(1); expect(list.monitors[0]).toMatchObject({ id: created.monitor.id, url: 'https://example.com/health' }); }); it('jitters a new monitor across its first interval', async () => { vi.spyOn(Math, 'random').mockReturnValue(0.5); const before = Date.now(); const created = await ( await createMonitor(await authenticatedCookie(), { intervalSeconds: 600 }) ).json<{ monitor: { lastCheckedAt: string }; }>(); const lastCheckedAt = Date.parse(created.monitor.lastCheckedAt); expect(lastCheckedAt).toBeGreaterThanOrEqual(before - 301_000); expect(lastCheckedAt).toBeLessThanOrEqual(Date.now() - 299_000); }); it.each([ [{ url: 'file:///etc/passwd' }, 'Enter a valid http or https URL'], [{ intervalSeconds: 60 }, 'intervalSeconds must be an integer between 300 and 86400'], [{ expectedStatus: 99 }, 'expectedStatus must be an integer between 100 and 599'], [{ retryCount: -1 }, 'retryCount must be an integer between 0 and 3'], [{ retryCount: 4 }, 'retryCount must be an integer between 0 and 3'], [{ failureThreshold: 0 }, 'failureThreshold must be an integer between 1 and 10'], [{ failureThreshold: 11 }, 'failureThreshold must be an integer between 1 and 10'], [{ requestHeaders: { Authorization: 'Bearer ok\r\nX-Evil: true' } }, 'Invalid value for header: Authorization'], [{ requestHeaders: { Host: 'example.test' } }, 'Header is not allowed: Host'], [ { requestHeaders: Object.fromEntries(Array.from({ length: 11 }, (_, index) => [`X-${index}`, 'value'])) }, 'requestHeaders cannot contain more than 10 headers', ], [{ requestBody: '{}' }, 'requestBody can only be used with POST monitors'], [{ degradedLatencyMs: 0 }, 'degradedLatencyMs must be an integer between 1 and 30000'], [{ degradedLatencyMs: 30_001 }, 'degradedLatencyMs must be an integer between 1 and 30000'], ] as const)('rejects invalid monitor input %o', async (overrides, message) => { const response = await createMonitor(await authenticatedCookie(), overrides); expect(response.status).toBe(400); expect(await response.json()).toEqual({ message }); }); it('patches only the requested fields', async () => { const cookie = await authenticatedCookie(); const created = await (await createMonitor(cookie)).json<{ monitor: { id: number } }>(); const response = await apiFetch(`/api/monitors/${created.monitor.id}`, 'PATCH', cookie, { name: 'Renamed endpoint', enabled: false, retryCount: 3, failureThreshold: 4, }); const body = await response.json<{ monitor: { name: string; url: string; enabled: boolean; retryCount: number; failureThreshold: number }; }>(); expect(response.status).toBe(200); expect(body.monitor).toMatchObject({ name: 'Renamed endpoint', url: 'https://example.com/health', enabled: false, retryCount: 3, failureThreshold: 4, }); }); it('stores advanced settings and allows nullable fields to be cleared', async () => { const cookie = await authenticatedCookie(); const created = await ( await createMonitor(cookie, { method: 'POST', expectKeyword: 'healthy', keywordInverted: true, requestHeaders: { Authorization: 'Bearer test' }, requestBody: '{"probe":true}', degradedLatencyMs: 1500, }) ).json<{ monitor: { id: number; requestHeaders: string } }>(); expect(JSON.parse(created.monitor.requestHeaders)).toEqual({ Authorization: 'Bearer test' }); const response = await apiFetch(`/api/monitors/${created.monitor.id}`, 'PATCH', cookie, { expectKeyword: null, requestHeaders: null, requestBody: null, degradedLatencyMs: null, }); const body = await response.json<{ monitor: { expectKeyword: null; requestHeaders: null; requestBody: null; degradedLatencyMs: null }; }>(); expect(body.monitor).toMatchObject({ expectKeyword: null, requestHeaders: null, requestBody: null, degradedLatencyMs: null }); }); it('does not allow clients to set the internal failure counter', async () => { const cookie = await authenticatedCookie(); const created = await (await createMonitor(cookie)).json<{ monitor: { id: number } }>(); const response = await apiFetch(`/api/monitors/${created.monitor.id}`, 'PATCH', cookie, { consecutiveFailures: 99 }); expect(response.status).toBe(400); expect(await response.json()).toEqual({ message: 'Provide at least one field to update' }); }); it('returns a pending transition without opening an incident on the first manual failure', async () => { const cookie = await authenticatedCookie(); const created = await (await createMonitor(cookie, { retryCount: 0 })).json<{ monitor: { id: number } }>(); vi.stubGlobal( 'fetch', vi.fn(async () => new Response(null, { status: 500 })), ); const response = await apiFetch(`/api/monitors/${created.monitor.id}/check`, 'POST', cookie); const body = await response.json<{ transition: string; result: { attempts: number }; monitor: { lastOk: boolean | null; consecutiveFailures: number }; }>(); expect(body).toMatchObject({ transition: 'pending', result: { attempts: 1 }, monitor: { lastOk: null, consecutiveFailures: 1 }, }); const incident = await env.DB.prepare('SELECT COUNT(*) AS count FROM incidents').first<{ count: number }>(); expect(incident?.count).toBe(0); }); it('opens an incident when a manual check reaches the confirmation threshold', async () => { const cookie = await authenticatedCookie(); const created = await ( await createMonitor(cookie, { retryCount: 0, failureThreshold: 2 }) ).json<{ monitor: { id: number }; }>(); vi.stubGlobal( 'fetch', vi.fn(async () => new Response(null, { status: 500 })), ); const first = await apiFetch(`/api/monitors/${created.monitor.id}/check`, 'POST', cookie); const second = await apiFetch(`/api/monitors/${created.monitor.id}/check`, 'POST', cookie); expect((await first.json<{ transition: string }>()).transition).toBe('pending'); expect((await second.json<{ transition: string }>()).transition).toBe('opened'); const incident = await env.DB.prepare('SELECT COUNT(*) AS count FROM incident_monitors WHERE monitor_id = ?') .bind(created.monitor.id) .first<{ count: number }>(); expect(incident?.count).toBe(1); }); it('retries a manual check and returns the attempt count', async () => { const cookie = await authenticatedCookie(); const created = await (await createMonitor(cookie, { retryCount: 1 })).json<{ monitor: { id: number } }>(); const fetchMock = vi .fn() .mockResolvedValueOnce(new Response(null, { status: 500 })) .mockResolvedValueOnce(new Response(null, { status: 200 })); vi.stubGlobal('fetch', fetchMock); const response = await apiFetch(`/api/monitors/${created.monitor.id}/check`, 'POST', cookie); const body = await response.json<{ transition: null; result: { ok: boolean; attempts: number } }>(); expect(body).toMatchObject({ transition: null, result: { ok: true, attempts: 2 } }); expect(fetchMock).toHaveBeenCalledTimes(2); }); it('returns detail, checks, incidents, and raw stats', async () => { const cookie = await authenticatedCookie(); const created = await (await createMonitor(cookie)).json<{ monitor: { id: number } }>(); const id = created.monitor.id; const now = Date.now(); await env.DB.batch([ env.DB.prepare('INSERT INTO checks (monitor_id, ok, status_code, latency_ms, checked_at) VALUES (?, 1, 200, 100, ?)').bind( id, now - 2000, ), env.DB.prepare('INSERT INTO checks (monitor_id, ok, status_code, latency_ms, checked_at) VALUES (?, 0, 500, 300, ?)').bind( id, now - 1000, ), env.DB.prepare( "INSERT INTO incidents (id, status, impact, source, started_at, start_status_code, start_error, created_at, updated_at) VALUES (100, 'investigating', 'major', 'auto', ?, 500, 'Down', ?, ?)", ).bind(now - 1000, now - 1000, now - 1000), env.DB.prepare('INSERT INTO incident_monitors (incident_id, monitor_id) VALUES (100, ?)').bind(id), ]); const [detail, checksResponse, incidentsResponse, statsResponse] = await Promise.all([ apiFetch(`/api/monitors/${id}`, 'GET', cookie), apiFetch(`/api/monitors/${id}/checks`, 'GET', cookie), apiFetch(`/api/monitors/${id}/incidents`, 'GET', cookie), apiFetch(`/api/monitors/${id}/stats`, 'GET', cookie), ]); expect((await detail.json<{ monitor: { id: number } }>()).monitor.id).toBe(id); expect((await checksResponse.json<{ checks: unknown[] }>()).checks).toHaveLength(2); expect((await incidentsResponse.json<{ incidents: unknown[] }>()).incidents).toHaveLength(1); const stats = await statsResponse.json<{ windows: { '24h': { uptimePct: number; totalChecks: number; upChecks: number; avgLatencyMs: number; incidentCount: number } }; }>(); expect(stats.windows['24h']).toEqual({ uptimePct: 50, totalChecks: 2, upChecks: 1, avgLatencyMs: 200, incidentCount: 1 }); }); it('combines daily rollups with the current partial day for seven-day and long-range stats', async () => { const cookie = await authenticatedCookie(); const created = await (await createMonitor(cookie)).json<{ monitor: { id: number } }>(); const id = created.monitor.id; const now = new Date(); const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()); await env.DB.batch([ env.DB.prepare( 'INSERT INTO monitor_daily_stats (monitor_id, day, total_checks, up_checks, avg_latency_ms, min_latency_ms, max_latency_ms) VALUES (?, ?, 8, 6, 100, 50, 150)', ).bind(id, today - 24 * 60 * 60 * 1000), env.DB.prepare('INSERT INTO checks (monitor_id, ok, status_code, latency_ms, checked_at) VALUES (?, 1, 200, 200, ?)').bind( id, today + 1000, ), env.DB.prepare('INSERT INTO checks (monitor_id, ok, status_code, latency_ms, checked_at) VALUES (?, 1, 200, 200, ?)').bind( id, today + 2000, ), ]); const response = await apiFetch(`/api/monitors/${id}/stats`, 'GET', cookie); const stats = await response.json<{ windows: Record<'7d' | '30d', { uptimePct: number; totalChecks: number; upChecks: number; avgLatencyMs: number }>; }>(); expect(stats.windows['7d']).toMatchObject({ uptimePct: 80, totalChecks: 10, upChecks: 8, avgLatencyMs: 120, }); expect(stats.windows['30d']).toMatchObject({ uptimePct: 80, totalChecks: 10, upChecks: 8, avgLatencyMs: 120, }); }); it('deletes the monitor and its check records explicitly', async () => { const cookie = await authenticatedCookie(); const created = await (await createMonitor(cookie)).json<{ monitor: { id: number } }>(); await env.DB.prepare('INSERT INTO checks (monitor_id, ok, status_code, latency_ms, checked_at) VALUES (?, 1, 200, 12, ?)') .bind(created.monitor.id, Date.now()) .run(); const response = await apiFetch(`/api/monitors/${created.monitor.id}`, 'DELETE', cookie); expect(response.status).toBe(200); expect(await response.json()).toEqual({ ok: true }); const monitorCount = await env.DB.prepare('SELECT COUNT(*) AS count FROM monitors').first<{ count: number }>(); const checkCount = await env.DB.prepare('SELECT COUNT(*) AS count FROM checks').first<{ count: number }>(); expect(monitorCount?.count).toBe(0); expect(checkCount?.count).toBe(0); }); });