@@ -8,6 +8,7 @@ const applyBotRateLimitMock = jest.fn<(req: NextApiRequest, res: NextApiResponse
88const verifyJwtMock = jest . fn < ( ) => unknown > ( ) ;
99const isBotJwtMock = jest . fn < ( ) => boolean > ( ) ;
1010const findBotUserMock = jest . fn < ( ) => Promise < unknown > > ( ) ;
11+ const getProviderMock = jest . fn ( ) ;
1112const providerGetMock = jest . fn < ( path : string ) => Promise < unknown > > ( ) ;
1213const parseScopeMock = jest . fn < ( scope : string ) => string [ ] > ( ) ;
1314const scopeIncludesMock = jest . fn < ( scopes : string [ ] , required : string ) => boolean > ( ) ;
@@ -73,9 +74,7 @@ jest.unstable_mockModule(
7374 "@/utils/get-provider" ,
7475 ( ) => ( {
7576 __esModule : true ,
76- getProvider : ( ) => ( {
77- get : providerGetMock ,
78- } ) ,
77+ getProvider : getProviderMock ,
7978 } ) ,
8079) ;
8180
@@ -125,6 +124,9 @@ beforeEach(() => {
125124 id : "bot-1" ,
126125 botKey : { scope : JSON . stringify ( [ "multisig:read" , "governance:read" ] ) } ,
127126 } ) ;
127+ getProviderMock . mockReturnValue ( {
128+ get : providerGetMock ,
129+ } ) ;
128130} ) ;
129131
130132describe ( "governanceActiveProposals API" , ( ) => {
@@ -310,4 +312,144 @@ describe("governanceActiveProposals API", () => {
310312 } ) ;
311313 expect ( payload . activeCount ) . toBe ( 1 ) ;
312314 } ) ;
315+
316+ it ( "falls back to direct Blockfrost REST when provider list fetch fails without a status" , async ( ) => {
317+ providerGetMock . mockImplementation ( async ( path ) => {
318+ if ( path . startsWith ( "/governance/proposals?" ) ) {
319+ throw new Error ( "Internal Server Error" ) ;
320+ }
321+ if ( path === "/governance/proposals/tx-active/0" ) {
322+ return {
323+ ratified_epoch : null ,
324+ enacted_epoch : null ,
325+ dropped_epoch : null ,
326+ expired_epoch : null ,
327+ expiration : 999 ,
328+ deposit : "1000000" ,
329+ return_address : "addr_test1..." ,
330+ } ;
331+ }
332+ if ( path === "/governance/proposals/tx-active/0/metadata" ) {
333+ throw { status : 404 } ;
334+ }
335+ return null ;
336+ } ) ;
337+ const originalKey = process . env . BLOCKFROST_API_KEY_PREPROD ;
338+ process . env . BLOCKFROST_API_KEY_PREPROD = "preprod-key" ;
339+ const fetchSpy = jest . spyOn ( globalThis , "fetch" ) . mockResolvedValue (
340+ new Response (
341+ JSON . stringify ( [
342+ {
343+ tx_hash : "tx-active" ,
344+ cert_index : 0 ,
345+ governance_type : "info_action" ,
346+ enacted_epoch : null ,
347+ dropped_epoch : null ,
348+ expired_epoch : null ,
349+ ratified_epoch : null ,
350+ } ,
351+ ] ) ,
352+ { status : 200 } ,
353+ ) ,
354+ ) ;
355+
356+ try {
357+ const req = {
358+ method : "GET" ,
359+ headers : { authorization : "Bearer token" } ,
360+ query : { network : "0" , count : "20" , page : "1" , order : "desc" , details : "false" } ,
361+ } as unknown as NextApiRequest ;
362+ const res = createMockResponse ( ) ;
363+
364+ await handler ( req , res ) ;
365+
366+ expect ( fetchSpy ) . toHaveBeenCalledWith (
367+ "https://cardano-preprod.blockfrost.io/api/v0/governance/proposals?count=20&page=1&order=desc" ,
368+ expect . objectContaining ( {
369+ headers : expect . objectContaining ( { project_id : "preprod-key" } ) ,
370+ } ) ,
371+ ) ;
372+ expect ( res . status ) . toHaveBeenCalledWith ( 200 ) ;
373+ const payload = ( res . json as unknown as jest . Mock ) . mock . calls [ 0 ] ?. [ 0 ] as any ;
374+ expect ( payload . proposals ) . toHaveLength ( 1 ) ;
375+ expect ( payload . activeCount ) . toBe ( 1 ) ;
376+ } finally {
377+ if ( originalKey === undefined ) {
378+ delete process . env . BLOCKFROST_API_KEY_PREPROD ;
379+ } else {
380+ process . env . BLOCKFROST_API_KEY_PREPROD = originalKey ;
381+ }
382+ fetchSpy . mockRestore ( ) ;
383+ }
384+ } ) ;
385+
386+ it ( "falls back to direct Blockfrost REST when provider construction fails" , async ( ) => {
387+ getProviderMock . mockImplementation ( ( ) => {
388+ throw new TypeError ( "Cannot read properties of undefined (reading 'slice')" ) ;
389+ } ) ;
390+ const originalKey = process . env . BLOCKFROST_API_KEY_PREPROD ;
391+ process . env . BLOCKFROST_API_KEY_PREPROD = "preprod-key" ;
392+ const fetchSpy = jest . spyOn ( globalThis , "fetch" ) . mockImplementation ( async ( url ) => {
393+ const urlString = String ( url ) ;
394+ if ( urlString . includes ( "/governance/proposals?" ) ) {
395+ return new Response (
396+ JSON . stringify ( [
397+ {
398+ tx_hash : "tx-active" ,
399+ cert_index : 0 ,
400+ governance_type : "info_action" ,
401+ enacted_epoch : null ,
402+ dropped_epoch : null ,
403+ expired_epoch : null ,
404+ ratified_epoch : null ,
405+ } ,
406+ ] ) ,
407+ { status : 200 } ,
408+ ) ;
409+ }
410+ if ( urlString . includes ( "/governance/proposals/tx-active/0/metadata" ) ) {
411+ return new Response ( JSON . stringify ( { error : "Not Found" , status_code : 404 } ) , {
412+ status : 404 ,
413+ } ) ;
414+ }
415+ if ( urlString . includes ( "/governance/proposals/tx-active/0" ) ) {
416+ return new Response (
417+ JSON . stringify ( {
418+ ratified_epoch : null ,
419+ enacted_epoch : null ,
420+ dropped_epoch : null ,
421+ expired_epoch : null ,
422+ expiration : 999 ,
423+ deposit : "1000000" ,
424+ return_address : "addr_test1..." ,
425+ } ) ,
426+ { status : 200 } ,
427+ ) ;
428+ }
429+ return new Response ( JSON . stringify ( { error : "Unexpected path" } ) , { status : 500 } ) ;
430+ } ) ;
431+
432+ try {
433+ const req = {
434+ method : "GET" ,
435+ headers : { authorization : "Bearer token" } ,
436+ query : { network : "0" , count : "20" , page : "1" , order : "desc" , details : "false" } ,
437+ } as unknown as NextApiRequest ;
438+ const res = createMockResponse ( ) ;
439+
440+ await handler ( req , res ) ;
441+
442+ expect ( res . status ) . toHaveBeenCalledWith ( 200 ) ;
443+ const payload = ( res . json as unknown as jest . Mock ) . mock . calls [ 0 ] ?. [ 0 ] as any ;
444+ expect ( payload . proposals ) . toHaveLength ( 1 ) ;
445+ expect ( payload . activeCount ) . toBe ( 1 ) ;
446+ } finally {
447+ if ( originalKey === undefined ) {
448+ delete process . env . BLOCKFROST_API_KEY_PREPROD ;
449+ } else {
450+ process . env . BLOCKFROST_API_KEY_PREPROD = originalKey ;
451+ }
452+ fetchSpy . mockRestore ( ) ;
453+ }
454+ } ) ;
313455} ) ;
0 commit comments