diff --git a/Gemfile.lock b/Gemfile.lock index 5c1ce7d8..c3752188 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - hooks-ruby (0.2.1) + hooks-ruby (0.3.0) dry-schema (~> 1.14, >= 1.14.1) grape (~> 2.3) puma (~> 6.6) @@ -148,7 +148,7 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.4) - rubocop (1.76.1) + rubocop (1.76.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -156,7 +156,7 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.45.0, < 2.0) + rubocop-ast (>= 1.45.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.45.1) diff --git a/README.md b/README.md index 55191046..aa9d31ed 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,11 @@ Here is a very high-level overview of how Hooks works: ```yaml # file: config/endpoints/hello.yml path: /hello - handler: MyCustomHandler # This is a custom handler plugin you would define in the plugins/handlers directory + handler: my_custom_handler # This is a custom handler plugin you would define in the plugins/handlers directory (snake_case) ``` + > Note: If your handler's class name is `MyCustomHandler`, you would define it in the `plugins/handlers/my_custom_handler.rb` file. The `handler` field in the endpoint configuration file should be the snake_case version of the class name. So if your handler class is `MyCustomHandler`, you would use `my_custom_handler` in the endpoint configuration file. + 3. Now create a corresponding handler plugin in the `plugins/handlers` directory. Here is an example of a simple handler plugin: ```ruby @@ -64,7 +66,7 @@ Here is a very high-level overview of how Hooks works: # For this example, we will just return a success message { status: "success", - handler: "MyCustomHandler", + handler: "my_custom_handler", payload_received: payload, timestamp: Time.now.utc.iso8601 } @@ -208,16 +210,16 @@ Endpoint configurations are defined in the `config/endpoints` directory. Each en ```yaml # file: config/endpoints/hello.yml path: /hello # becomes /webhooks/hello based on the root_path in hooks.yml -handler: HelloHandler # This is a custom handler plugin you would define in the plugins/handlers +handler: hello_handler # This is a custom handler plugin you would define in the plugins/handlers ``` ```yaml # file: config/endpoints/goodbye.yml path: /goodbye # becomes /webhooks/goodbye based on the root_path in hooks.yml -handler: GoodbyeHandler # This is another custom handler plugin you would define in the plugins/handlers +handler: goodbye_handler # This is another custom handler plugin you would define in the plugins/handlers auth: - type: Goodbye # This is a custom authentication plugin you would define in the plugins/auth + type: goodbye # This is a custom authentication plugin you would define in the plugins/auth secret_env_key: GOODBYE_API_KEY # the name of the environment variable containing the secret header: Authorization @@ -255,7 +257,7 @@ class GoodbyeHandler < Hooks::Plugins::Handlers::Base # Ditto for the goodbye endpoint { message: "goodbye webhook processed successfully", - handler: "GoodbyeHandler", + handler: "goodbye_handler", timestamp: Time.now.utc.iso8601 } end diff --git a/docs/auth_plugins.md b/docs/auth_plugins.md index f54312cd..f821b904 100644 --- a/docs/auth_plugins.md +++ b/docs/auth_plugins.md @@ -448,3 +448,20 @@ module Hooks end end ``` + +The configuration for this IP filtering plugin would look like this: + +```yaml +path: /example +handler: CoolNewHandler # could be any handler you want to use + +auth: + type: ip_filtering_plugin # using the custom IP filtering plugin (remember IpFilteringPlugin becomes ip_filtering_plugin) + +# You can specify additional options in the `opts` section but the `allowed_ips` option is required for this plugin demo to work +opts: + allowed_ips: # list of allowed IPs + - "" + - "" + - "" +``` diff --git a/docs/handler_plugins.md b/docs/handler_plugins.md index 41ca7aa4..fef7e3f6 100644 --- a/docs/handler_plugins.md +++ b/docs/handler_plugins.md @@ -8,6 +8,7 @@ Handler plugins are Ruby classes that extend the `Hooks::Plugins::Handlers::Base - `payload`: The webhook payload, which can be a Hash or a String. This is the data that the webhook sender sends to your endpoint. - `headers`: A Hash of HTTP headers that were sent with the webhook request. +- `env`: A modified Rack environment that contains a lot of context about the request. This includes information about the request method, path, query parameters, and more. See [`rack_env_builder.rb`](../lib/hooks/app/rack_env_builder.rb) for the complete list of available keys. - `config`: A Hash containing the endpoint configuration. This can include any additional settings or parameters that you want to use in your handler. Most of the time, this won't be used but sometimes endpoint configs add `opts` that can be useful for the handler. ```ruby @@ -28,6 +29,20 @@ class Example < Hooks::Plugins::Handlers::Base end ``` +After you write your own handler, it can be referenced in endpoint configuration files like so: + +```yaml +# example file path: config/endpoints/example.yml +path: /example_webhook +handler: example # this is the name of the handler plugin class +``` + +It should be noted that the `handler:` key in the endpoint configuration file should match the name of the handler plugin class, but in lowercase and snake case. For example, if your handler plugin class is named `ExampleHandler`, the `handler:` key in the endpoint configuration file should be `example_handler`. Here are some more examples: + +- `ExampleHandler` -> `example_handler` +- `MyCustomHandler` -> `my_custom_handler` +- `Cool2Handler` -> `cool_2_handler` + ### `payload` Parameter The `payload` parameter can be a Hash or a String. If the payload is a String, it will be parsed as JSON. If it is a Hash, it will be passed directly to the handler. The payload can contain any data that the webhook sender wants to send. diff --git a/lib/hooks/app/api.rb b/lib/hooks/app/api.rb index b9d53a39..0e3ea7b3 100644 --- a/lib/hooks/app/api.rb +++ b/lib/hooks/app/api.rb @@ -4,6 +4,7 @@ require "json" require "securerandom" require_relative "helpers" +#require_relative "network/ip_filtering" require_relative "auth/auth" require_relative "rack_env_builder" require_relative "../plugins/handlers/base" @@ -82,6 +83,13 @@ def self.create(config:, endpoints:, log:) plugin.on_request(rack_env) end + # TODO: IP filtering before processing the request if defined + # If IP filtering is enabled at either global or endpoint level, run the filtering rules + # before processing the request + #if config[:ip_filtering] || endpoint_config[:ip_filtering] + #ip_filtering!(headers, endpoint_config, config, request_context, rack_env) + #end + enforce_request_limits(config, request_context) request.body.rewind raw_body = request.body.read diff --git a/lib/hooks/app/helpers.rb b/lib/hooks/app/helpers.rb index 9aae3193..0b32ca5d 100644 --- a/lib/hooks/app/helpers.rb +++ b/lib/hooks/app/helpers.rb @@ -74,7 +74,7 @@ def parse_payload(raw_body, headers, symbolize: false) # Load handler class # - # @param handler_class_name [String] The name of the handler class to load + # @param handler_class_name [String] The name of the handler in snake_case (e.g., "github_handler") # @return [Object] An instance of the loaded handler class # @raise [StandardError] If handler cannot be found def load_handler(handler_class_name) diff --git a/lib/hooks/core/config_validator.rb b/lib/hooks/core/config_validator.rb index d2bd8791..e680d9d3 100644 --- a/lib/hooks/core/config_validator.rb +++ b/lib/hooks/core/config_validator.rb @@ -125,11 +125,12 @@ def self.valid_handler_name?(handler_name) # Must not be empty or only whitespace return false if handler_name.strip.empty? - # Must match a safe pattern: alphanumeric + underscore, starting with uppercase - return false unless handler_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/) + # Must match strict snake_case pattern: starts with lowercase, no trailing/consecutive underscores + return false unless handler_name.match?(/\A[a-z][a-z0-9]*(?:_[a-z0-9]+)*\z/) - # Must not be a system/built-in class name - return false if Hooks::Security::DANGEROUS_CLASSES.include?(handler_name) + # Convert to PascalCase for security check (since DANGEROUS_CLASSES uses PascalCase) + pascal_case_name = handler_name.split("_").map(&:capitalize).join("") + return false if Hooks::Security::DANGEROUS_CLASSES.include?(pascal_case_name) true end diff --git a/lib/hooks/core/plugin_loader.rb b/lib/hooks/core/plugin_loader.rb index 05ba1da3..7c137b81 100644 --- a/lib/hooks/core/plugin_loader.rb +++ b/lib/hooks/core/plugin_loader.rb @@ -61,11 +61,13 @@ def get_auth_plugin(plugin_name) # Get handler plugin class by name # - # @param handler_name [String] Name of the handler (e.g., "DefaultHandler", "Team1Handler") + # @param handler_name [String] Name of the handler in snake_case (e.g., "github_handler", "team_1_handler") # @return [Class] The handler plugin class # @raise [StandardError] if handler not found def get_handler_plugin(handler_name) - plugin_class = @handler_plugins[handler_name] + # Convert snake_case to PascalCase for registry lookup + pascal_case_name = handler_name.split("_").map(&:capitalize).join("") + plugin_class = @handler_plugins[pascal_case_name] unless plugin_class raise StandardError, "Handler plugin '#{handler_name}' not found. Available handlers: #{@handler_plugins.keys.join(', ')}" diff --git a/lib/hooks/plugins/auth/base.rb b/lib/hooks/plugins/auth/base.rb index 1b0f1fcc..e1cf5fa9 100644 --- a/lib/hooks/plugins/auth/base.rb +++ b/lib/hooks/plugins/auth/base.rb @@ -4,6 +4,7 @@ require_relative "../../core/log" require_relative "../../core/global_components" require_relative "../../core/component_access" +require_relative "timestamp_validator" module Hooks module Plugins @@ -53,6 +54,13 @@ def self.fetch_secret(config, secret_env_key_name: :secret_env_key) return secret.strip end + # Get timestamp validator instance + # + # @return [TimestampValidator] Singleton timestamp validator instance + def self.timestamp_validator + TimestampValidator.new + end + # Find a header value by name with case-insensitive matching # # @param headers [Hash] HTTP headers from the request diff --git a/lib/hooks/plugins/auth/hmac.rb b/lib/hooks/plugins/auth/hmac.rb index 927c5fc1..e3d5d3d1 100644 --- a/lib/hooks/plugins/auth/hmac.rb +++ b/lib/hooks/plugins/auth/hmac.rb @@ -3,7 +3,6 @@ require "openssl" require "time" require_relative "base" -require_relative "timestamp_validator" module Hooks module Plugins @@ -271,14 +270,6 @@ def self.valid_timestamp?(headers, config) timestamp_validator.valid?(timestamp_value, tolerance) end - # Get timestamp validator instance - # - # @return [TimestampValidator] Singleton timestamp validator instance - # @api private - def self.timestamp_validator - @timestamp_validator ||= TimestampValidator.new - end - # Compute HMAC signature based on configuration requirements # # Generates the expected HMAC signature for the given payload using the diff --git a/lib/hooks/version.rb b/lib/hooks/version.rb index cea51c14..a506b8d9 100644 --- a/lib/hooks/version.rb +++ b/lib/hooks/version.rb @@ -4,5 +4,5 @@ module Hooks # Current version of the Hooks webhook framework # @return [String] The version string following semantic versioning - VERSION = "0.2.1".freeze + VERSION = "0.3.0".freeze end diff --git a/spec/acceptance/acceptance_tests.rb b/spec/acceptance/acceptance_tests.rb index 6d04fd9d..7bd4dc38 100644 --- a/spec/acceptance/acceptance_tests.rb +++ b/spec/acceptance/acceptance_tests.rb @@ -400,13 +400,13 @@ def expired_unix_timestamp(seconds_ago = 600) expect_response(response, Net::HTTPSuccess) body = parse_json_response(response) expect(body["status"]).to eq("test_success") - expect(body["handler"]).to eq("TestHandler") + expect(body["handler"]).to eq("test_handler") expect(body["payload_received"]).to eq({}) expect(body["env_received"]).to have_key("REQUEST_METHOD") env = body["env_received"] expect(env["hooks.request_id"]).to be_a(String) - expect(env["hooks.handler"]).to eq("TestHandler") + expect(env["hooks.handler"]).to eq("test_handler") expect(env["hooks.endpoint_config"]).to be_a(Hash) expect(env["hooks.start_time"]).to be_a(String) expect(env["hooks.full_path"]).to eq("/webhooks/with_custom_auth_plugin") @@ -445,7 +445,7 @@ def expired_unix_timestamp(seconds_ago = 600) expect(body).to have_key("request_id") expect(body["request_id"]).to be_a(String) expect(body).to have_key("handler") - expect(body["handler"]).to eq("Boomtown") + expect(body["handler"]).to eq("boomtown") end end @@ -454,11 +454,11 @@ def expired_unix_timestamp(seconds_ago = 600) payload = {}.to_json headers = {} response = make_request(:post, "/webhooks/does_not_exist", payload, headers) - expect_response(response, Net::HTTPInternalServerError, /Handler plugin 'DoesNotExist' not found/) + expect_response(response, Net::HTTPInternalServerError, /Handler plugin 'does_not_exist' not found/) body = parse_json_response(response) expect(body["error"]).to eq("server_error") expect(body["message"]).to match( - /Handler plugin 'DoesNotExist' not found. Available handlers: DefaultHandler,.*/ + /Handler plugin 'does_not_exist' not found. Available handlers: DefaultHandler,.*/ ) end end diff --git a/spec/acceptance/config/endpoints/boomtown.yaml b/spec/acceptance/config/endpoints/boomtown.yaml index 497ed57c..40a9cbd5 100644 --- a/spec/acceptance/config/endpoints/boomtown.yaml +++ b/spec/acceptance/config/endpoints/boomtown.yaml @@ -1,3 +1,3 @@ path: /boomtown -handler: Boomtown +handler: boomtown method: post diff --git a/spec/acceptance/config/endpoints/boomtown_with_error.yml b/spec/acceptance/config/endpoints/boomtown_with_error.yml index 6713a17a..a7d7e612 100644 --- a/spec/acceptance/config/endpoints/boomtown_with_error.yml +++ b/spec/acceptance/config/endpoints/boomtown_with_error.yml @@ -1,3 +1,3 @@ path: /boomtown_with_error -handler: BoomtownWithError +handler: boomtown_with_error method: post diff --git a/spec/acceptance/config/endpoints/does_not_exist.yml b/spec/acceptance/config/endpoints/does_not_exist.yml index f5a9c5a2..d0c62184 100644 --- a/spec/acceptance/config/endpoints/does_not_exist.yml +++ b/spec/acceptance/config/endpoints/does_not_exist.yml @@ -1,2 +1,2 @@ path: /does_not_exist -handler: DoesNotExist +handler: does_not_exist diff --git a/spec/acceptance/config/endpoints/github.yaml b/spec/acceptance/config/endpoints/github.yaml index 7ba7c5f3..0ceadbcd 100644 --- a/spec/acceptance/config/endpoints/github.yaml +++ b/spec/acceptance/config/endpoints/github.yaml @@ -1,6 +1,6 @@ # Sample endpoint configuration for GitHub webhooks path: /github -handler: GithubHandler +handler: github_handler # GitHub uses HMAC SHA256 signature validation auth: diff --git a/spec/acceptance/config/endpoints/hello.yml b/spec/acceptance/config/endpoints/hello.yml index 13c4a325..02b9d9ce 100644 --- a/spec/acceptance/config/endpoints/hello.yml +++ b/spec/acceptance/config/endpoints/hello.yml @@ -1,2 +1,2 @@ path: /hello -handler: Hello +handler: hello diff --git a/spec/acceptance/config/endpoints/hmac_with_timestamp.yml b/spec/acceptance/config/endpoints/hmac_with_timestamp.yml index 87274666..2cc88fd8 100644 --- a/spec/acceptance/config/endpoints/hmac_with_timestamp.yml +++ b/spec/acceptance/config/endpoints/hmac_with_timestamp.yml @@ -1,5 +1,5 @@ path: /hmac_with_timestamp -handler: Hello +handler: hello auth: type: hmac diff --git a/spec/acceptance/config/endpoints/ip_filtering_example.yml b/spec/acceptance/config/endpoints/ip_filtering_example.yml index 698e67ec..6d8e9e84 100644 --- a/spec/acceptance/config/endpoints/ip_filtering_example.yml +++ b/spec/acceptance/config/endpoints/ip_filtering_example.yml @@ -1,5 +1,5 @@ path: /ip_filtering_example -handler: Hello +handler: hello auth: type: ip_filtering_example diff --git a/spec/acceptance/config/endpoints/okta.yaml b/spec/acceptance/config/endpoints/okta.yaml index ddffc9f8..663f8541 100644 --- a/spec/acceptance/config/endpoints/okta.yaml +++ b/spec/acceptance/config/endpoints/okta.yaml @@ -1,5 +1,5 @@ path: /okta -handler: OktaHandler +handler: okta_handler auth: type: shared_secret diff --git a/spec/acceptance/config/endpoints/okta_setup.yaml b/spec/acceptance/config/endpoints/okta_setup.yaml index 29461b92..7fddac2f 100644 --- a/spec/acceptance/config/endpoints/okta_setup.yaml +++ b/spec/acceptance/config/endpoints/okta_setup.yaml @@ -1,3 +1,3 @@ path: /okta_webhook_setup -handler: OktaSetupHandler +handler: okta_setup_handler method: get diff --git a/spec/acceptance/config/endpoints/slack.yaml b/spec/acceptance/config/endpoints/slack.yaml index b235929a..2faaaded 100644 --- a/spec/acceptance/config/endpoints/slack.yaml +++ b/spec/acceptance/config/endpoints/slack.yaml @@ -1,5 +1,5 @@ path: /slack -handler: SlackHandler +handler: slack_handler auth: type: hmac diff --git a/spec/acceptance/config/endpoints/tailscale.yaml b/spec/acceptance/config/endpoints/tailscale.yaml index 5382bfaf..567748ca 100644 --- a/spec/acceptance/config/endpoints/tailscale.yaml +++ b/spec/acceptance/config/endpoints/tailscale.yaml @@ -1,5 +1,5 @@ path: /tailscale -handler: Hello +handler: hello auth: type: hmac diff --git a/spec/acceptance/config/endpoints/team1.yaml b/spec/acceptance/config/endpoints/team1.yaml index a37e599d..b60dd3d4 100644 --- a/spec/acceptance/config/endpoints/team1.yaml +++ b/spec/acceptance/config/endpoints/team1.yaml @@ -1,6 +1,6 @@ # Sample endpoint configuration for Team 1 path: /team1 -handler: Team1Handler +handler: team_1_handler # Signature validation (optional) # auth: diff --git a/spec/acceptance/config/endpoints/with_custom_auth.yml b/spec/acceptance/config/endpoints/with_custom_auth.yml index 2d8db854..bc2cd9ed 100644 --- a/spec/acceptance/config/endpoints/with_custom_auth.yml +++ b/spec/acceptance/config/endpoints/with_custom_auth.yml @@ -1,5 +1,5 @@ path: /with_custom_auth_plugin -handler: TestHandler +handler: test_handler auth: type: example diff --git a/spec/acceptance/plugins/handlers/team1_handler.rb b/spec/acceptance/plugins/handlers/team_1_handler.rb similarity index 100% rename from spec/acceptance/plugins/handlers/team1_handler.rb rename to spec/acceptance/plugins/handlers/team_1_handler.rb diff --git a/spec/acceptance/plugins/handlers/test_handler.rb b/spec/acceptance/plugins/handlers/test_handler.rb index f4d2ee1b..4637dce7 100644 --- a/spec/acceptance/plugins/handlers/test_handler.rb +++ b/spec/acceptance/plugins/handlers/test_handler.rb @@ -4,7 +4,7 @@ class TestHandler < Hooks::Plugins::Handlers::Base def call(payload:, headers:, env:, config:) { status: "test_success", - handler: "TestHandler", + handler: "test_handler", payload_received: payload, env_received: env, config_opts: config[:opts], diff --git a/spec/integration/global_lifecycle_hooks_spec.rb b/spec/integration/global_lifecycle_hooks_spec.rb index da46b8c1..5ba3e427 100644 --- a/spec/integration/global_lifecycle_hooks_spec.rb +++ b/spec/integration/global_lifecycle_hooks_spec.rb @@ -103,7 +103,7 @@ def call(payload:, headers:, env:, config:) # Create an endpoint configuration endpoint_config_content = <<~YAML path: /integration-test - handler: IntegrationTestHandler + handler: integration_test_handler YAML File.write(File.join(temp_endpoints_dir, "integration_test.yml"), endpoint_config_content) end diff --git a/spec/integration/hooks_integration_spec.rb b/spec/integration/hooks_integration_spec.rb index 19080aa6..da06c030 100644 --- a/spec/integration/hooks_integration_spec.rb +++ b/spec/integration/hooks_integration_spec.rb @@ -33,7 +33,7 @@ def app FileUtils.mkdir_p("./spec/integration/tmp/endpoints") File.write("./spec/integration/tmp/endpoints/test.yaml", { path: "/test", - handler: "TestHandler", + handler: "test_handler", opts: { test_mode: true } }.to_yaml) diff --git a/spec/unit/app/rack_env_builder_spec.rb b/spec/unit/app/rack_env_builder_spec.rb index 829214c2..10f34698 100644 --- a/spec/unit/app/rack_env_builder_spec.rb +++ b/spec/unit/app/rack_env_builder_spec.rb @@ -16,7 +16,7 @@ let(:request_context) do { request_id: "req-123", - handler: "TestHandler" + handler: "test_handler" } end let(:endpoint_config) do @@ -86,7 +86,7 @@ it "includes Hooks-specific environment variables" do expect(result["hooks.request_id"]).to eq("req-123") - expect(result["hooks.handler"]).to eq("TestHandler") + expect(result["hooks.handler"]).to eq("test_handler") expect(result["hooks.endpoint_config"]).to eq(endpoint_config) expect(result["hooks.start_time"]).to eq("2025-06-16T10:30:45Z") expect(result["hooks.full_path"]).to eq("/api/v1/test") @@ -280,7 +280,7 @@ described_class.new( mock_request, { "Accept" => "application/vnd.api+json" }, - { request_id: "mock-123", handler: "MockHandler" }, + { request_id: "mock-123", handler: "mock_handler" }, { path: "/resource", method: "patch" }, Time.parse("2025-06-16T15:45:30Z"), "/api/v2/resource" diff --git a/spec/unit/lib/hooks/app/helpers_security_spec.rb b/spec/unit/lib/hooks/app/helpers_security_spec.rb index d01a05d2..97f13133 100644 --- a/spec/unit/lib/hooks/app/helpers_security_spec.rb +++ b/spec/unit/lib/hooks/app/helpers_security_spec.rb @@ -59,7 +59,7 @@ def env end it "successfully loads DefaultHandler" do - handler = instance.load_handler("DefaultHandler") + handler = instance.load_handler("default_handler") expect(handler).to be_an_instance_of(DefaultHandler) end end diff --git a/spec/unit/lib/hooks/app/helpers_spec.rb b/spec/unit/lib/hooks/app/helpers_spec.rb index 7e7bb663..66feb99e 100644 --- a/spec/unit/lib/hooks/app/helpers_spec.rb +++ b/spec/unit/lib/hooks/app/helpers_spec.rb @@ -307,7 +307,7 @@ def error!(message, code) end it "successfully loads DefaultHandler" do - handler = helper.load_handler("DefaultHandler") + handler = helper.load_handler("default_handler") expect(handler).to be_an_instance_of(DefaultHandler) end end diff --git a/spec/unit/lib/hooks/core/config_loader_spec.rb b/spec/unit/lib/hooks/core/config_loader_spec.rb index f9861253..bab47139 100644 --- a/spec/unit/lib/hooks/core/config_loader_spec.rb +++ b/spec/unit/lib/hooks/core/config_loader_spec.rb @@ -281,14 +281,14 @@ let(:endpoint1_config) do { "path" => "/webhook/test1", - "handler" => "TestHandler1", + "handler" => "test_handler_1", "method" => "POST" } end let(:endpoint2_config) do { "path" => "/webhook/test2", - "handler" => "TestHandler2", + "handler" => "test_handler_2", "method" => "PUT" } end @@ -304,12 +304,12 @@ expect(endpoints).to have_attributes(size: 2) expect(endpoints).to include( path: "/webhook/test1", - handler: "TestHandler1", + handler: "test_handler_1", method: "POST" ) expect(endpoints).to include( path: "/webhook/test2", - handler: "TestHandler2", + handler: "test_handler_2", method: "PUT" ) end @@ -320,7 +320,7 @@ let(:endpoint_config) do { "path" => "/webhook/json", - "handler" => "JsonHandler", + "handler" => "json_handler", "method" => "POST" } end @@ -335,7 +335,7 @@ expect(endpoints).to have_attributes(size: 1) expect(endpoints.first).to eq( path: "/webhook/json", - handler: "JsonHandler", + handler: "json_handler", method: "POST" ) end @@ -348,7 +348,7 @@ let(:valid_config) do { "path" => "/webhook/valid", - "handler" => "ValidHandler" + "handler" => "valid_handler" } end @@ -364,7 +364,7 @@ expect(endpoints).to have_attributes(size: 1) expect(endpoints.first).to eq( path: "/webhook/valid", - handler: "ValidHandler" + handler: "valid_handler" ) end it "allows environment variable setup" do diff --git a/spec/unit/lib/hooks/core/config_validator_security_spec.rb b/spec/unit/lib/hooks/core/config_validator_security_spec.rb index e49bd743..99327b73 100644 --- a/spec/unit/lib/hooks/core/config_validator_security_spec.rb +++ b/spec/unit/lib/hooks/core/config_validator_security_spec.rb @@ -8,13 +8,13 @@ context "with secure handler names" do it "accepts valid handler names" do valid_configs = [ - { path: "/webhook", handler: "MyHandler" }, - { path: "/webhook", handler: "GitHubHandler" }, - { path: "/webhook", handler: "Team1Handler" }, - { path: "/webhook", handler: "WebhookHandler" }, - { path: "/webhook", handler: "CustomWebhookHandler" }, - { path: "/webhook", handler: "Handler123" }, - { path: "/webhook", handler: "My_Handler" } + { path: "/webhook", handler: "my_handler" }, + { path: "/webhook", handler: "github_handler" }, + { path: "/webhook", handler: "team_1_handler" }, + { path: "/webhook", handler: "webhook_handler" }, + { path: "/webhook", handler: "custom_webhook_handler" }, + { path: "/webhook", handler: "handler_123" }, + { path: "/webhook", handler: "my_handler" } ] valid_configs.each do |config| @@ -26,7 +26,9 @@ it "rejects dangerous system class names" do dangerous_configs = Hooks::Security::DANGEROUS_CLASSES.map do |class_name| - { path: "/webhook", handler: class_name } + # Convert PascalCase to snake_case for config + snake_case_name = class_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "") + { path: "/webhook", handler: snake_case_name } end dangerous_configs.each do |config| @@ -38,14 +40,19 @@ it "rejects handler names with invalid format" do invalid_configs = [ - { path: "/webhook", handler: "handler" }, # lowercase start - { path: "/webhook", handler: "123Handler" }, # number start - { path: "/webhook", handler: "_Handler" }, # underscore start - { path: "/webhook", handler: "Handler$Test" }, # special characters - { path: "/webhook", handler: "Handler.Test" }, # dots - { path: "/webhook", handler: "Handler/Test" }, # slashes - { path: "/webhook", handler: "Handler Test" }, # spaces - { path: "/webhook", handler: "Handler\nTest" } # newlines + { path: "/webhook", handler: "Handler" }, # uppercase start + { path: "/webhook", handler: "123handler" }, # number start + { path: "/webhook", handler: "_handler" }, # underscore start + { path: "/webhook", handler: "handler$test" }, # special characters + { path: "/webhook", handler: "handler.test" }, # dots + { path: "/webhook", handler: "handler/test" }, # slashes + { path: "/webhook", handler: "handler test" }, # spaces + { path: "/webhook", handler: "handler\ntest" }, # newlines + { path: "/webhook", handler: "handlerTest" }, # camelCase + { path: "/webhook", handler: "HandlerTest" }, # PascalCase + { path: "/webhook", handler: "handler_" }, # trailing underscore + { path: "/webhook", handler: "my__handler" }, # consecutive underscores + { path: "/webhook", handler: "handler__test" } # consecutive underscores in middle ] invalid_configs.each do |config| @@ -88,9 +95,9 @@ context "with endpoint arrays" do it "validates all endpoints in an array and reports the problematic one" do endpoints = [ - { path: "/webhook1", handler: "ValidHandler" }, - { path: "/webhook2", handler: "File" }, # This should fail - { path: "/webhook3", handler: "AnotherValidHandler" } + { path: "/webhook1", handler: "valid_handler" }, + { path: "/webhook2", handler: "File" }, # This should fail (PascalCase) + { path: "/webhook3", handler: "another_valid_handler" } ] expect do diff --git a/spec/unit/lib/hooks/core/config_validator_spec.rb b/spec/unit/lib/hooks/core/config_validator_spec.rb index 7ec79233..545961c0 100644 --- a/spec/unit/lib/hooks/core/config_validator_spec.rb +++ b/spec/unit/lib/hooks/core/config_validator_spec.rb @@ -178,7 +178,7 @@ it "returns validated configuration with required fields only" do config = { path: "/webhook/simple", - handler: "SimpleHandler" + handler: "simple_handler" } result = described_class.validate_endpoint_config(config) @@ -189,7 +189,7 @@ it "returns validated configuration with request validator" do config = { path: "/webhook/secure", - handler: "SecureHandler", + handler: "secure_handler", auth: { type: "hmac", secret_env_key: "WEBHOOK_SECRET", @@ -211,7 +211,7 @@ it "returns validated configuration with opts hash" do config = { path: "/webhook/custom", - handler: "CustomHandler", + handler: "custom_handler", opts: { custom_option: "value", another_option: 123 @@ -226,7 +226,7 @@ it "returns validated configuration with all optional fields" do config = { path: "/webhook/full", - handler: "FullHandler", + handler: "full_handler", auth: { type: "custom_validator", secret_env_key: "SECRET", @@ -245,7 +245,7 @@ it "returns validated configuration with method specified" do config = { path: "/webhook/put", - handler: "PutHandler", + handler: "put_handler", method: "put" } @@ -260,7 +260,7 @@ valid_methods.each do |method| config = { path: "/webhook/test", - handler: "TestHandler", + handler: "test_handler", method: method } @@ -272,7 +272,7 @@ context "with invalid configuration" do it "raises ValidationError for missing path" do - config = { handler: "TestHandler" } + config = { handler: "test_handler" } expect { described_class.validate_endpoint_config(config) @@ -290,7 +290,7 @@ it "raises ValidationError for empty path" do config = { path: "", - handler: "TestHandler" + handler: "test_handler" } expect { @@ -312,7 +312,7 @@ it "raises ValidationError for non-string path" do config = { path: 123, - handler: "TestHandler" + handler: "test_handler" } expect { @@ -334,7 +334,7 @@ it "raises ValidationError for auth missing required type" do config = { path: "/webhook/test", - handler: "TestHandler", + handler: "test_handler", auth: { header: "X-Signature" } @@ -348,7 +348,7 @@ it "raises ValidationError for auth with empty type" do config = { path: "/webhook/test", - handler: "TestHandler", + handler: "test_handler", auth: { type: "" } @@ -362,7 +362,7 @@ it "raises ValidationError for non-hash auth" do config = { path: "/webhook/test", - handler: "TestHandler", + handler: "test_handler", auth: "hmac" } @@ -374,7 +374,7 @@ it "raises ValidationError for non-hash opts" do config = { path: "/webhook/test", - handler: "TestHandler", + handler: "test_handler", opts: "invalid" } @@ -386,7 +386,7 @@ it "raises ValidationError for zero timestamp_tolerance" do config = { path: "/webhook/test", - handler: "TestHandler", + handler: "test_handler", auth: { type: "hmac", timestamp_tolerance: 0 @@ -401,7 +401,7 @@ it "raises ValidationError for negative timestamp_tolerance" do config = { path: "/webhook/test", - handler: "TestHandler", + handler: "test_handler", auth: { type: "hmac", timestamp_tolerance: -100 @@ -416,7 +416,7 @@ it "raises ValidationError for empty string fields in auth" do config = { path: "/webhook/test", - handler: "TestHandler", + handler: "test_handler", auth: { type: "hmac", secret_env_key: "", @@ -433,7 +433,7 @@ it "raises ValidationError for invalid HTTP method" do config = { path: "/webhook/test", - handler: "TestHandler", + handler: "test_handler", method: "invalid" } @@ -445,7 +445,7 @@ it "raises ValidationError for non-string method" do config = { path: "/webhook/test", - handler: "TestHandler", + handler: "test_handler", method: 123 } @@ -470,7 +470,7 @@ endpoints = [ { path: "/webhook/test", - handler: "TestHandler" + handler: "test_handler" } ] @@ -483,11 +483,11 @@ endpoints = [ { path: "/webhook/test1", - handler: "TestHandler1" + handler: "test_handler1" }, { path: "/webhook/test2", - handler: "TestHandler2", + handler: "test_handler2", auth: { type: "hmac", header: "X-Hub-Signature" @@ -506,7 +506,7 @@ endpoints = [ { path: "/webhook/valid", - handler: "ValidHandler" + handler: "valid_handler" }, { path: "/webhook/invalid", @@ -514,7 +514,7 @@ }, { path: "/webhook/another", - handler: "AnotherHandler" + handler: "another_handler" } ] @@ -527,14 +527,14 @@ endpoints = [ { path: "/webhook/valid1", - handler: "ValidHandler1" + handler: "valid_handler1" }, { path: "/webhook/valid2", - handler: "ValidHandler2" + handler: "valid_handler2" }, { - handler: "InvalidHandler" + handler: "invalid_handler" # missing path } ] @@ -548,7 +548,7 @@ endpoints = [ { path: "/webhook/test", - handler: "TestHandler", + handler: "test_handler", auth: { # missing required type header: "X-Signature" diff --git a/spec/unit/lib/hooks/core/plugin_loader_spec.rb b/spec/unit/lib/hooks/core/plugin_loader_spec.rb index b85bcece..4cb6f511 100644 --- a/spec/unit/lib/hooks/core/plugin_loader_spec.rb +++ b/spec/unit/lib/hooks/core/plugin_loader_spec.rb @@ -163,12 +163,12 @@ def call(payload:, headers:, env:, config:) end it "returns built-in handler plugins" do - expect(described_class.get_handler_plugin("DefaultHandler")).to eq(DefaultHandler) + expect(described_class.get_handler_plugin("default_handler")).to eq(DefaultHandler) end it "raises error for non-existent plugin" do - expect { described_class.get_handler_plugin("NonExistentHandler") }.to raise_error( - StandardError, /Handler plugin 'NonExistentHandler' not found/ + expect { described_class.get_handler_plugin("non_existent_handler") }.to raise_error( + StandardError, /Handler plugin 'non_existent_handler' not found/ ) end end diff --git a/spec/unit/lib/hooks/handlers/base_spec.rb b/spec/unit/lib/hooks/handlers/base_spec.rb index dac84b4d..3c9519d9 100644 --- a/spec/unit/lib/hooks/handlers/base_spec.rb +++ b/spec/unit/lib/hooks/handlers/base_spec.rb @@ -100,7 +100,7 @@ def call(payload:, headers:, env:, config:) complex_config = { "endpoint" => "/test", "opts" => { "timeout" => 30 }, - "handler" => "TestHandler" + "handler" => "test_handler" } result = handler.call(payload: payload, headers: headers, env: env, config: complex_config) expect(result[:config_received]).to eq(complex_config) @@ -189,10 +189,10 @@ def call(payload:, headers:, env:, config:) it "allows stats and failbot usage in subclasses" do test_handler_class = Class.new(described_class) do def call(payload:, headers:, env:, config:) - stats.increment("handler.called", { handler: "TestHandler" }) + stats.increment("handler.called", { handler: "test_handler" }) if payload.nil? - failbot.report("Payload is nil", { handler: "TestHandler" }) + failbot.report("Payload is nil", { handler: "test_handler" }) end { status: "processed" } @@ -234,15 +234,15 @@ def report(error_or_message, context = {}) # Test with non-nil payload handler.call(payload: { "test" => "data" }, headers: {}, env: {}, config: {}) expect(collected_data).to include( - { type: :stats, action: :increment, metric: "handler.called", tags: { handler: "TestHandler" } } + { type: :stats, action: :increment, metric: "handler.called", tags: { handler: "test_handler" } } ) # Test with nil payload collected_data.clear handler.call(payload: nil, headers: {}, env: {}, config: {}) expect(collected_data).to match_array([ - { type: :stats, action: :increment, metric: "handler.called", tags: { handler: "TestHandler" } }, - { type: :failbot, action: :report, message: "Payload is nil", context: { handler: "TestHandler" } } + { type: :stats, action: :increment, metric: "handler.called", tags: { handler: "test_handler" } }, + { type: :failbot, action: :report, message: "Payload is nil", context: { handler: "test_handler" } } ]) ensure Hooks::Core::GlobalComponents.stats = original_stats diff --git a/vendor/cache/rubocop-1.76.1.gem b/vendor/cache/rubocop-1.76.1.gem deleted file mode 100644 index 024cb307..00000000 Binary files a/vendor/cache/rubocop-1.76.1.gem and /dev/null differ diff --git a/vendor/cache/rubocop-1.76.2.gem b/vendor/cache/rubocop-1.76.2.gem new file mode 100644 index 00000000..1bc1489d Binary files /dev/null and b/vendor/cache/rubocop-1.76.2.gem differ