Skip to content

Commit 01d9e23

Browse files
committed
Strengthen feed request specs
1 parent 5142e87 commit 01d9e23

2 files changed

Lines changed: 59 additions & 88 deletions

File tree

spec/html2rss/web/app_integration_spec.rb

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
include Rack::Test::Methods
1212

1313
let(:app) { described_class.freeze.app }
14+
let(:secret_key) { ENV.fetch('HTML2RSS_SECRET_KEY') }
1415

1516
let(:feed_url) { 'https://example.com/articles' }
16-
let(:feed_token) { "valid-feed-token-#{SecureRandom.hex(4)}" }
17+
let(:feed_token) do
18+
Html2rss::Web::Auth.generate_feed_token(account[:username], feed_url, strategy: 'faraday')
19+
end
1720
let(:encoded_feed_token) { CGI.escape(feed_token) }
1821

1922
let(:account) do
@@ -55,18 +58,6 @@
5558
allow(Html2rss::Web::LocalConfig).to receive(:yaml).and_return(accounts_config)
5659
stub_const('Html2rss::FeedChannel', Class.new { attr_reader :ttl })
5760
stub_const('Html2rss::Feed', Class.new { attr_reader :channel })
58-
token_payload = instance_double(
59-
Html2rss::Web::FeedToken,
60-
url: feed_url,
61-
username: account[:username],
62-
strategy: 'faraday'
63-
)
64-
allow(Html2rss::Web::FeedToken).to receive_messages(
65-
decode: token_payload,
66-
validate_and_decode: token_payload
67-
)
68-
allow(Html2rss::Web::AccountManager).to receive(:get_account_by_username).and_return(account)
69-
allow(Html2rss::Web::UrlValidator).to receive(:url_allowed?).and_return(true)
7061
allow(Html2rss::Web::AutoSource).to receive(:enabled?).and_return(true)
7162
allow(Html2rss::Web::Feeds::Service).to receive(:call).and_return(feed_result)
7263
allow(Html2rss::Web::Feeds::RssRenderer).to receive(:call).and_return('<rss version="2.0"></rss>')
@@ -76,8 +67,6 @@
7667

7768
describe 'GET /api/v1/feeds/:token' do # rubocop:disable RSpec/MultipleMemoizedHelpers
7869
it 'returns unauthorized for invalid tokens' do
79-
allow(Html2rss::Web::FeedToken).to receive(:decode).and_return(nil)
80-
8170
get '/api/v1/feeds/invalid-token', {}, { 'HTTP_ACCEPT' => 'application/xml' }
8271

8372
expect(last_response.status).to eq(401)
@@ -172,8 +161,6 @@
172161
end
173162

174163
it 'returns JSON Feed-shaped errors for invalid json feed tokens' do
175-
allow(Html2rss::Web::FeedToken).to receive(:decode).and_return(nil)
176-
177164
get '/api/v1/feeds/invalid-token.json'
178165

179166
expect([last_response.status, last_response.headers['Content-Type'], json_feed_error]).to eq(
@@ -225,8 +212,6 @@ def stub_escaped_feed_token(raw_token:, encoded_token:)
225212
end
226213

227214
context 'without authentication' do # rubocop:disable RSpec/MultipleMemoizedHelpers
228-
before { allow(Html2rss::Web::Auth).to receive(:authenticate).and_return(nil) }
229-
230215
it 'requires authentication' do
231216
post '/api/v1/feeds', request_payload.to_json, json_headers
232217

@@ -237,19 +222,19 @@ def stub_escaped_feed_token(raw_token:, encoded_token:)
237222
end
238223

239224
context 'with authenticated account' do # rubocop:disable RSpec/MultipleMemoizedHelpers
240-
before { allow(Html2rss::Web::Auth).to receive(:authenticate).and_return(account) }
225+
before do
226+
allow(Html2rss::Web::Api::V1::FeedMetadata).to receive(:site_title_for).and_return('Example Feed')
227+
end
241228

242229
it 'returns bad request when JSON payload is invalid' do
243-
post '/api/v1/feeds', '{ invalid', json_headers
230+
post '/api/v1/feeds', '{ invalid', auth_headers
244231

245232
expect(last_response.status).to eq(400)
246233
expect(last_response.content_type).to include('application/json')
247234
expect(json_body).to include('error' => include('message' => 'Invalid JSON payload'))
248235
end
249236

250237
it 'returns bad request when URL is missing' do
251-
allow(Html2rss::Web::Api::V1::FeedMetadata).to receive(:site_title_for).and_return('Example')
252-
253238
post '/api/v1/feeds', request_payload.merge(url: '').to_json, auth_headers
254239

255240
expect(last_response.status).to eq(400)

spec/html2rss/web/feeds/responder_spec.rb

Lines changed: 51 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,62 @@
11
# frozen_string_literal: true
22

33
require 'spec_helper'
4+
require 'rack'
45
require_relative '../../../../app'
56

67
RSpec.describe Html2rss::Web::Feeds::Responder do
78
let(:response) { Rack::Response.new }
8-
let(:request) { instance_double(Struct.new(:response), response: response) }
9-
10-
def feed_request(representation)
11-
Html2rss::Web::Feeds::Contracts::Request.new(
12-
target_kind: :token,
13-
representation: representation,
14-
feed_name: nil,
15-
token: 'token',
16-
params: {}
9+
let(:result) do
10+
Html2rss::Web::Feeds::Contracts::RenderResult.new(
11+
status: :ok,
12+
payload: nil,
13+
message: nil,
14+
ttl_seconds: 600,
15+
cache_key: 'feed_result:test',
16+
error_message: nil
1717
)
1818
end
19+
let(:static_config) do
20+
{
21+
channel: { url: 'https://example.com', ttl: 10 },
22+
strategy: :faraday
23+
}
24+
end
1925

20-
def resolved_source
21-
Html2rss::Web::Feeds::Contracts::ResolvedSource.new(
22-
source_kind: :token,
23-
cache_identity: 'token:abc',
24-
generator_input: { strategy: :faraday, channel: { url: 'https://example.com' } },
25-
ttl_seconds: 600
26-
)
26+
before do
27+
allow(Html2rss::Web::LocalConfig).to receive(:find).with('example').and_return(static_config)
28+
allow(Html2rss::Web::Observability).to receive(:emit)
2729
end
2830

2931
context 'with a cacheable success result' do
3032
subject(:write_response) do
3133
described_class.call(
32-
request: request,
33-
target_kind: :token,
34-
identifier: 'token'
35-
)
36-
end
37-
38-
let(:representation) { Html2rss::Web::FeedResponseFormat::RSS }
39-
40-
let(:result) do
41-
Html2rss::Web::Feeds::Contracts::RenderResult.new(
42-
status: :ok,
43-
payload: nil,
44-
message: nil,
45-
ttl_seconds: 600,
46-
cache_key: 'feed_result:test',
47-
error_message: nil
34+
request: request_for(path: '/example', accept: 'application/xml'),
35+
target_kind: :static,
36+
identifier: 'example'
4837
)
4938
end
5039

5140
before do
52-
allow(Html2rss::Web::Feeds::Request).to receive(:call).and_return(feed_request(representation))
53-
allow(Html2rss::Web::Feeds::SourceResolver).to receive(:call).and_return(resolved_source)
5441
allow(Html2rss::Web::Feeds::Service).to receive(:call).and_return(result)
55-
allow(Html2rss::Web::Observability).to receive(:emit)
5642
allow(Html2rss::Web::Feeds::RssRenderer).to receive(:call).with(result).and_return('<rss/>')
5743
end
5844

5945
it 'writes the expected response tuple' do
6046
expect(response_tuple(write_response)).to eq([200, 'application/xml', '<rss/>'])
6147
end
6248

63-
it 'marks the response as cacheable', :aggregate_failures do
49+
it 'resolves the source through the real request and source resolver path', :aggregate_failures do
6450
write_response
6551

52+
expect(Html2rss::Web::Feeds::Service).to have_received(:call).with(
53+
have_attributes(
54+
source_kind: :static,
55+
cache_identity: a_string_starting_with('static:example:'),
56+
generator_input: include(strategy: :faraday, channel: { url: 'https://example.com', ttl: 10 }),
57+
ttl_seconds: 600
58+
)
59+
)
6660
expect(response['Cache-Control']).to include('max-age=600')
6761
expect(response['Cache-Control']).to include('public')
6862
expect(response['Vary']).to eq('Accept')
@@ -74,7 +68,7 @@ def resolved_source
7468
expect(Html2rss::Web::Observability).to have_received(:emit).with(
7569
event_name: 'feed.render',
7670
outcome: 'success',
77-
details: include(strategy: :faraday, url: 'https://example.com'),
71+
details: include(strategy: :faraday, url: 'https://example.com', feed_name: 'example'),
7872
level: :info
7973
)
8074
end
@@ -83,14 +77,12 @@ def resolved_source
8377
context 'with an error result' do
8478
subject(:write_response) do
8579
described_class.call(
86-
request: request,
87-
target_kind: :token,
88-
identifier: 'token'
80+
request: request_for(path: '/example.json', accept: 'application/feed+json'),
81+
target_kind: :static,
82+
identifier: 'example.json'
8983
)
9084
end
9185

92-
let(:representation) { Html2rss::Web::FeedResponseFormat::JSON_FEED }
93-
9486
let(:result) do
9587
Html2rss::Web::Feeds::Contracts::RenderResult.new(
9688
status: :error,
@@ -103,10 +95,7 @@ def resolved_source
10395
end
10496

10597
before do
106-
allow(Html2rss::Web::Feeds::Request).to receive(:call).and_return(feed_request(representation))
107-
allow(Html2rss::Web::Feeds::SourceResolver).to receive(:call).and_return(resolved_source)
10898
allow(Html2rss::Web::Feeds::Service).to receive(:call).and_return(result)
109-
allow(Html2rss::Web::Observability).to receive(:emit)
11099
allow(Html2rss::Web::Feeds::JsonRenderer).to receive(:call).with(result).and_return('{"title":"Error"}')
111100
end
112101

@@ -125,30 +114,14 @@ def resolved_source
125114
context 'when response rendering fails after feed generation succeeds' do
126115
subject(:write_response) do
127116
described_class.call(
128-
request: request,
129-
target_kind: :token,
130-
identifier: 'token'
131-
)
132-
end
133-
134-
let(:representation) { Html2rss::Web::FeedResponseFormat::RSS }
135-
136-
let(:result) do
137-
Html2rss::Web::Feeds::Contracts::RenderResult.new(
138-
status: :ok,
139-
payload: nil,
140-
message: nil,
141-
ttl_seconds: 600,
142-
cache_key: 'feed_result:test',
143-
error_message: nil
117+
request: request_for(path: '/example', accept: 'application/xml'),
118+
target_kind: :static,
119+
identifier: 'example'
144120
)
145121
end
146122

147123
before do
148-
allow(Html2rss::Web::Feeds::Request).to receive(:call).and_return(feed_request(representation))
149-
allow(Html2rss::Web::Feeds::SourceResolver).to receive(:call).and_return(resolved_source)
150124
allow(Html2rss::Web::Feeds::Service).to receive(:call).and_return(result)
151-
allow(Html2rss::Web::Observability).to receive(:emit)
152125
allow(Html2rss::Web::Feeds::RssRenderer).to receive(:call).and_raise(StandardError, 'render failed')
153126
end
154127

@@ -158,14 +131,27 @@ def resolved_source
158131
expect(Html2rss::Web::Observability).to have_received(:emit).once.with(
159132
event_name: 'feed.render',
160133
outcome: 'failure',
161-
details: include(error_class: 'StandardError', error_message: 'render failed'),
134+
details: include(error_class: 'StandardError', error_message: 'render failed', feed_name: 'example'),
162135
level: :warn
163136
)
164137
end
165138
end
166139

167140
private
168141

142+
# @param path [String]
143+
# @param accept [String]
144+
# @return [Rack::Request]
145+
def request_for(path:, accept:)
146+
rack_response = response
147+
148+
Rack::Request.new(
149+
Rack::MockRequest.env_for(path, 'HTTP_ACCEPT' => accept)
150+
).tap do |request|
151+
request.define_singleton_method(:response) { rack_response }
152+
end
153+
end
154+
169155
# @param body [String]
170156
# @return [Array<(Integer, String, String)>]
171157
def response_tuple(body)

0 commit comments

Comments
 (0)