Skip to content

Commit 307e9c7

Browse files
committed
fix: harden feed creation contracts
1 parent eb73421 commit 307e9c7

4 files changed

Lines changed: 54 additions & 2 deletions

File tree

frontend/src/__tests__/App.contract.test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,29 @@ describe('App contract', () => {
122122

123123
expect(screen.getByText('Invalid response format from API metadata')).toBeInTheDocument();
124124
});
125+
126+
it('reopens token recovery when a saved token is rejected by /api/v1/feeds', async () => {
127+
authenticate();
128+
129+
server.use(
130+
http.post('/api/v1/feeds', async () =>
131+
HttpResponse.json({ success: false, error: { message: 'Unauthorized' } }, { status: 401 })
132+
)
133+
);
134+
135+
render(<App />);
136+
137+
await screen.findByLabelText('Page URL');
138+
139+
fireEvent.input(screen.getByLabelText('Page URL'), {
140+
target: { value: 'https://example.com/articles' },
141+
});
142+
fireEvent.click(screen.getByRole('button', { name: 'Generate feed URL' }));
143+
144+
await screen.findByText('Access token was rejected. Paste a valid token to continue.');
145+
146+
expect(screen.getByText('Add access token')).toBeInTheDocument();
147+
expect(screen.queryByText('Feed generation failed')).not.toBeInTheDocument();
148+
expect(window.sessionStorage.getItem('html2rss_access_token')).toBeNull();
149+
});
125150
});

frontend/src/__tests__/useFeedConversion.contract.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,26 @@ describe('useFeedConversion contract', () => {
6060
expect(result.current.result).toBeNull();
6161
expect(result.current.error).toBe('URL parameter is required');
6262
});
63+
64+
it('normalizes malformed successful responses', async () => {
65+
server.use(
66+
http.post('/api/v1/feeds', async () =>
67+
HttpResponse.text('not-json', {
68+
status: 200,
69+
headers: { 'content-type': 'application/json' },
70+
})
71+
)
72+
);
73+
74+
const { result } = renderHook(() => useFeedConversion());
75+
76+
await act(async () => {
77+
await expect(
78+
result.current.convertFeed('https://example.com/articles', 'ssrf_filter', 'token')
79+
).rejects.toThrow('Invalid response format from feed creation API');
80+
});
81+
82+
expect(result.current.result).toBeNull();
83+
expect(result.current.error).toBe('Invalid response format from feed creation API');
84+
});
6385
});

frontend/src/hooks/useFeedConversion.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export function useFeedConversion() {
8585
}
8686

8787
const toErrorMessage = (error: unknown): string => {
88+
if (error instanceof SyntaxError) return 'Invalid response format from feed creation API';
8889
if (error instanceof Error) return error.message;
8990
if (typeof error === 'string' && error.trim()) return error;
9091

frontend/src/styles/main.css

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,8 +627,11 @@ a:focus-visible {
627627
.result-actions--quiet {
628628
width: min(100%, 40rem);
629629
margin: 0 auto;
630+
display: flex;
631+
flex-wrap: nowrap;
632+
align-items: center;
630633
justify-items: start;
631-
justify-content: start;
634+
justify-content: flex-start;
632635
gap: var(--space-4);
633636
margin-top: calc(var(--space-2) * -1);
634637
}
@@ -697,7 +700,7 @@ a:focus-visible {
697700
}
698701

699702
.result-actions {
700-
grid-template-columns: repeat(2, minmax(0, auto));
703+
grid-template-columns: none;
701704
}
702705

703706
.utility-strip__items {
@@ -727,6 +730,7 @@ a:focus-visible {
727730
}
728731

729732
.result-actions--quiet {
733+
display: grid;
730734
grid-template-columns: 1fr;
731735
gap: var(--space-3);
732736
}

0 commit comments

Comments
 (0)