Monitor websites uptime using Cloudflare Workers
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447import argon2 from 'argon2';import { PgDialect } from 'drizzle-orm/pg-core';import { afterEach, describe, expect, it, vi } from 'vitest';
import { buildApi } from './server.js';
const serviceId = '20000000-0000-4000-8000-000000000001';const admin = { id: '10000000-0000-4000-8000-000000000001', email: 'admin@example.com' };const apps: Awaited<ReturnType<typeof buildApi>>[] = [];
afterEach(async () => { await Promise.all(apps.splice(0).map((app) => app.close()));});
describe('notification services', () => { it('requires authentication', async () => { const app = await makeApp([[]]); const response = await app.inject({ method: 'GET', url: '/api/notification-services' }); expect(response.statusCode).toBe(401); });
it('lists services without returning provider credentials', async () => { const app = await makeApp([ [], [{ ...admin, passwordHash: await passwordHash() }], [], [admin], [ { id: serviceId, name: 'Operations Telegram', provider: 'telegram', enabled: true, config: { botToken: '123456:abcdefghijklmnopqrstuvwxyz', chatId: '-100123456' }, createdAt: '2026-09-16T10:00:00.000Z', updatedAt: '2026-09-16T10:00:00.000Z', }, ], ]); const cookie = await login(app); const response = await app.inject({ method: 'GET', url: '/api/notification-services', headers: { cookie }, });
expect(response.statusCode).toBe(200); expect(response.json()).toEqual({ services: [ { id: serviceId, name: 'Operations Telegram', provider: 'telegram', enabled: true, config: { chatId: '-100123456' }, createdAt: '2026-09-16T10:00:00.000Z', updatedAt: '2026-09-16T10:00:00.000Z', }, ], }); expect(response.body).not.toContain('botToken'); expect(response.body).not.toContain('abcdefghijklmnopqrstuvwxyz'); });
it('returns safe fields and redacts credentials for every additional provider', async () => { const base = { enabled: true, createdAt: '2026-09-16T10:00:00.000Z', updatedAt: '2026-09-16T10:00:00.000Z', }; const services = [ { ...base, id: '20000000-0000-4000-8000-000000000002', name: 'Resend', provider: 'resend', config: { apiKey: 're_secret', from: 'uptime@example.com', to: ['ops@example.com'], subject: 'Alert', }, }, { ...base, id: '20000000-0000-4000-8000-000000000003', name: 'Gotify', provider: 'gotify', config: { serverUrl: 'https://push.example.com', applicationToken: 'gotify-secret', priority: 8, }, }, { ...base, id: '20000000-0000-4000-8000-000000000004', name: 'Webhook', provider: 'webhook', config: { webhookUrl: 'https://hooks.example.com/secret', bearerToken: 'bearer-secret' }, }, { ...base, id: '20000000-0000-4000-8000-000000000005', name: 'SMTP', provider: 'smtp', config: { host: 'smtp.example.com', port: 587, security: 'starttls', username: 'uptime', password: 'smtp-secret', from: 'uptime@example.com', to: ['ops@example.com'], subject: 'Alert', }, }, { ...base, id: '20000000-0000-4000-8000-000000000006', name: 'Home Assistant', provider: 'home-assistant', config: { serverUrl: 'http://homeassistant.local:8123', accessToken: 'ha-secret', service: 'notify', }, }, ]; const app = await makeApp([ [], [{ ...admin, passwordHash: await passwordHash() }], [], [admin], services, ]); const cookie = await login(app); const response = await app.inject({ method: 'GET', url: '/api/notification-services', headers: { cookie }, });
expect(response.statusCode).toBe(200); expect(response.json().services.map((service: { config: unknown }) => service.config)).toEqual([ { from: 'uptime@example.com', to: ['ops@example.com'], subject: 'Alert' }, { serverUrl: 'https://push.example.com', priority: 8 }, {}, { host: 'smtp.example.com', port: 587, security: 'starttls', username: 'uptime', from: 'uptime@example.com', to: ['ops@example.com'], subject: 'Alert', }, { serverUrl: 'http://homeassistant.local:8123', service: 'notify' }, ]); for (const secret of [ 're_secret', 'gotify-secret', 'hooks.example.com', 'bearer-secret', 'smtp-secret', 'ha-secret', ]) { expect(response.body).not.toContain(secret); } });
it('creates a service while keeping its credential write-only', async () => { const botToken = '123456:abcdefghijklmnopqrstuvwxyz'; const created = { id: serviceId, name: 'Operations Telegram', provider: 'telegram', enabled: true, config: { botToken, chatId: '-100123456' }, createdAt: '2026-09-16T10:00:00.000Z', updatedAt: '2026-09-16T10:00:00.000Z', }; const app = await makeApp([ [], [{ ...admin, passwordHash: await passwordHash() }], [], [admin], [created], ]); const cookie = await login(app); const response = await app.inject({ method: 'POST', url: '/api/notification-services', headers: { cookie }, payload: { name: created.name, provider: created.provider, enabled: true, config: { botToken, chatId: created.config.chatId }, }, });
expect(response.statusCode).toBe(201); expect(response.json().service.config).toEqual({ chatId: '-100123456' }); expect(response.body).not.toContain(botToken); });
it('preserves a saved secret when an edit submits a blank secret', async () => { const botToken = '123456:abcdefghijklmnopqrstuvwxyz'; const current = { id: serviceId, name: 'Operations Telegram', provider: 'telegram', enabled: true, config: { botToken, chatId: '-100123456' }, createdAt: '2026-09-16T10:00:00.000Z', updatedAt: '2026-09-16T10:00:00.000Z', }; const compiledQueries: Array<{ sql: string; params: unknown[] }> = []; const responses: unknown[][] = [ [], [{ ...admin, passwordHash: await passwordHash() }], [], [admin], [current], [{ ...current, name: 'Primary Telegram' }], ]; const database = { execute: async (query: unknown) => { compiledQueries.push(new PgDialect().sqlToQuery(query as never)); return { rows: responses.shift() ?? [] }; }, transaction: async (run: (tx: unknown) => Promise<unknown>) => run(database), }; const app = await buildTestApi(database); const cookie = await login(app); const response = await app.inject({ method: 'PATCH', url: `/api/notification-services/${serviceId}`, headers: { cookie }, payload: { name: 'Primary Telegram', config: { botToken: '', chatId: '-100123456' }, }, });
expect(response.statusCode).toBe(200); const update = compiledQueries.find((query) => query.sql.includes('update notification_services'), ); expect( compiledQueries.some( (query) => query.sql.includes('from notification_services where id =') && query.sql.includes('for update'), ), ).toBe(true); expect(update?.params).toContain(JSON.stringify(current.config)); expect( compiledQueries.some((query) => query.sql.includes('update notification_deliveries')), ).toBe(false); expect( compiledQueries.some((query) => query.sql.includes('update monitor_notification_state')), ).toBe(false); expect(response.body).not.toContain(botToken); });
it('preserves new-provider secrets and rejects unrelated update keys', async () => { const current = { id: serviceId, name: 'Gotify', provider: 'gotify', enabled: true, config: { serverUrl: 'https://push.example.com', applicationToken: 'gotify-secret', priority: 8, }, createdAt: '2026-09-16T10:00:00.000Z', updatedAt: '2026-09-16T10:00:00.000Z', }; const updated = { ...current, config: { ...current.config, priority: 9 } }; const compiledQueries: Array<{ sql: string; params: unknown[] }> = []; const responses: unknown[][] = [ [], [{ ...admin, passwordHash: await passwordHash() }], [], [admin], [current], [updated], [], [], [admin], [updated], ]; const database = { execute: async (query: unknown) => { compiledQueries.push(new PgDialect().sqlToQuery(query as never)); return { rows: responses.shift() ?? [] }; }, transaction: async (run: (tx: unknown) => Promise<unknown>) => run(database), }; const app = await buildTestApi(database); const cookie = await login(app); const updateResponse = await app.inject({ method: 'PATCH', url: `/api/notification-services/${serviceId}`, headers: { cookie }, payload: { config: { applicationToken: '', priority: 9 } }, });
expect(updateResponse.statusCode).toBe(200); const updateQuery = compiledQueries.find((query) => query.sql.includes('update notification_services'), ); expect(updateQuery?.params).toContain(JSON.stringify(updated.config)); expect(updateResponse.body).not.toContain('gotify-secret');
const unrelatedResponse = await app.inject({ method: 'PATCH', url: `/api/notification-services/${serviceId}`, headers: { cookie }, payload: { config: { chatId: '123456' } }, }); expect(unrelatedResponse.statusCode).toBe(400); expect(unrelatedResponse.json().error.code).toBe('validation_error'); });
it('rejects invalid provider configuration without echoing the credential', async () => { const app = await makeApp([ [], [{ ...admin, passwordHash: await passwordHash() }], [], [admin], ]); const cookie = await login(app); const invalidUrl = 'http://example.com/not-a-webhook'; const response = await app.inject({ method: 'POST', url: '/api/notification-services', headers: { cookie }, payload: { name: 'Invalid Discord', provider: 'discord', config: { webhookUrl: invalidUrl }, }, });
expect(response.statusCode).toBe(400); expect(response.json().error.code).toBe('validation_error'); expect(response.body).not.toContain(invalidUrl); });
it('sends a rate-limited test through the configured provider', async () => { const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(new Response(null, { status: 204 })); const webhookUrl = 'https://discord.com/api/webhooks/123456/secret-token'; const app = await makeApp( [ [], [{ ...admin, passwordHash: await passwordHash() }], [], [admin], [ { id: serviceId, name: 'Operations Discord', provider: 'discord', enabled: true, config: { webhookUrl }, createdAt: '2026-09-16T10:00:00.000Z', updatedAt: '2026-09-16T10:00:00.000Z', }, ], ], fetchMock, ); const cookie = await login(app); const response = await app.inject({ method: 'POST', url: `/api/notification-services/${serviceId}/test`, headers: { cookie }, });
expect(response.statusCode).toBe(200); expect(response.json()).toEqual({ success: true }); expect(fetchMock).toHaveBeenCalledOnce(); expect(fetchMock.mock.calls[0]?.[0]).toBe(`${webhookUrl}?wait=true`); expect(String(fetchMock.mock.calls[0]?.[1]?.body)).toContain('Uptime notification test'); });});
async function makeApp(responses: unknown[][], notificationFetch?: typeof fetch) { const hash = responses.length > 1 ? String((responses[1]?.[0] as { passwordHash?: string })?.passwordHash) : await passwordHash(); const database = { execute: async () => ({ rows: responses.shift() ?? [] }), transaction: async (run: (tx: unknown) => Promise<unknown>) => run(database), }; const app = await buildApi( { DATABASE_URL: 'postgresql://uptime:uptime@localhost:5432/uptime', SESSION_COOKIE_SECURE: false, ADMIN_EMAIL: 'admin@example.com', ADMIN_PASSWORD_HASH: hash, SESSION_SECRET: 'a'.repeat(32), }, { db: database as never, now: () => new Date('2026-09-16T12:00:00.000Z'), ...(notificationFetch ? { notificationFetch } : {}), }, ); apps.push(app); return app;}
async function buildTestApi(database: object) { const app = await buildApi( { DATABASE_URL: 'postgresql://uptime:uptime@localhost:5432/uptime', SESSION_COOKIE_SECURE: false, ADMIN_EMAIL: 'admin@example.com', ADMIN_PASSWORD_HASH: await passwordHash(), SESSION_SECRET: 'a'.repeat(32), }, { db: database as never, now: () => new Date('2026-09-16T12:00:00.000Z') }, ); apps.push(app); return app;}
async function login(app: Awaited<ReturnType<typeof buildApi>>) { const response = await app.inject({ method: 'POST', url: '/api/auth/login', payload: { password: 'correct horse battery staple' }, }); const setCookie = response.headers['set-cookie']; return (Array.isArray(setCookie) ? setCookie[0] : setCookie)?.split(';')[0] ?? '';}
function passwordHash() { return argon2.hash('correct horse battery staple', { type: argon2.argon2id });}