Skip to content

Commit 9346f63

Browse files
CopilotGrantBirki
andcommitted
Add comprehensive tests for app helpers module
Co-authored-by: GrantBirki <[email protected]>
1 parent 7b681e8 commit 9346f63

File tree

1 file changed

+372
-0
lines changed

1 file changed

+372
-0
lines changed
Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
# frozen_string_literal: true
2+
3+
require "tempfile"
4+
5+
describe Hooks::App::Helpers do
6+
let(:test_class) do
7+
Class.new do
8+
include Hooks::App::Helpers
9+
10+
attr_accessor :headers, :env, :request_obj
11+
12+
def headers
13+
@headers ||= {}
14+
end
15+
16+
def env
17+
@env ||= {}
18+
end
19+
20+
def request
21+
@request_obj
22+
end
23+
24+
def error!(message, code)
25+
raise StandardError, "#{code}: #{message}"
26+
end
27+
end
28+
end
29+
30+
let(:helper) { test_class.new }
31+
32+
describe "#uuid" do
33+
it "generates a valid UUID" do
34+
result = helper.uuid
35+
36+
expect(result).to match(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/)
37+
end
38+
39+
it "generates unique UUIDs on each call" do
40+
uuid1 = helper.uuid
41+
uuid2 = helper.uuid
42+
43+
expect(uuid1).not_to eq(uuid2)
44+
end
45+
end
46+
47+
describe "#enforce_request_limits" do
48+
let(:config) { { request_limit: 1000 } }
49+
50+
context "with content-length in headers" do
51+
it "passes when content length is within limit" do
52+
helper.headers["Content-Length"] = "500"
53+
54+
expect { helper.enforce_request_limits(config) }.not_to raise_error
55+
end
56+
57+
it "raises error when content length exceeds limit" do
58+
helper.headers["Content-Length"] = "1500"
59+
60+
expect { helper.enforce_request_limits(config) }.to raise_error(StandardError, /413.*too large/)
61+
end
62+
end
63+
64+
context "with different header formats" do
65+
it "handles uppercase CONTENT_LENGTH" do
66+
helper.headers["CONTENT_LENGTH"] = "1500"
67+
68+
expect { helper.enforce_request_limits(config) }.to raise_error(StandardError, /413.*too large/)
69+
end
70+
71+
it "handles lowercase content-length" do
72+
helper.headers["content-length"] = "1500"
73+
74+
expect { helper.enforce_request_limits(config) }.to raise_error(StandardError, /413.*too large/)
75+
end
76+
77+
it "handles HTTP_CONTENT_LENGTH" do
78+
helper.headers["HTTP_CONTENT_LENGTH"] = "1500"
79+
80+
expect { helper.enforce_request_limits(config) }.to raise_error(StandardError, /413.*too large/)
81+
end
82+
end
83+
84+
context "with content-length in env" do
85+
it "uses env CONTENT_LENGTH when headers are empty" do
86+
helper.env["CONTENT_LENGTH"] = "1500"
87+
88+
expect { helper.enforce_request_limits(config) }.to raise_error(StandardError, /413.*too large/)
89+
end
90+
91+
it "uses env HTTP_CONTENT_LENGTH when headers are empty" do
92+
helper.env["HTTP_CONTENT_LENGTH"] = "1500"
93+
94+
expect { helper.enforce_request_limits(config) }.to raise_error(StandardError, /413.*too large/)
95+
end
96+
end
97+
98+
context "with request object" do
99+
it "uses request.content_length when available" do
100+
request_mock = double("request")
101+
allow(request_mock).to receive(:content_length).and_return(1500)
102+
helper.request_obj = request_mock
103+
104+
expect { helper.enforce_request_limits(config) }.to raise_error(StandardError, /413.*too large/)
105+
end
106+
end
107+
108+
context "without content length information" do
109+
it "passes when no content length is available" do
110+
expect { helper.enforce_request_limits(config) }.not_to raise_error
111+
end
112+
end
113+
end
114+
115+
describe "#parse_payload" do
116+
context "with JSON content" do
117+
it "parses valid JSON with application/json content type" do
118+
headers = { "Content-Type" => "application/json" }
119+
body = '{"key": "value"}'
120+
121+
result = helper.parse_payload(body, headers)
122+
123+
expect(result).to eq({ key: "value" })
124+
end
125+
126+
it "parses JSON that looks like JSON without content type" do
127+
headers = {}
128+
body = '{"key": "value"}'
129+
130+
result = helper.parse_payload(body, headers)
131+
132+
expect(result).to eq({ key: "value" })
133+
end
134+
135+
it "parses JSON arrays" do
136+
headers = {}
137+
body = '[{"key": "value"}]'
138+
139+
result = helper.parse_payload(body, headers)
140+
141+
expect(result).to eq([{ "key" => "value" }])
142+
end
143+
144+
it "symbolizes keys by default" do
145+
headers = { "Content-Type" => "application/json" }
146+
body = '{"string_key": "value", "nested": {"inner_key": "inner_value"}}'
147+
148+
result = helper.parse_payload(body, headers)
149+
150+
expect(result).to eq({
151+
string_key: "value",
152+
nested: { "inner_key" => "inner_value" } # Only top level is symbolized
153+
})
154+
end
155+
156+
it "does not symbolize keys when symbolize is false" do
157+
headers = { "Content-Type" => "application/json" }
158+
body = '{"string_key": "value"}'
159+
160+
result = helper.parse_payload(body, headers, symbolize: false)
161+
162+
expect(result).to eq({ "string_key" => "value" })
163+
end
164+
end
165+
166+
context "with different content type headers" do
167+
it "handles uppercase CONTENT_TYPE" do
168+
headers = { "CONTENT_TYPE" => "application/json" }
169+
body = '{"key": "value"}'
170+
171+
result = helper.parse_payload(body, headers)
172+
173+
expect(result).to eq({ key: "value" })
174+
end
175+
176+
it "handles lowercase content-type" do
177+
headers = { "content-type" => "application/json" }
178+
body = '{"key": "value"}'
179+
180+
result = helper.parse_payload(body, headers)
181+
182+
expect(result).to eq({ key: "value" })
183+
end
184+
185+
it "handles HTTP_CONTENT_TYPE" do
186+
headers = { "HTTP_CONTENT_TYPE" => "application/json" }
187+
body = '{"key": "value"}'
188+
189+
result = helper.parse_payload(body, headers)
190+
191+
expect(result).to eq({ key: "value" })
192+
end
193+
end
194+
195+
context "with invalid JSON" do
196+
it "returns raw body when JSON parsing fails" do
197+
headers = { "Content-Type" => "application/json" }
198+
body = '{"invalid": json}'
199+
200+
result = helper.parse_payload(body, headers)
201+
202+
expect(result).to eq(body)
203+
end
204+
end
205+
206+
context "with non-JSON content" do
207+
it "returns raw body for plain text" do
208+
headers = { "Content-Type" => "text/plain" }
209+
body = "plain text content"
210+
211+
result = helper.parse_payload(body, headers)
212+
213+
expect(result).to eq(body)
214+
end
215+
216+
it "returns raw body for XML" do
217+
headers = { "Content-Type" => "application/xml" }
218+
body = "<xml>content</xml>"
219+
220+
result = helper.parse_payload(body, headers)
221+
222+
expect(result).to eq(body)
223+
end
224+
end
225+
end
226+
227+
describe "#valid_handler_class_name?" do
228+
it "returns true for valid handler class names" do
229+
valid_names = ["MyHandler", "GitHubHandler", "Team1Handler", "APIHandler"]
230+
231+
valid_names.each do |name|
232+
expect(helper.send(:valid_handler_class_name?, name)).to be true
233+
end
234+
end
235+
236+
it "returns false for non-string input" do
237+
expect(helper.send(:valid_handler_class_name?, nil)).to be false
238+
expect(helper.send(:valid_handler_class_name?, 123)).to be false
239+
expect(helper.send(:valid_handler_class_name?, [])).to be false
240+
end
241+
242+
it "returns false for empty or whitespace-only strings" do
243+
expect(helper.send(:valid_handler_class_name?, "")).to be false
244+
expect(helper.send(:valid_handler_class_name?, " ")).to be false
245+
expect(helper.send(:valid_handler_class_name?, "\t")).to be false
246+
end
247+
248+
it "returns false for class names not starting with uppercase" do
249+
expect(helper.send(:valid_handler_class_name?, "myHandler")).to be false
250+
expect(helper.send(:valid_handler_class_name?, "handler")).to be false
251+
expect(helper.send(:valid_handler_class_name?, "123Handler")).to be false
252+
end
253+
254+
it "returns false for class names with invalid characters" do
255+
expect(helper.send(:valid_handler_class_name?, "My-Handler")).to be false
256+
expect(helper.send(:valid_handler_class_name?, "My.Handler")).to be false
257+
expect(helper.send(:valid_handler_class_name?, "My Handler")).to be false
258+
expect(helper.send(:valid_handler_class_name?, "My/Handler")).to be false
259+
end
260+
261+
it "returns false for dangerous class names" do
262+
Hooks::Security::DANGEROUS_CLASSES.each do |dangerous_class|
263+
expect(helper.send(:valid_handler_class_name?, dangerous_class)).to be false
264+
end
265+
end
266+
end
267+
268+
describe "#determine_error_code" do
269+
it "returns 400 for ArgumentError" do
270+
error = ArgumentError.new("bad argument")
271+
272+
expect(helper.send(:determine_error_code, error)).to eq(400)
273+
end
274+
275+
it "returns 501 for NotImplementedError" do
276+
error = NotImplementedError.new("not implemented")
277+
278+
expect(helper.send(:determine_error_code, error)).to eq(501)
279+
end
280+
281+
it "returns 500 for other errors" do
282+
error = StandardError.new("generic error")
283+
284+
expect(helper.send(:determine_error_code, error)).to eq(500)
285+
end
286+
287+
it "returns 500 for RuntimeError" do
288+
error = RuntimeError.new("runtime error")
289+
290+
expect(helper.send(:determine_error_code, error)).to eq(500)
291+
end
292+
end
293+
294+
describe "#load_handler" do
295+
let(:temp_dir) { Dir.mktmpdir }
296+
let(:handler_class_name) { "TestHandler" }
297+
298+
after do
299+
FileUtils.rm_rf(temp_dir)
300+
end
301+
302+
context "with valid handler" do
303+
it "loads and instantiates a valid handler" do
304+
# Create a test handler file
305+
handler_content = <<~RUBY
306+
class TestHandler < Hooks::Handlers::Base
307+
def call(payload:, headers:, config:)
308+
{ status: "ok" }
309+
end
310+
end
311+
RUBY
312+
313+
File.write(File.join(temp_dir, "test_handler.rb"), handler_content)
314+
315+
result = helper.load_handler(handler_class_name, temp_dir)
316+
317+
expect(result).to be_an_instance_of(TestHandler)
318+
expect(result).to respond_to(:call)
319+
end
320+
end
321+
322+
context "with invalid handler class name" do
323+
it "raises error for invalid class name" do
324+
expect { helper.load_handler("invalid-name", temp_dir) }.to raise_error(StandardError, /400.*invalid handler class name/)
325+
end
326+
327+
it "raises error for dangerous class name" do
328+
expect { helper.load_handler("File", temp_dir) }.to raise_error(StandardError, /400.*invalid handler class name/)
329+
end
330+
end
331+
332+
context "with path traversal attempts" do
333+
it "raises error for path traversal" do
334+
expect { helper.load_handler("../../../EvilHandler", temp_dir) }.to raise_error(StandardError, /400.*invalid handler class name/)
335+
end
336+
end
337+
338+
context "with missing handler file" do
339+
it "raises LoadError when handler file does not exist" do
340+
expect { helper.load_handler("MissingHandler", temp_dir) }.to raise_error(LoadError, /Handler MissingHandler not found/)
341+
end
342+
end
343+
344+
context "with handler that doesn't inherit from Base" do
345+
it "raises error when handler doesn't inherit from Base" do
346+
# Create a handler that doesn't inherit from Base
347+
handler_content = <<~RUBY
348+
class BadHandler
349+
def call(payload:, headers:, config:)
350+
{ status: "ok" }
351+
end
352+
end
353+
RUBY
354+
355+
File.write(File.join(temp_dir, "bad_handler.rb"), handler_content)
356+
357+
expect { helper.load_handler("BadHandler", temp_dir) }.to raise_error(StandardError, /400.*must inherit from Hooks::Handlers::Base/)
358+
end
359+
end
360+
361+
context "with handler file that has syntax errors" do
362+
it "raises SyntaxError when handler file has syntax errors" do
363+
# Create a handler with syntax errors
364+
handler_content = "class SyntaxErrorHandler < Hooks::Handlers::Base\n def call\n {invalid syntax\n end\nend"
365+
366+
File.write(File.join(temp_dir, "syntax_error_handler.rb"), handler_content)
367+
368+
expect { helper.load_handler("SyntaxErrorHandler", temp_dir) }.to raise_error(SyntaxError)
369+
end
370+
end
371+
end
372+
end

0 commit comments

Comments
 (0)