Skip to content

Commit 4f05b45

Browse files
Middleware specs to cover more CSRF edge cases
and refactor the specs to be more clear.
1 parent ef06461 commit 4f05b45

File tree

1 file changed

+109
-52
lines changed

1 file changed

+109
-52
lines changed

spec/better_errors/middleware_spec.rb

Lines changed: 109 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ module BetterErrors
44
describe Middleware do
55
let(:app) { Middleware.new(->env { ":)" }) }
66
let(:exception) { RuntimeError.new("oh no :(") }
7+
let(:status) { response_env[0] }
8+
let(:headers) { response_env[1] }
9+
let(:body) { response_env[2].join }
710

8-
it "passes non-error responses through" do
9-
expect(app.call({})).to eq(":)")
11+
context 'when the application raises no exception' do
12+
it "passes non-error responses through" do
13+
expect(app.call({})).to eq(":)")
14+
end
1015
end
1116

1217
it "calls the internal methods" do
@@ -24,11 +29,6 @@ module BetterErrors
2429
app.call("PATH_INFO" => "/__better_errors/")
2530
end
2631

27-
it "shows the error page on any subfolder path" do
28-
expect(app).to receive :show_error_page
29-
app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/")
30-
end
31-
3232
it "doesn't show the error page to a non-local address" do
3333
expect(app).not_to receive :better_errors_call
3434
app.call("REMOTE_ADDR" => "1.2.3.4")
@@ -62,34 +62,71 @@ module BetterErrors
6262
expect { app.call("REMOTE_ADDR" => "0:0:0:0:0:0:0:1%0" ) }.to_not raise_error
6363
end
6464

65-
context "when requesting the /__better_errors manually" do
66-
let(:app) { Middleware.new(->env { ":)" }) }
65+
context "when /__better_errors is requested directly" do
66+
let(:response_env) { app.call("PATH_INFO" => "/__better_errors") }
6767

68-
it "shows that no errors have been recorded" do
69-
status, headers, body = app.call("PATH_INFO" => "/__better_errors")
70-
expect(body.join).to match /No errors have been recorded yet./
71-
end
68+
context "when no error has been recorded since startup" do
69+
it "shows that no errors have been recorded" do
70+
expect(body).to match /No errors have been recorded yet./
71+
end
7272

73-
it 'does not attempt to use ActionDispatch::ExceptionWrapper with a nil exception' do
74-
ad_ew = double("ActionDispatch::ExceptionWrapper")
75-
stub_const('ActionDispatch::ExceptionWrapper', ad_ew)
76-
expect(ad_ew).to_not receive :new
73+
it 'does not attempt to use ActionDispatch::ExceptionWrapper on the nil exception' do
74+
ad_ew = double("ActionDispatch::ExceptionWrapper")
75+
stub_const('ActionDispatch::ExceptionWrapper', ad_ew)
76+
expect(ad_ew).to_not receive :new
77+
78+
response_env
79+
end
80+
81+
context 'when requested inside a subfolder path' do
82+
let(:response_env) { app.call("PATH_INFO" => "/any_sub/folder/__better_errors") }
7783

78-
status, headers, body = app.call("PATH_INFO" => "/__better_errors")
84+
it "shows that no errors have been recorded" do
85+
expect(body).to match /No errors have been recorded yet./
86+
end
87+
end
7988
end
8089

81-
it "shows that no errors have been recorded on any subfolder path" do
82-
status, headers, body = app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors")
83-
expect(body.join).to match /No errors have been recorded yet./
90+
context 'when an error has been recorded' do
91+
let(:app) {
92+
Middleware.new(->env do
93+
# Only raise on the first request
94+
raise exception unless @already_raised
95+
@already_raised = true
96+
end)
97+
}
98+
before do
99+
app.call({})
100+
end
101+
102+
it 'returns the information of the most recent error' do
103+
expect(body).to include("oh no :(")
104+
end
105+
106+
it 'does not attempt to use ActionDispatch::ExceptionWrapper' do
107+
ad_ew = double("ActionDispatch::ExceptionWrapper")
108+
stub_const('ActionDispatch::ExceptionWrapper', ad_ew)
109+
expect(ad_ew).to_not receive :new
110+
111+
response_env
112+
end
113+
114+
context 'when inside a subfolder path' do
115+
let(:response_env) { app.call("PATH_INFO" => "/any_sub/folder/__better_errors") }
116+
117+
it "shows the error page on any subfolder path" do
118+
expect(app).to receive :show_error_page
119+
app.call("PATH_INFO" => "/any_sub/folder/path/__better_errors/")
120+
end
121+
end
84122
end
85123
end
86124

87125
context "when handling an error" do
88126
let(:app) { Middleware.new(->env { raise exception }) }
127+
let(:response_env) { app.call({}) }
89128

90129
it "returns status 500" do
91-
status, headers, body = app.call({})
92-
93130
expect(status).to eq(500)
94131
end
95132

@@ -109,11 +146,9 @@ module BetterErrors
109146
}
110147

111148
it "shows the exception as-is" do
112-
status, _, body = app.call({})
113-
114149
expect(status).to eq(500)
115-
expect(body.join).to match(/\n> Second Exception\n/)
116-
expect(body.join).not_to match(/\n> First Exception\n/)
150+
expect(body).to match(/\n> Second Exception\n/)
151+
expect(body).not_to match(/\n> First Exception\n/)
117152
end
118153
end
119154

@@ -135,11 +170,9 @@ def initialize(message, original_exception = nil)
135170
}
136171

137172
it "shows the original exception instead of the last-raised one" do
138-
status, _, body = app.call({})
139-
140173
expect(status).to eq(500)
141-
expect(body.join).not_to match(/Second Exception/)
142-
expect(body.join).to match(/First Exception/)
174+
expect(body).not_to match(/Second Exception/)
175+
expect(body).to match(/First Exception/)
143176
end
144177
end
145178

@@ -151,10 +184,8 @@ def initialize(message, original_exception = nil)
151184
}
152185

153186
it "shows the exception as-is" do
154-
status, _, body = app.call({})
155-
156187
expect(status).to eq(500)
157-
expect(body.join).to match(/The Exception/)
188+
expect(body).to match(/The Exception/)
158189
end
159190
end
160191
end
@@ -164,28 +195,57 @@ def initialize(message, original_exception = nil)
164195
allow(ad_ew).to receive('new').with(anything, exception) { double("ExceptionWrapper", status_code: 404) }
165196
stub_const('ActionDispatch::ExceptionWrapper', ad_ew)
166197

167-
status, headers, body = app.call({})
168-
169198
expect(status).to eq(404)
170199
end
171200

172201
it "returns UTF-8 error pages" do
173-
status, headers, body = app.call({})
174-
175202
expect(headers["Content-Type"]).to match /charset=utf-8/
176203
end
177204

178-
it "returns text pages by default" do
179-
status, headers, body = app.call({})
180-
205+
it "returns text content by default" do
181206
expect(headers["Content-Type"]).to match /text\/plain/
182207
end
183208

184-
it "returns HTML pages by default" do
185-
# Chrome's 'Accept' header looks similar this.
186-
status, headers, body = app.call("HTTP_ACCEPT" => "text/html,application/xhtml+xml;q=0.9,*/*")
209+
context 'when a CSRF token cookie is not specified' do
210+
it 'includes a newly-generated CSRF token cookie' do
211+
expect(headers).to include(
212+
'Set-Cookie' => /BetterErrors-CSRF-Token=[-a-z0-9]+; HttpOnly; SameSite=Strict/
213+
)
214+
end
215+
end
216+
217+
context 'when a CSRF token cookie is specified' do
218+
let(:response_env) { app.call({ 'HTTP_COOKIE' => 'BetterErrors-CSRF-Token=abc123' }) }
219+
220+
it 'does not set a new CSRF token cookie' do
221+
expect(headers).not_to include('Set-Cookie')
222+
end
223+
end
224+
225+
context 'when the Accept header specifies HTML first' do
226+
let(:response_env) { app.call("HTTP_ACCEPT" => "text/html,application/xhtml+xml;q=0.9,*/*") }
227+
228+
it "returns HTML content" do
229+
expect(headers["Content-Type"]).to match /text\/html/
230+
end
231+
232+
it 'includes the newly-generated CSRF token in the body of the page' do
233+
matches = headers['Set-Cookie'].match(/BetterErrors-CSRF-Token=(?<tok>[-a-z0-9]+); HttpOnly; SameSite=Strict/)
234+
expect(body).to include(matches[:tok])
235+
end
187236

188-
expect(headers["Content-Type"]).to match /text\/html/
237+
context 'when a CSRF token cookie is specified' do
238+
let(:response_env) {
239+
app.call({
240+
'HTTP_COOKIE' => 'BetterErrors-CSRF-Token=csrfTokenGHI',
241+
"HTTP_ACCEPT" => "text/html,application/xhtml+xml;q=0.9,*/*",
242+
})
243+
}
244+
245+
it 'includes that CSRF token in the body of the page' do
246+
expect(body).to include('csrfTokenGHI')
247+
end
248+
end
189249
end
190250

191251
context 'the logger' do
@@ -196,7 +256,7 @@ def initialize(message, original_exception = nil)
196256

197257
it "receives the exception as a fatal message" do
198258
expect(logger).to receive(:fatal).with(/RuntimeError/)
199-
app.call({})
259+
response_env
200260
end
201261

202262
context 'when Rails is being used' do
@@ -208,7 +268,7 @@ def initialize(message, original_exception = nil)
208268
expect(logger).to receive(:fatal) do |message|
209269
expect(message).to_not match(/rspec-core/)
210270
end
211-
app.call({})
271+
response_env
212272
end
213273
end
214274
context 'when Rails is not being used' do
@@ -220,24 +280,21 @@ def initialize(message, original_exception = nil)
220280
expect(logger).to receive(:fatal) do |message|
221281
expect(message).to match(/rspec-core/)
222282
end
223-
app.call({})
283+
response_env
224284
end
225285
end
226286
end
227287
end
228288

229289
context "requesting the variables for a specific frame" do
230290
let(:env) { {} }
231-
let(:result) {
291+
let(:response_env) {
232292
app.call(request_env)
233293
}
234294
let(:request_env) {
235295
Rack::MockRequest.env_for("/__better_errors/#{id}/variables", input: StringIO.new(JSON.dump(request_body_data)))
236296
}
237297
let(:request_body_data) { {"index": 0} }
238-
let(:status) { result[0] }
239-
let(:headers) { result[1] }
240-
let(:body) { result[2].join }
241298
let(:json_body) { JSON.parse(body) }
242299
let(:id) { 'abcdefg' }
243300

0 commit comments

Comments
 (0)