Skip to content

Commit 4583168

Browse files
committed
ask api/v1/strategies
1 parent 1be3874 commit 4583168

File tree

6 files changed

+158
-14
lines changed

6 files changed

+158
-14
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ describe('App contract', () => {
4444
await waitFor(() => {
4545
expect(screen.getByText('Your RSS feed is live!')).toBeInTheDocument();
4646
expect(
47-
screen.getByText('Drop it straight into your reader or explore the preview without leaving this page.')
47+
screen.getByText(
48+
'Drop it straight into your reader or explore the preview without leaving this page.'
49+
)
4850
).toBeInTheDocument();
4951
});
5052
});

frontend/src/__tests__/App.test.tsx

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ vi.mock('../hooks/useFeedConversion', () => ({
1212
useFeedConversion: vi.fn(),
1313
}));
1414

15+
vi.mock('../hooks/useStrategies', () => ({
16+
useStrategies: vi.fn(),
17+
}));
18+
1519
import { useAuth } from '../hooks/useAuth';
1620
import { useFeedConversion } from '../hooks/useFeedConversion';
21+
import { useStrategies } from '../hooks/useStrategies';
1722

1823
const mockUseAuth = useAuth as any;
1924
const mockUseFeedConversion = useFeedConversion as any;
25+
const mockUseStrategies = useStrategies as any;
2026

2127
describe('App', () => {
2228
const mockLogin = vi.fn();
@@ -41,16 +47,20 @@ describe('App', () => {
4147
convertFeed: mockConvertFeed,
4248
clearResult: mockClearResult,
4349
});
50+
51+
mockUseStrategies.mockReturnValue({
52+
strategies: [],
53+
isLoading: false,
54+
error: null,
55+
});
4456
});
4557

4658
it('should render demo section when not authenticated', () => {
4759
render(<App />);
4860

49-
expect(screen.getByText('🚀 Try It Out')).toBeInTheDocument();
61+
expect(screen.getByText('🚀 Try it out')).toBeInTheDocument();
5062
expect(
51-
screen.getByText(
52-
'Click any button below to instantly convert these websites to RSS feeds - no signup required!'
53-
)
63+
screen.getByText('Launch a demo conversion to see the results instantly. No sign-in required.')
5464
).toBeInTheDocument();
5565
expect(screen.getByText('Sign in here')).toBeInTheDocument();
5666
});
@@ -59,25 +69,42 @@ describe('App', () => {
5969
mockUseAuth.mockReturnValue({
6070
isAuthenticated: true,
6171
username: 'testuser',
72+
token: 'test-token',
6273
login: mockLogin,
6374
logout: mockLogout,
6475
});
6576

77+
mockUseStrategies.mockReturnValue({
78+
strategies: [
79+
{ id: 'ssrf_filter', name: 'ssrf_filter', display_name: 'SSRF Filter' },
80+
{ id: 'browserless', name: 'browserless', display_name: 'Browserless' },
81+
],
82+
isLoading: false,
83+
error: null,
84+
});
85+
6686
render(<App />);
6787

6888
expect(screen.getByText('Welcome, testuser!')).toBeInTheDocument();
69-
expect(screen.getByText('🌐 Convert Website')).toBeInTheDocument();
70-
expect(screen.getByText('Enter the URL of the website you want to convert to RSS')).toBeInTheDocument();
89+
expect(screen.getByText('🌐 Convert website')).toBeInTheDocument();
90+
expect(screen.getByText('Enter a URL to generate an RSS feed.')).toBeInTheDocument();
7191
});
7292

7393
it('should call logout when logout button is clicked', () => {
7494
mockUseAuth.mockReturnValue({
7595
isAuthenticated: true,
7696
username: 'testuser',
97+
token: 'test-token',
7798
login: mockLogin,
7899
logout: mockLogout,
79100
});
80101

102+
mockUseStrategies.mockReturnValue({
103+
strategies: [{ id: 'ssrf_filter', name: 'ssrf_filter', display_name: 'SSRF Filter' }],
104+
isLoading: false,
105+
error: null,
106+
});
107+
81108
render(<App />);
82109

83110
const logoutButton = screen.getByText('Logout');
@@ -91,10 +118,17 @@ describe('App', () => {
91118
mockUseAuth.mockReturnValue({
92119
isAuthenticated: true,
93120
username: 'tester',
121+
token: 'test-token',
94122
login: mockLogin,
95123
logout: mockLogout,
96124
});
97125

126+
mockUseStrategies.mockReturnValue({
127+
strategies: [{ id: 'ssrf_filter', name: 'ssrf_filter', display_name: 'SSRF Filter' }],
128+
isLoading: false,
129+
error: null,
130+
});
131+
98132
mockUseFeedConversion.mockReturnValue({
99133
isConverting: false,
100134
result: null,
@@ -105,7 +139,7 @@ describe('App', () => {
105139

106140
render(<App />);
107141

108-
expect(screen.getByText('❌ Error')).toBeInTheDocument();
142+
expect(screen.getByText('Conversion error')).toBeInTheDocument();
109143
expect(screen.getByText('Access Denied')).toBeInTheDocument();
110144
});
111145
});

frontend/src/__tests__/ResultDisplay.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ describe('ResultDisplay', () => {
2323

2424
expect(screen.getByText('🎉')).toBeInTheDocument();
2525
expect(screen.getByText('Your RSS feed is live!')).toBeInTheDocument();
26-
expect(screen.getByText('Drop it straight into your reader or explore the preview without leaving this page.')).toBeInTheDocument();
26+
expect(
27+
screen.getByText('Drop it straight into your reader or explore the preview without leaving this page.')
28+
).toBeInTheDocument();
2729
});
2830

2931
it('should call onClose when convert-another button is clicked', () => {

frontend/src/__tests__/mocks/server.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
11
import { setupServer } from 'msw/node';
2+
import { rest } from 'msw';
23

3-
export const server = setupServer();
4+
export const server = setupServer(
5+
rest.get('/api/v1/strategies', (req, res, ctx) => {
6+
return res(
7+
ctx.json({
8+
success: true,
9+
data: {
10+
strategies: [
11+
{
12+
id: 'ssrf_filter',
13+
name: 'ssrf_filter',
14+
display_name: 'SSRF Filter',
15+
},
16+
{
17+
id: 'browserless',
18+
name: 'browserless',
19+
display_name: 'Browserless',
20+
},
21+
],
22+
},
23+
meta: { total: 2 },
24+
})
25+
);
26+
})
27+
);
428

529
export interface FeedResponseOverrides {
630
id?: string;

frontend/src/components/App.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ResultDisplay } from './ResultDisplay';
44
import { QuickLogin } from './QuickLogin';
55
import { useAuth } from '../hooks/useAuth';
66
import { useFeedConversion } from '../hooks/useFeedConversion';
7+
import { useStrategies } from '../hooks/useStrategies';
78
import styles from './App.module.css';
89

910
export function App() {
@@ -17,6 +18,7 @@ export function App() {
1718
error: authError,
1819
} = useAuth();
1920
const { isConverting, result, error, convertFeed, clearResult } = useFeedConversion();
21+
const { strategies, isLoading: strategiesLoading, error: strategiesError } = useStrategies(token);
2022

2123
const [showAuthForm, setShowAuthForm] = useState(false);
2224
const [authFormData, setAuthFormData] = useState({ username: '', token: '' });
@@ -28,14 +30,20 @@ export function App() {
2830
}
2931
}, [isAuthenticated]);
3032

33+
useEffect(() => {
34+
if (strategies.length > 0 && !feedFormData.strategy) {
35+
setFeedFormData((prev) => ({ ...prev, strategy: strategies[0].id }));
36+
}
37+
}, [strategies]);
38+
3139
const handleAuthSubmit = async (event?: Event) => {
3240
event?.preventDefault();
3341

3442
if (!authFormData.username || !authFormData.token) return;
3543

3644
try {
3745
await login(authFormData.username, authFormData.token);
38-
} catch (error) {}
46+
} catch (error) { }
3947
};
4048

4149
const handleFeedSubmit = async (event: Event) => {
@@ -45,7 +53,7 @@ export function App() {
4553

4654
try {
4755
await convertFeed(feedFormData.url, feedFormData.strategy, token || '');
48-
} catch (error) {}
56+
} catch (error) { }
4957
};
5058

5159
const handleShowAuth = () => {
@@ -60,8 +68,9 @@ export function App() {
6068

6169
const handleDemoConversion = async (url: string) => {
6270
try {
63-
await convertFeed(url, 'ssrf_filter', 'self-host-for-full-access');
64-
} catch (error) {}
71+
const demoStrategy = strategies.length > 0 ? strategies[0].id : 'ssrf_filter';
72+
await convertFeed(url, demoStrategy, 'self-host-for-full-access');
73+
} catch (error) { }
6574
};
6675

6776
const showResultExperience = Boolean(result);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { useState, useEffect } from 'preact/hooks';
2+
3+
interface Strategy {
4+
id: string;
5+
name: string;
6+
display_name: string;
7+
}
8+
9+
interface StrategiesState {
10+
strategies: Strategy[];
11+
isLoading: boolean;
12+
error: string | null;
13+
}
14+
15+
export function useStrategies(token: string | null) {
16+
const [state, setState] = useState<StrategiesState>({
17+
strategies: [],
18+
isLoading: false,
19+
error: null,
20+
});
21+
22+
const fetchStrategies = async () => {
23+
if (!token) {
24+
setState({ strategies: [], isLoading: false, error: null });
25+
return;
26+
}
27+
28+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
29+
30+
try {
31+
const response = await fetch('/api/v1/strategies', {
32+
method: 'GET',
33+
headers: {
34+
Authorization: `Bearer ${token}`,
35+
'Content-Type': 'application/json',
36+
},
37+
});
38+
39+
if (!response.ok) {
40+
throw new Error(`Failed to fetch strategies: ${response.status} ${response.statusText}`);
41+
}
42+
43+
const data = await response.json();
44+
45+
if (data.success && data.data?.strategies) {
46+
setState({
47+
strategies: data.data.strategies,
48+
isLoading: false,
49+
error: null,
50+
});
51+
} else {
52+
throw new Error('Invalid response format from strategies API');
53+
}
54+
} catch (error) {
55+
setState({
56+
strategies: [],
57+
isLoading: false,
58+
error: error instanceof Error ? error.message : 'Failed to fetch strategies',
59+
});
60+
}
61+
};
62+
63+
useEffect(() => {
64+
fetchStrategies();
65+
}, [token]);
66+
67+
return {
68+
strategies: state.strategies,
69+
isLoading: state.isLoading,
70+
error: state.error,
71+
refetch: fetchStrategies,
72+
};
73+
}

0 commit comments

Comments
 (0)