import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { verifyHCaptcha } from '../src/integrations/hcaptcha.js'; import { sendNtfyNotification } from '../src/integrations/ntfy.js'; import type { NtfyConfig, StoredRequest } from '../src/types/index.js'; describe('verifyHCaptcha', () => { beforeEach(() => { vi.stubGlobal('fetch', vi.fn()); }); afterEach(() => { vi.unstubAllGlobals(); }); it('returns success boolean when response is ok', async () => { const fetchMock = vi.mocked(fetch); fetchMock.mockResolvedValue({ ok: true, statusText: 'OK', json: async () => ({ success: true }), } as Response); const result = await verifyHCaptcha('token', 'secret'); expect(result.left().present()).toBe(false); expect(result.right().get()).toBe(true); expect(fetchMock).toHaveBeenCalledTimes(1); const [url, init] = fetchMock.mock.calls[0]!; expect(url).toBe('https://hcaptcha.com/siteverify'); expect(init?.method).toBe('POST'); expect(init?.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' }); expect(init?.body).toBeInstanceOf(URLSearchParams); expect((init?.body as URLSearchParams).get('secret')).toBe('secret'); expect((init?.body as URLSearchParams).get('response')).toBe('token'); }); it('returns error when response is not ok', async () => { const fetchMock = vi.mocked(fetch); fetchMock.mockResolvedValue({ ok: false, statusText: 'Bad Request', } as Response); const result = await verifyHCaptcha('token', 'secret'); expect(result.left().present()).toBe(true); expect(result.left().get().message).toBe('hCaptcha verification failed: Bad Request'); }); }); describe('sendNtfyNotification', () => { beforeEach(() => { vi.stubGlobal('fetch', vi.fn()); }); afterEach(() => { vi.unstubAllGlobals(); }); it('is a no-op when not enabled or misconfigured', async () => { const fetchMock = vi.mocked(fetch); const config: NtfyConfig = { enabled: false }; const request: StoredRequest = { timestamp: 1, uuid: 'uuid', routeName: 'route1', method: 'POST', headers: {}, body: {}, }; const result = await sendNtfyNotification(config, request); expect(result.left().present()).toBe(false); expect(fetchMock).not.toHaveBeenCalled(); }); it('posts a notification to the configured server/topic', async () => { const fetchMock = vi.mocked(fetch); fetchMock.mockResolvedValue({ ok: true, statusText: 'OK' } as Response); const config: NtfyConfig = { enabled: true, server: 'https://ntfy.example.com', topic: 'topic1' }; const request: StoredRequest = { timestamp: Date.parse('2020-01-01T00:00:00.000Z'), uuid: 'uuid', routeName: 'route1', method: 'POST', headers: {}, body: {}, }; const result = await sendNtfyNotification(config, request); expect(result.left().present()).toBe(false); expect(fetchMock).toHaveBeenCalledTimes(1); const [url, init] = fetchMock.mock.calls[0]!; expect(url).toBe('https://ntfy.example.com/topic1'); expect(init?.method).toBe('POST'); expect(init?.headers).toEqual({ Title: 'Webhook received: route1', Tags: 'webhook,posthook', Priority: '3', }); expect(init?.body).toContain('Method: POST'); expect(init?.body).toContain('Timestamp: 2020-01-01T00:00:00.000Z'); expect(init?.body).toContain('UUID: uuid'); }); it('returns an error when ntfy responds with non-2xx', async () => { const fetchMock = vi.mocked(fetch); fetchMock.mockResolvedValue({ ok: false, statusText: 'Unauthorized' } as Response); const config: NtfyConfig = { enabled: true, server: 'https://ntfy.example.com', topic: 'topic1' }; const request: StoredRequest = { timestamp: 1, uuid: 'uuid', routeName: 'route1', method: 'POST', headers: {}, body: {}, }; const result = await sendNtfyNotification(config, request); expect(result.left().present()).toBe(true); expect(result.left().get().message).toBe('ntfy notification failed: Unauthorized'); }); });