@@ -412,6 +412,33 @@ describe('useFeedConversion', () => {
412412 } ) ;
413413 } ) ;
414414
415+ it ( 'does not auto-retry browserless for unauthorized faraday failures' , async ( ) => {
416+ fetchMock . mockResolvedValueOnce (
417+ new Response (
418+ JSON . stringify ( {
419+ success : false ,
420+ error : { message : 'Unauthorized' } ,
421+ } ) ,
422+ {
423+ status : 401 ,
424+ headers : { 'Content-Type' : 'application/json' } ,
425+ }
426+ )
427+ ) ;
428+
429+ const { result } = renderHook ( ( ) => useFeedConversion ( ) ) ;
430+
431+ await act ( async ( ) => {
432+ await expect (
433+ result . current . convertFeed ( 'https://example.com/articles' , 'faraday' , 'testtoken' )
434+ ) . rejects . toThrow ( 'Unauthorized' ) ;
435+ } ) ;
436+
437+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 ) ;
438+ expect ( result . current . result ) . toBeNull ( ) ;
439+ expect ( result . current . error ) . toBe ( 'Unauthorized' ) ;
440+ } ) ;
441+
415442 it ( 'does not offer a duplicate manual retry after automatic fallback also fails' , async ( ) => {
416443 fetchMock
417444 . mockResolvedValueOnce (
@@ -459,4 +486,125 @@ describe('useFeedConversion', () => {
459486 'Tried faraday first, then browserless. First attempt failed with: Upstream timeout. Second attempt failed with: Browserless also failed'
460487 ) ;
461488 } ) ;
489+
490+ it ( 'ignores stale preview updates from an earlier conversion request' , async ( ) => {
491+ const feedA = {
492+ id : 'feed-a-id' ,
493+ name : 'Feed A' ,
494+ url : 'https://example.com/a' ,
495+ strategy : 'faraday' ,
496+ feed_token : 'feed-a-token' ,
497+ public_url : 'https://example.com/feed-a' ,
498+ json_public_url : 'https://example.com/feed-a.json' ,
499+ created_at : '2024-01-01T00:00:00Z' ,
500+ updated_at : '2024-01-01T00:00:00Z' ,
501+ } ;
502+ const feedB = {
503+ id : 'feed-b-id' ,
504+ name : 'Feed B' ,
505+ url : 'https://example.com/b' ,
506+ strategy : 'faraday' ,
507+ feed_token : 'feed-b-token' ,
508+ public_url : 'https://example.com/feed-b' ,
509+ json_public_url : 'https://example.com/feed-b.json' ,
510+ created_at : '2024-01-01T00:00:00Z' ,
511+ updated_at : '2024-01-01T00:00:00Z' ,
512+ } ;
513+
514+ let resolvePreviewA : ( ( value : Response ) => void ) | null = null ;
515+ const previewAPromise = new Promise < Response > ( ( resolve ) => {
516+ resolvePreviewA = resolve ;
517+ } ) ;
518+ let resolvePreviewB : ( ( value : Response ) => void ) | null = null ;
519+ const previewBPromise = new Promise < Response > ( ( resolve ) => {
520+ resolvePreviewB = resolve ;
521+ } ) ;
522+
523+ fetchMock
524+ . mockResolvedValueOnce (
525+ new Response (
526+ JSON . stringify ( {
527+ success : true ,
528+ data : { feed : feedA } ,
529+ } ) ,
530+ {
531+ status : 201 ,
532+ headers : { 'Content-Type' : 'application/json' } ,
533+ }
534+ )
535+ )
536+ . mockReturnValueOnce ( previewAPromise as Promise < Response > )
537+ . mockResolvedValueOnce (
538+ new Response (
539+ JSON . stringify ( {
540+ success : true ,
541+ data : { feed : feedB } ,
542+ } ) ,
543+ {
544+ status : 201 ,
545+ headers : { 'Content-Type' : 'application/json' } ,
546+ }
547+ )
548+ )
549+ . mockReturnValueOnce ( previewBPromise as Promise < Response > ) ;
550+
551+ const { result } = renderHook ( ( ) => useFeedConversion ( ) ) ;
552+
553+ await act ( async ( ) => {
554+ await result . current . convertFeed ( 'https://example.com/a' , 'faraday' , 'testtoken' ) ;
555+ } ) ;
556+ await act ( async ( ) => {
557+ await result . current . convertFeed ( 'https://example.com/b' , 'faraday' , 'testtoken' ) ;
558+ } ) ;
559+
560+ expect ( result . current . result ?. feed . feed_token ) . toBe ( 'feed-b-token' ) ;
561+
562+ resolvePreviewB ?.(
563+ new Response (
564+ JSON . stringify ( {
565+ items : [
566+ {
567+ title : 'Preview B' ,
568+ content_text : 'Current preview item' ,
569+ url : 'https://example.com/b/item' ,
570+ date_published : '2024-01-02T00:00:00Z' ,
571+ } ,
572+ ] ,
573+ } ) ,
574+ {
575+ status : 200 ,
576+ headers : { 'Content-Type' : 'application/feed+json' } ,
577+ }
578+ )
579+ ) ;
580+
581+ await waitFor ( ( ) => {
582+ expect ( result . current . result ?. feed . feed_token ) . toBe ( 'feed-b-token' ) ;
583+ expect ( result . current . result ?. preview . items [ 0 ] ?. title ) . toBe ( 'Preview B' ) ;
584+ } ) ;
585+
586+ resolvePreviewA ?.(
587+ new Response (
588+ JSON . stringify ( {
589+ items : [
590+ {
591+ title : 'Preview A' ,
592+ content_text : 'Stale preview item' ,
593+ url : 'https://example.com/a/item' ,
594+ date_published : '2024-01-03T00:00:00Z' ,
595+ } ,
596+ ] ,
597+ } ) ,
598+ {
599+ status : 200 ,
600+ headers : { 'Content-Type' : 'application/feed+json' } ,
601+ }
602+ )
603+ ) ;
604+
605+ await waitFor ( ( ) => {
606+ expect ( result . current . result ?. feed . feed_token ) . toBe ( 'feed-b-token' ) ;
607+ expect ( result . current . result ?. preview . items [ 0 ] ?. title ) . toBe ( 'Preview B' ) ;
608+ } ) ;
609+ } ) ;
462610} ) ;
0 commit comments