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\n end"
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