Skip to content

Commit d7c24e6

Browse files
committed
ask api/v1/strategies
1 parent 405eec5 commit d7c24e6

14 files changed

Lines changed: 193 additions & 191 deletions

File tree

README.md

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ The in-repo docs live under `frontend/src/content/docs/` and are published by As
4040

4141
## REST API Snapshot
4242
```bash
43-
# List feeds available to the token
43+
# List available strategies
4444
curl -H "Authorization: Bearer <token>" \
45-
"https://your-domain.com/api/v1/feeds"
45+
"https://your-domain.com/api/v1/strategies"
4646

4747
# Create a feed and capture the signed public URL
4848
curl -X POST "https://your-domain.com/api/v1/feeds" \
@@ -76,37 +76,37 @@ The Ruby server continues to serve the production build while Astro runs with ho
7676

7777
## Make Targets
7878

79-
| Command | Purpose |
80-
| --- | --- |
81-
| `make help` | List available shortcuts. |
82-
| `make setup` | Install Ruby and Node dependencies. |
83-
| `make dev` | Run Ruby (port 3000) and Astro (port 4321) dev servers. |
84-
| `make dev-ruby` | Start only the Ruby server. |
85-
| `make dev-frontend` | Start only the Astro dev server. |
86-
| `make test` | Run Ruby and frontend test suites. |
87-
| `make test-ruby` | Run Ruby specs. |
88-
| `make test-frontend` | Run frontend unit and contract tests. |
89-
| `make lint` | Run all linters. |
90-
| `make lintfix` | Auto-fix lint warnings where possible. |
91-
| `make clean` | Remove build artefacts. |
79+
| Command | Purpose |
80+
| -------------------- | ------------------------------------------------------- |
81+
| `make help` | List available shortcuts. |
82+
| `make setup` | Install Ruby and Node dependencies. |
83+
| `make dev` | Run Ruby (port 3000) and Astro (port 4321) dev servers. |
84+
| `make dev-ruby` | Start only the Ruby server. |
85+
| `make dev-frontend` | Start only the Astro dev server. |
86+
| `make test` | Run Ruby and frontend test suites. |
87+
| `make test-ruby` | Run Ruby specs. |
88+
| `make test-frontend` | Run frontend unit and contract tests. |
89+
| `make lint` | Run all linters. |
90+
| `make lintfix` | Auto-fix lint warnings where possible. |
91+
| `make clean` | Remove build artefacts. |
9292

9393
## Frontend npm Scripts
9494

95-
| Command | Purpose |
96-
| --- | --- |
97-
| `npm run dev` | Astro dev server with hot reload. |
98-
| `npm run build` | Production build. |
99-
| `npm run test:run` | Unit tests (Vitest). |
100-
| `npm run test:contract` | Contract tests with MSW. |
95+
| Command | Purpose |
96+
| ----------------------- | --------------------------------- |
97+
| `npm run dev` | Astro dev server with hot reload. |
98+
| `npm run build` | Production build. |
99+
| `npm run test:run` | Unit tests (Vitest). |
100+
| `npm run test:contract` | Contract tests with MSW. |
101101

102102
## Testing Strategy
103103

104-
| Layer | Tooling | Focus |
105-
| --- | --- | --- |
106-
| Ruby API | RSpec + Rack::Test | Feed creation, retrieval, auth paths. |
107-
| Frontend unit | Vitest + Testing Library | Component rendering and hooks with mocked fetch. |
108-
| Frontend contract | Vitest + MSW | End-to-end fetch flows against mocked API responses. |
109-
| Docker smoke | RSpec (`:docker`) | Net::HTTP probes against the containerised service. |
104+
| Layer | Tooling | Focus |
105+
| ----------------- | ------------------------ | ---------------------------------------------------- |
106+
| Ruby API | RSpec + Rack::Test | Feed creation, retrieval, auth paths. |
107+
| Frontend unit | Vitest + Testing Library | Component rendering and hooks with mocked fetch. |
108+
| Frontend contract | Vitest + MSW | End-to-end fetch flows against mocked API responses. |
109+
| Docker smoke | RSpec (`:docker`) | Net::HTTP probes against the containerised service. |
110110

111111
## Contributing
112112

app/api/v1/feeds.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ module Web
1717
module Api
1818
module V1
1919
# RESTful API v1 for feeds
20-
module Feeds
20+
module Feeds # rubocop:disable Metrics/ModuleLength
2121
DEFAULT_TTL_SECONDS = 3600 # 1 hour
2222

23-
class << self
23+
class << self # rubocop:disable Metrics/ClassLength
2424
def show(request, token)
2525
ensure_auto_source_enabled!
2626

app/feeds.rb

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,9 @@
88
module Html2rss
99
module Web
1010
##
11-
# Feeds functionality for listing and generating RSS feeds
11+
# Feeds functionality for generating RSS feeds
1212
module Feeds
1313
class << self
14-
def list_feeds
15-
LocalConfig.feed_names.map do |name|
16-
{
17-
id: name.to_s,
18-
name: name.to_s,
19-
description: "RSS feed for #{name}",
20-
public_url: "/#{name}"
21-
}
22-
end
23-
end
24-
2514
def generate_feed(feed_name, params = {})
2615
config = LocalConfig.find(feed_name)
2716
config[:params] = (config[:params] || {}).merge(params) if params.any?

app/local_config.rb

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,6 @@ def global
3636
yaml.reject { |key| key == :feeds }
3737
end
3838

39-
##
40-
# @return [Array<Symbol>] names of locally available feeds
41-
def feed_names
42-
feeds.keys
43-
end
44-
4539
##
4640
# @return [Hash<Symbol, Any>]
4741
def yaml

app/url_validator.rb

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,6 @@ def url_allowed?(account, url)
3232
end
3333
end
3434

35-
# @param url [String]
36-
# @param patterns [Array<String>]
37-
# @return [Boolean]
38-
def url_matches_patterns?(url, patterns)
39-
return false unless (normalized_url = normalize_url(url))
40-
41-
Array(patterns).any? do |pattern|
42-
wildcard?(pattern) ? match_wildcard?(pattern, normalized_url) : match_exact?(pattern, normalized_url)
43-
end
44-
end
45-
46-
# @param url [String]
47-
# @param pattern [String]
48-
# @return [Boolean]
49-
def url_matches_pattern?(url, pattern)
50-
return false unless (normalized_url = normalize_url(url))
51-
52-
wildcard?(pattern) ? match_wildcard?(pattern, normalized_url) : match_exact?(pattern, normalized_url)
53-
end
54-
5535
private
5636

5737
def match_exact?(pattern, normalized_url)

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: 10 additions & 1 deletion
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,6 +30,12 @@ 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

@@ -60,7 +68,8 @@ export function App() {
6068

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

0 commit comments

Comments
 (0)