Skip to content

Commit 4c775bd

Browse files
committed
Harden feed conversion retry/race tests
(cherry picked from commit 4bbe6d1)
1 parent ed2b3e9 commit 4c775bd

2 files changed

Lines changed: 148 additions & 9 deletions

File tree

frontend/src/__tests__/useFeedConversion.test.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
});

spec/html2rss/web/api/v1_spec.rb

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -475,17 +475,8 @@ def expected_featured_feeds
475475
end
476476

477477
it 'normalizes hostname-only input to https before feed creation', :aggregate_failures do
478-
allow(Html2rss::Web::AutoSource).to receive(:create_stable_feed).and_call_original
479-
480478
post_feed_request(url: 'example.com/articles', strategy: 'faraday')
481479

482-
expect(Html2rss::Web::AutoSource).to have_received(:create_stable_feed).with(
483-
anything,
484-
'https://example.com/articles',
485-
kind_of(Hash),
486-
'faraday'
487-
)
488-
489480
expect(last_response.status).to eq(201)
490481
json = expect_success_response(last_response)
491482
expect(json.dig('data', 'feed', 'url')).to eq('https://example.com/articles')

0 commit comments

Comments
 (0)