Skip to content

Commit 652e398

Browse files
authored
Merge pull request #29 from github/copilot/fix-28
Add comprehensive test coverage for failure scenarios in plugin system
2 parents f8eaf03 + 52f235e commit 652e398

File tree

5 files changed

+283
-4
lines changed

5 files changed

+283
-4
lines changed

lib/hooks/core/plugin_loader.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def load_custom_auth_plugins(auth_plugin_dir)
123123
Dir.glob(File.join(auth_plugin_dir, "*.rb")).sort.each do |file_path|
124124
begin
125125
load_custom_auth_plugin(file_path, auth_plugin_dir)
126-
rescue => e
126+
rescue StandardError, SyntaxError => e
127127
raise StandardError, "Failed to load auth plugin from #{file_path}: #{e.message}"
128128
end
129129
end
@@ -139,7 +139,7 @@ def load_custom_handler_plugins(handler_plugin_dir)
139139
Dir.glob(File.join(handler_plugin_dir, "*.rb")).sort.each do |file_path|
140140
begin
141141
load_custom_handler_plugin(file_path, handler_plugin_dir)
142-
rescue => e
142+
rescue StandardError, SyntaxError => e
143143
raise StandardError, "Failed to load handler plugin from #{file_path}: #{e.message}"
144144
end
145145
end
@@ -155,7 +155,7 @@ def load_custom_lifecycle_plugins(lifecycle_plugin_dir)
155155
Dir.glob(File.join(lifecycle_plugin_dir, "*.rb")).sort.each do |file_path|
156156
begin
157157
load_custom_lifecycle_plugin(file_path, lifecycle_plugin_dir)
158-
rescue => e
158+
rescue StandardError, SyntaxError => e
159159
raise StandardError, "Failed to load lifecycle plugin from #{file_path}: #{e.message}"
160160
end
161161
end
@@ -171,7 +171,7 @@ def load_custom_instrument_plugins(instruments_plugin_dir)
171171
Dir.glob(File.join(instruments_plugin_dir, "*.rb")).sort.each do |file_path|
172172
begin
173173
load_custom_instrument_plugin(file_path, instruments_plugin_dir)
174-
rescue => e
174+
rescue StandardError, SyntaxError => e
175175
raise StandardError, "Failed to load instrument plugin from #{file_path}: #{e.message}"
176176
end
177177
end

spec/unit/lib/hooks/app/helpers_spec.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,19 @@ def error!(message, code)
300300
end
301301
end
302302

303+
describe "#safe_json_parse" do
304+
it "raises ArgumentError when JSON payload exceeds size limit" do
305+
# Test the actual size limit by temporarily setting a small limit
306+
stub_const("ENV", ENV.to_h.merge("JSON_MAX_SIZE" => "10"))
307+
308+
large_json = '{"data": "' + "x" * 20 + '"}'
309+
310+
expect {
311+
helper.send(:safe_json_parse, large_json)
312+
}.to raise_error(ArgumentError, "JSON payload too large for parsing")
313+
end
314+
end
315+
303316
describe "#determine_error_code" do
304317
it "returns 400 for ArgumentError" do
305318
error = ArgumentError.new("bad argument")

spec/unit/lib/hooks/core/builder_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require_relative "../../../spec_helper"
4+
35
describe Hooks::Core::Builder do
46
let(:log) { instance_double(Logger).as_null_object }
57
let(:temp_dir) { "/tmp/hooks_builder_test" }
@@ -272,6 +274,20 @@
272274
}.to raise_error(Hooks::Core::ConfigurationError,
273275
"Endpoint validation failed: Invalid endpoint")
274276
end
277+
278+
it "raises ConfigurationError when plugin loading fails" do
279+
allow(Hooks::Core::ConfigLoader).to receive(:load).and_return({ endpoints_dir: "/test" })
280+
allow(Hooks::Core::ConfigValidator).to receive(:validate_global_config).and_return({ endpoints_dir: "/test" })
281+
allow(Hooks::Core::ConfigLoader).to receive(:load_endpoints).and_return([])
282+
allow(Hooks::Core::ConfigValidator).to receive(:validate_endpoints).and_return([])
283+
allow(Hooks::Core::PluginLoader).to receive(:load_all_plugins)
284+
.and_raise(StandardError, "Plugin loading error")
285+
286+
expect {
287+
builder.build
288+
}.to raise_error(Hooks::Core::ConfigurationError,
289+
"Plugin loading failed: Plugin loading error")
290+
end
275291
end
276292
end
277293

spec/unit/lib/hooks/core/plugin_loader_spec.rb

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,252 @@ def call(payload:, headers:, config:)
172172
)
173173
end
174174
end
175+
176+
describe "failure scenarios" do
177+
describe "auth plugin loading failures" do
178+
it "raises error when auth plugin file fails to load" do
179+
temp_auth_dir = File.join(temp_dir, "auth_failures")
180+
FileUtils.mkdir_p(temp_auth_dir)
181+
182+
# Create a malformed Ruby file
183+
malformed_file = File.join(temp_auth_dir, "broken_auth.rb")
184+
File.write(malformed_file, "class BrokenAuth\n # Missing end statement")
185+
186+
expect {
187+
described_class.load_all_plugins({ auth_plugin_dir: temp_auth_dir })
188+
}.to raise_error(StandardError, /Failed to load auth plugin from.*broken_auth\.rb/)
189+
end
190+
191+
it "raises error for auth plugin path traversal attempt" do
192+
temp_auth_dir = File.join(temp_dir, "auth_secure")
193+
FileUtils.mkdir_p(temp_auth_dir)
194+
195+
# Create a plugin file outside the auth directory
196+
outside_file = File.join(temp_dir, "outside_auth.rb")
197+
File.write(outside_file, "# Outside file")
198+
199+
expect {
200+
described_class.send(:load_custom_auth_plugin, outside_file, temp_auth_dir)
201+
}.to raise_error(SecurityError, /Auth plugin path outside of auth plugin directory/)
202+
end
203+
204+
it "raises error for invalid auth plugin class name" do
205+
temp_auth_dir = File.join(temp_dir, "auth_invalid")
206+
FileUtils.mkdir_p(temp_auth_dir)
207+
208+
# Create plugin with invalid class name
209+
invalid_file = File.join(temp_auth_dir, "file.rb")
210+
File.write(invalid_file, "# File with dangerous class name")
211+
212+
expect {
213+
described_class.send(:load_custom_auth_plugin, invalid_file, temp_auth_dir)
214+
}.to raise_error(StandardError, /Invalid auth plugin class name: File/)
215+
end
216+
217+
it "raises error when auth plugin doesn't inherit from correct base class" do
218+
temp_auth_dir = File.join(temp_dir, "auth_inheritance")
219+
FileUtils.mkdir_p(temp_auth_dir)
220+
221+
# Create plugin with wrong inheritance
222+
wrong_file = File.join(temp_auth_dir, "wrong_auth.rb")
223+
File.write(wrong_file, <<~RUBY)
224+
module Hooks
225+
module Plugins
226+
module Auth
227+
class WrongAuth
228+
def self.valid?(payload:, headers:, config:)
229+
true
230+
end
231+
end
232+
end
233+
end
234+
end
235+
RUBY
236+
237+
expect {
238+
described_class.send(:load_custom_auth_plugin, wrong_file, temp_auth_dir)
239+
}.to raise_error(StandardError, /Auth plugin class must inherit from Hooks::Plugins::Auth::Base/)
240+
end
241+
end
242+
243+
describe "handler plugin loading failures" do
244+
it "raises error when handler plugin file fails to load" do
245+
temp_handler_dir = File.join(temp_dir, "handler_failures")
246+
FileUtils.mkdir_p(temp_handler_dir)
247+
248+
# Create a malformed Ruby file
249+
malformed_file = File.join(temp_handler_dir, "broken_handler.rb")
250+
File.write(malformed_file, "class BrokenHandler\n # Missing end statement")
251+
252+
expect {
253+
described_class.load_all_plugins({ handler_plugin_dir: temp_handler_dir })
254+
}.to raise_error(StandardError, /Failed to load handler plugin from.*broken_handler\.rb/)
255+
end
256+
257+
it "raises error for handler plugin path traversal attempt" do
258+
temp_handler_dir = File.join(temp_dir, "handler_secure")
259+
FileUtils.mkdir_p(temp_handler_dir)
260+
261+
# Create a plugin file outside the handler directory
262+
outside_file = File.join(temp_dir, "outside_handler.rb")
263+
File.write(outside_file, "# Outside file")
264+
265+
expect {
266+
described_class.send(:load_custom_handler_plugin, outside_file, temp_handler_dir)
267+
}.to raise_error(SecurityError, /Handler plugin path outside of handler plugin directory/)
268+
end
269+
270+
it "raises error for invalid handler plugin class name" do
271+
temp_handler_dir = File.join(temp_dir, "handler_invalid")
272+
FileUtils.mkdir_p(temp_handler_dir)
273+
274+
# Create plugin with invalid class name
275+
invalid_file = File.join(temp_handler_dir, "file.rb")
276+
File.write(invalid_file, "# File with dangerous class name")
277+
278+
expect {
279+
described_class.send(:load_custom_handler_plugin, invalid_file, temp_handler_dir)
280+
}.to raise_error(StandardError, /Invalid handler class name: File/)
281+
end
282+
283+
it "raises error when handler plugin doesn't inherit from correct base class" do
284+
temp_handler_dir = File.join(temp_dir, "handler_inheritance")
285+
FileUtils.mkdir_p(temp_handler_dir)
286+
287+
# Create plugin with wrong inheritance
288+
wrong_file = File.join(temp_handler_dir, "wrong_handler.rb")
289+
File.write(wrong_file, <<~RUBY)
290+
class WrongHandler
291+
def call(payload:, headers:, config:)
292+
{ message: "wrong handler" }
293+
end
294+
end
295+
RUBY
296+
297+
expect {
298+
described_class.send(:load_custom_handler_plugin, wrong_file, temp_handler_dir)
299+
}.to raise_error(StandardError, /Handler class must inherit from Hooks::Plugins::Handlers::Base/)
300+
end
301+
end
302+
303+
describe "lifecycle plugin loading failures" do
304+
it "raises error when lifecycle plugin file fails to load" do
305+
temp_lifecycle_dir = File.join(temp_dir, "lifecycle_failures")
306+
FileUtils.mkdir_p(temp_lifecycle_dir)
307+
308+
# Create a malformed Ruby file
309+
malformed_file = File.join(temp_lifecycle_dir, "broken_lifecycle.rb")
310+
File.write(malformed_file, "class BrokenLifecycle\n # Missing end statement")
311+
312+
expect {
313+
described_class.load_all_plugins({ lifecycle_plugin_dir: temp_lifecycle_dir })
314+
}.to raise_error(StandardError, /Failed to load lifecycle plugin from.*broken_lifecycle\.rb/)
315+
end
316+
317+
it "raises error for lifecycle plugin path traversal attempt" do
318+
temp_lifecycle_dir = File.join(temp_dir, "lifecycle_secure")
319+
FileUtils.mkdir_p(temp_lifecycle_dir)
320+
321+
# Create a plugin file outside the lifecycle directory
322+
outside_file = File.join(temp_dir, "outside_lifecycle.rb")
323+
File.write(outside_file, "# Outside file")
324+
325+
expect {
326+
described_class.send(:load_custom_lifecycle_plugin, outside_file, temp_lifecycle_dir)
327+
}.to raise_error(SecurityError, /Lifecycle plugin path outside of lifecycle plugin directory/)
328+
end
329+
330+
it "raises error for invalid lifecycle plugin class name" do
331+
temp_lifecycle_dir = File.join(temp_dir, "lifecycle_invalid")
332+
FileUtils.mkdir_p(temp_lifecycle_dir)
333+
334+
# Create plugin with invalid class name
335+
invalid_file = File.join(temp_lifecycle_dir, "file.rb")
336+
File.write(invalid_file, "# File with dangerous class name")
337+
338+
expect {
339+
described_class.send(:load_custom_lifecycle_plugin, invalid_file, temp_lifecycle_dir)
340+
}.to raise_error(StandardError, /Invalid lifecycle plugin class name: File/)
341+
end
342+
343+
it "raises error when lifecycle plugin doesn't inherit from correct base class" do
344+
temp_lifecycle_dir = File.join(temp_dir, "lifecycle_inheritance")
345+
FileUtils.mkdir_p(temp_lifecycle_dir)
346+
347+
# Create plugin with wrong inheritance
348+
wrong_file = File.join(temp_lifecycle_dir, "wrong_lifecycle.rb")
349+
File.write(wrong_file, <<~RUBY)
350+
class WrongLifecycle
351+
def on_request(env)
352+
# Wrong base class
353+
end
354+
end
355+
RUBY
356+
357+
expect {
358+
described_class.send(:load_custom_lifecycle_plugin, wrong_file, temp_lifecycle_dir)
359+
}.to raise_error(StandardError, /Lifecycle plugin class must inherit from Hooks::Plugins::Lifecycle/)
360+
end
361+
end
362+
363+
describe "instrument plugin loading failures" do
364+
it "raises error when instrument plugin file fails to load" do
365+
temp_instrument_dir = File.join(temp_dir, "instrument_failures")
366+
FileUtils.mkdir_p(temp_instrument_dir)
367+
368+
# Create a malformed Ruby file
369+
malformed_file = File.join(temp_instrument_dir, "broken_instrument.rb")
370+
File.write(malformed_file, "class BrokenInstrument\n # Missing end statement")
371+
372+
expect {
373+
described_class.load_all_plugins({ instruments_plugin_dir: temp_instrument_dir })
374+
}.to raise_error(StandardError, /Failed to load instrument plugin from.*broken_instrument\.rb/)
375+
end
376+
377+
it "raises error for instrument plugin path traversal attempt" do
378+
temp_instrument_dir = File.join(temp_dir, "instrument_secure")
379+
FileUtils.mkdir_p(temp_instrument_dir)
380+
381+
# Create a plugin file outside the instrument directory
382+
outside_file = File.join(temp_dir, "outside_instrument.rb")
383+
File.write(outside_file, "# Outside file")
384+
385+
expect {
386+
described_class.send(:load_custom_instrument_plugin, outside_file, temp_instrument_dir)
387+
}.to raise_error(SecurityError, /Instrument plugin path outside of instruments plugin directory/)
388+
end
389+
390+
it "raises error for invalid instrument plugin class name" do
391+
temp_instrument_dir = File.join(temp_dir, "instrument_invalid")
392+
FileUtils.mkdir_p(temp_instrument_dir)
393+
394+
# Create plugin with invalid class name
395+
invalid_file = File.join(temp_instrument_dir, "file.rb")
396+
File.write(invalid_file, "# File with dangerous class name")
397+
398+
expect {
399+
described_class.send(:load_custom_instrument_plugin, invalid_file, temp_instrument_dir)
400+
}.to raise_error(StandardError, /Invalid instrument plugin class name: File/)
401+
end
402+
403+
it "raises error when instrument plugin doesn't inherit from correct base class" do
404+
temp_instrument_dir = File.join(temp_dir, "instrument_inheritance")
405+
FileUtils.mkdir_p(temp_instrument_dir)
406+
407+
# Create plugin with wrong inheritance
408+
wrong_file = File.join(temp_instrument_dir, "wrong_instrument.rb")
409+
File.write(wrong_file, <<~RUBY)
410+
class WrongInstrument
411+
def record(metric_name, value, tags = {})
412+
# Wrong base class
413+
end
414+
end
415+
RUBY
416+
417+
expect {
418+
described_class.send(:load_custom_instrument_plugin, wrong_file, temp_instrument_dir)
419+
}.to raise_error(StandardError, /Instrument plugin class must inherit from StatsBase or FailbotBase/)
420+
end
421+
end
422+
end
175423
end

spec/unit/lib/hooks/plugins/lifecycle_spec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require_relative "../../../spec_helper"
4+
35
describe Hooks::Plugins::Lifecycle do
46
let(:plugin) { described_class.new }
57
let(:env) { { "REQUEST_METHOD" => "POST", "PATH_INFO" => "/webhook" } }

0 commit comments

Comments
 (0)