From c9d6c3623b4e97490abfb654d009743d4d5afe16 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Thu, 19 Feb 2026 13:59:23 -0500 Subject: [PATCH] Add missing unit tests for check runner GET handler and edge cases Tests for the GET handler (Vercel cron), sites query failure, successful checks not creating incidents, and Promise.allSettled rejection handling. Co-Authored-By: Claude Opus 4.6 --- src/__tests__/check-runner.test.ts | 138 ++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 1 deletion(-) diff --git a/src/__tests__/check-runner.test.ts b/src/__tests__/check-runner.test.ts index 72df601..98bc7cd 100644 --- a/src/__tests__/check-runner.test.ts +++ b/src/__tests__/check-runner.test.ts @@ -20,7 +20,7 @@ vi.mock('@/lib/supabase/admin', () => ({ createAdminClient: () => mockSupabase, })) -import { POST } from '@/app/api/checks/run/route' +import { GET, POST } from '@/app/api/checks/run/route' import { checkSite } from '@/lib/checker' describe('POST /api/checks/run', () => { @@ -49,6 +49,25 @@ describe('POST /api/checks/run', () => { expect(response.status).toBe(401) }) + test('returns 500 when sites query fails', async () => { + mockFrom.mockReturnValue({ + select: vi.fn().mockReturnValue({ + data: null, + error: { message: 'connection error' }, + }), + }) + + const request = new NextRequest('http://localhost/api/checks/run', { + method: 'POST', + headers: { authorization: 'Bearer test-secret' }, + }) + + const response = await POST(request) + expect(response.status).toBe(500) + const body = await response.json() + expect(body.error).toBe('Failed to fetch sites') + }) + test('returns empty result when no sites exist', async () => { mockFrom.mockReturnValue({ select: vi.fn().mockReturnValue({ @@ -137,6 +156,88 @@ describe('POST /api/checks/run', () => { }) }) + test('records checks without creating incidents for successful checks', async () => { + const site = { id: 'site-1', name: 'Test', url: 'https://example.com' } + const mockInsert = vi.fn().mockReturnValue({ + select: () => ({ + single: () => ({ + data: { id: 'check-1', site_id: 'site-1', status: 'success' }, + }), + }), + }) + + mockFrom.mockImplementation((table: string) => { + if (table === 'sites') { + return { select: () => ({ data: [site], error: null }) } + } + if (table === 'checks') { + return { insert: mockInsert } + } + return {} + }) + + vi.mocked(checkSite).mockResolvedValue({ + status: 'success', + statusCode: 200, + error: null, + }) + + const request = new NextRequest('http://localhost/api/checks/run', { + method: 'POST', + headers: { authorization: 'Bearer test-secret' }, + }) + + const response = await POST(request) + const body = await response.json() + expect(body.checks).toBe(1) + expect(body.incidents).toBe(0) + expect(mockInsert).toHaveBeenCalledWith( + expect.objectContaining({ status: 'success', status_code: 200 }) + ) + expect(mockSendIncidentAlert).not.toHaveBeenCalled() + }) + + test('skips rejected promises from Promise.allSettled', async () => { + const site1 = { id: 'site-1', name: 'Test1', url: 'https://example.com' } + const site2 = { id: 'site-2', name: 'Test2', url: 'https://example2.com' } + + mockFrom.mockImplementation((table: string) => { + if (table === 'sites') { + return { select: () => ({ data: [site1, site2], error: null }) } + } + if (table === 'checks') { + return { + insert: () => ({ + select: () => ({ + single: () => ({ + data: { id: 'check-1', site_id: 'site-2', status: 'success' }, + }), + }), + }), + } + } + return {} + }) + + vi.mocked(checkSite) + .mockRejectedValueOnce(new Error('DNS resolution failed')) + .mockResolvedValueOnce({ + status: 'success', + statusCode: 200, + error: null, + }) + + const request = new NextRequest('http://localhost/api/checks/run', { + method: 'POST', + headers: { authorization: 'Bearer test-secret' }, + }) + + const response = await POST(request) + const body = await response.json() + expect(body.checks).toBe(2) + expect(body.incidents).toBe(0) + }) + test('does not send email for existing open incident', async () => { const site = { id: 'site-1', name: 'Test', url: 'https://example.com' } @@ -188,3 +289,38 @@ describe('POST /api/checks/run', () => { expect(mockSendIncidentAlert).not.toHaveBeenCalled() }) }) + +describe('GET /api/checks/run (Vercel cron)', () => { + beforeEach(() => { + vi.clearAllMocks() + process.env.CRON_SECRET = 'test-secret' + }) + + test('GET handler works identically to POST (used by Vercel cron)', async () => { + mockFrom.mockReturnValue({ + select: vi.fn().mockReturnValue({ + data: [], + error: null, + }), + }) + + const request = new NextRequest('http://localhost/api/checks/run', { + method: 'GET', + headers: { authorization: 'Bearer test-secret' }, + }) + + const response = await GET(request) + const body = await response.json() + expect(body).toEqual({ message: 'No sites to check', checks: 0, incidents: 0 }) + }) + + test('GET handler rejects invalid auth', async () => { + const request = new NextRequest('http://localhost/api/checks/run', { + method: 'GET', + headers: { authorization: 'Bearer wrong' }, + }) + + const response = await GET(request) + expect(response.status).toBe(401) + }) +})