@@ -5,12 +5,15 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
55import type { APIRoute } from 'astro'
66import { GET as cleanupDsar } from '@pages/api/cron/cleanup-dsar-requests'
77import { GET as cleanupConfirmations } from '@pages/api/cron/cleanup-confirmations'
8+ import { GET as pingIntegrations } from '@pages/api/cron/ping-integrations'
89
910const supabaseAdminMock = vi . hoisted ( ( ) => ( {
1011 from : vi . fn ( ) ,
1112} ) )
1213
1314const getCronSecretMock = vi . hoisted ( ( ) => vi . fn ( ( ) => 'cron-secret' ) )
15+ const getUpstashApiUrlMock = vi . hoisted ( ( ) => vi . fn ( ( ) => 'https://example.upstash.io' ) )
16+ const getUpstashApiTokenMock = vi . hoisted ( ( ) => vi . fn ( ( ) => 'upstash-token' ) )
1417
1518vi . mock ( '@pages/api/_utils' , ( ) => ( {
1619 supabaseAdmin : supabaseAdminMock ,
@@ -23,6 +26,8 @@ vi.mock('@pages/api/_environment/environmentApi', async () => {
2326 return {
2427 ...actual ,
2528 getCronSecret : getCronSecretMock ,
29+ getUpstashApiUrl : getUpstashApiUrlMock ,
30+ getUpstashApiToken : getUpstashApiTokenMock ,
2631 }
2732} )
2833
@@ -58,6 +63,30 @@ const createDeleteChain = (options?: { data?: unknown[]; error?: unknown }): Del
5863 return chain
5964}
6065
66+ type SelectChain = {
67+ select : ( ...args : unknown [ ] ) => SelectChain
68+ limit : ( ...args : unknown [ ] ) => Promise < { data ?: unknown [ ] ; error ?: unknown ; count ?: number } >
69+ }
70+
71+ const createSelectChain = ( options ?: {
72+ data ?: unknown [ ]
73+ error ?: unknown
74+ count ?: number
75+ } ) : SelectChain => {
76+ const response = {
77+ data : options ?. data ?? [ ] ,
78+ error : options ?. error ?? null ,
79+ count : options ?. count ?? options ?. data ?. length ?? 0 ,
80+ }
81+
82+ const chain : SelectChain = {
83+ select : ( ) => chain ,
84+ limit : ( ) => Promise . resolve ( { ...response } ) ,
85+ }
86+
87+ return chain
88+ }
89+
6190describe ( 'Cron cleanup endpoints' , ( ) => {
6291 let warnSpy : ReturnType < typeof vi . spyOn >
6392 let logSpy : ReturnType < typeof vi . spyOn >
@@ -183,4 +212,115 @@ describe('Cron cleanup endpoints', () => {
183212 expect ( body . error . code ) . toBe ( 'CRON_DELETE_CONFIRMED_TOKENS_FAILED' )
184213 } )
185214 } )
215+
216+ describe ( 'ping-integrations' , ( ) => {
217+ const run = ( request : Request ) =>
218+ pingIntegrations ( buildContext ( request ) as unknown as Parameters < APIRoute > [ 0 ] )
219+
220+ let fetchMock : ReturnType < typeof vi . fn >
221+
222+ const buildRequest = ( headers ?: HeadersInit ) =>
223+ new Request ( 'http://localhost/api/cron/ping-integrations' , {
224+ method : 'GET' ,
225+ headers,
226+ } )
227+
228+ const createFetchResponse = ( overrides ?: Partial < Response > ) : Response => ( {
229+ ok : true ,
230+ status : 200 ,
231+ json : vi . fn ( ) . mockResolvedValue ( { result : null } ) ,
232+ text : vi . fn ( ) . mockResolvedValue ( '' ) ,
233+ headers : new Headers ( ) ,
234+ redirected : false ,
235+ statusText : 'OK' ,
236+ type : 'basic' ,
237+ url : getUpstashApiUrlMock ( ) ,
238+ clone : vi . fn ( ( ) => createFetchResponse ( overrides ) ) ,
239+ body : null ,
240+ bodyUsed : false ,
241+ arrayBuffer : vi . fn ( ) ,
242+ blob : vi . fn ( ) ,
243+ formData : vi . fn ( ) ,
244+ ...overrides ,
245+ } ) as unknown as Response
246+
247+ beforeEach ( ( ) => {
248+ fetchMock = vi . fn ( )
249+ vi . stubGlobal ( 'fetch' , fetchMock )
250+ } )
251+
252+ afterEach ( ( ) => {
253+ vi . unstubAllGlobals ( )
254+ } )
255+
256+ it ( 'rejects requests without valid secret' , async ( ) => {
257+ const response = await run ( buildRequest ( ) )
258+ const body = await response . json ( )
259+
260+ expect ( response . status ) . toBe ( 401 )
261+ expect ( body . error . code ) . toBe ( 'UNAUTHORIZED' )
262+ expect ( fetchMock ) . not . toHaveBeenCalled ( )
263+ } )
264+
265+ it ( 'pings Upstash and Supabase once authorized' , async ( ) => {
266+ fetchMock . mockResolvedValue ( createFetchResponse ( ) )
267+ supabaseAdminMock . from . mockReturnValueOnce ( createSelectChain ( { count : 5 } ) )
268+
269+ const response = await run (
270+ buildRequest ( {
271+ authorization : 'Bearer cron-secret' ,
272+ } ) ,
273+ )
274+ const body = await response . json ( )
275+
276+ expect ( response . status ) . toBe ( 200 )
277+ expect ( body . success ) . toBe ( true )
278+ expect ( typeof body . upstash . durationMs ) . toBe ( 'number' )
279+ expect ( body . supabase . rowsChecked ) . toBe ( 5 )
280+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 )
281+
282+ const fetchArgs = fetchMock . mock . calls [ 0 ] ! [ 1 ] as RequestInit | undefined
283+ const headers = fetchArgs ?. headers as Record < string , string > | undefined
284+ expect ( headers ?. Authorization ?? headers ?. authorization ) . toBe ( 'Bearer upstash-token' )
285+ } )
286+
287+ it ( 'returns upstream error details when Upstash responds with failure' , async ( ) => {
288+ fetchMock . mockResolvedValue (
289+ createFetchResponse ( {
290+ ok : false ,
291+ status : 503 ,
292+ statusText : 'Service Unavailable' ,
293+ json : vi . fn ( ) . mockResolvedValue ( { error : 'unavailable' } ) ,
294+ } ) ,
295+ )
296+ supabaseAdminMock . from . mockReturnValueOnce ( createSelectChain ( ) )
297+
298+ const response = await run (
299+ buildRequest ( {
300+ authorization : 'Bearer cron-secret' ,
301+ } ) ,
302+ )
303+ const body = await response . json ( )
304+
305+ expect ( response . status ) . toBe ( 503 )
306+ expect ( body . error . code ) . toBe ( 'CRON_UPSTASH_PING_FAILED' )
307+ } )
308+
309+ it ( 'surfaces Supabase errors when ping fails' , async ( ) => {
310+ fetchMock . mockResolvedValue ( createFetchResponse ( ) )
311+ supabaseAdminMock . from . mockReturnValueOnce (
312+ createSelectChain ( { error : { message : 'boom' } } ) ,
313+ )
314+
315+ const response = await run (
316+ buildRequest ( {
317+ authorization : 'Bearer cron-secret' ,
318+ } ) ,
319+ )
320+ const body = await response . json ( )
321+
322+ expect ( response . status ) . toBe ( 500 )
323+ expect ( body . error . code ) . toBe ( 'CRON_SUPABASE_PING_FAILED' )
324+ } )
325+ } )
186326} )
0 commit comments