diff --git a/Gemfile.lock b/Gemfile.lock index 5461111..0de8f5f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - hooks-ruby (0.3.2) + hooks-ruby (0.4.0) dry-schema (~> 1.14, >= 1.14.1) grape (~> 2.3) puma (~> 6.6) diff --git a/docs/configuration.md b/docs/configuration.md index 3f719a2..fc17d9c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -103,7 +103,15 @@ When set to `true`, enables a catch-all route that will handle requests to unkno When set to `true`, normalizes incoming HTTP headers by lowercasing and trimming them. This ensures consistency in header names and values. -**Default:** `true` +**Default:** `true` + +### `default_format` + +Sets the default response format when no specific format is requested. + +**Default:** `json` +**Valid values:** `json`, `txt`, `xml`, `any` +**Example:** `json` ## Endpoint Options diff --git a/docs/handler_plugins.md b/docs/handler_plugins.md index fef7e3f..0145747 100644 --- a/docs/handler_plugins.md +++ b/docs/handler_plugins.md @@ -4,13 +4,17 @@ This document provides in-depth information about handler plugins and how you ca ## Writing a Handler Plugin -Handler plugins are Ruby classes that extend the `Hooks::Plugins::Handlers::Base` class. They are used to process webhook payloads and can do anything you want. They follow a very simple interface that allows you to define a `call` method that takes three parameters: `payload`, `headers`, and `config`. The `call` method should return a hash with the response data. The hash that this method returns will be sent back to the webhook sender as a JSON response. +Handler plugins are Ruby classes that extend the `Hooks::Plugins::Handlers::Base` class. They are used to process webhook payloads and can do anything you want. They follow a very simple interface that allows you to define a `call` method that takes four parameters: `payload`, `headers`, `env`, and `config`. + +**Important:** The `call` method should return a hash by default. Since the server now defaults to JSON format, any hash returned by the handler will be automatically converted to JSON with the correct `Content-Type: application/json` headers set by Grape. This ensures consistent API responses and proper JSON serialization. - `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. +The method should return a **hash** that will be automatically serialized to JSON format with appropriate headers. The server defaults to JSON format for both input and output processing. + ```ruby # example file path: plugins/handlers/example.rb class Example < Hooks::Plugins::Handlers::Base @@ -18,12 +22,15 @@ class Example < Hooks::Plugins::Handlers::Base # # @param payload [Hash, String] webhook payload (pure JSON with string keys) # @param headers [Hash] HTTP headers (string keys, optionally normalized - default is normalized) - # @param env [Hash] A modifed Rack environment that contains a lot of context about the request + # @param env [Hash] A modified Rack environment that contains a lot of context about the request # @param config [Hash] Endpoint configuration - # @return [Hash] Response data + # @return [Hash] Response data (automatically converted to JSON) def call(payload:, headers:, env:, config:) + # Return a hash - it will be automatically converted to JSON with proper headers return { - status: "success" + status: "success", + message: "webhook processed successfully", + timestamp: Time.now.iso8601 } end end @@ -43,6 +50,31 @@ It should be noted that the `handler:` key in the endpoint configuration file sh - `MyCustomHandler` -> `my_custom_handler` - `Cool2Handler` -> `cool_2_handler` +## Default JSON Format + +By default, the Hooks server uses JSON format for both input and output processing. This means: + +- **Input**: Webhook payloads are parsed as JSON and passed to handlers as Ruby hashes +- **Output**: Handler return values (hashes) are automatically converted to JSON responses with `Content-Type: application/json` headers +- **Error Responses**: Authentication failures and handler errors return structured JSON responses + +**Best Practice**: Always return a hash from your handler's `call` method. The hash will be automatically serialized to JSON and sent to the webhook sender with proper headers. This ensures consistent API responses and proper JSON formatting. + +Example response format: + +```json +{ + "status": "success", + "message": "webhook processed successfully", + "data": { + "processed_at": "2023-10-01T12:34:56Z", + "items_processed": 5 + } +} +``` + +> **Note**: The JSON format behavior can be configured using the `default_format` option in your global configuration. See the [Configuration documentation](./configuration.md) for more details. + ### `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. @@ -159,6 +191,8 @@ The `log.debug`, `log.info`, `log.warn`, and `log.error` methods are available i All handler plugins have access to the `error!` method, which is used to raise an error with a specific message and HTTP status code. This is useful for returning error responses to the webhook sender. +When using `error!` with the default JSON format, both hash and string responses are handled appropriately: + ```ruby class Example < Hooks::Plugins::Handlers::Base # Example webhook handler @@ -167,11 +201,12 @@ class Example < Hooks::Plugins::Handlers::Base # @param headers [Hash] HTTP headers # @param env [Hash] A modified Rack environment that contains a lot of context about the request # @param config [Hash] Endpoint configuration - # @return [Hash] Response data + # @return [Hash] Response data (automatically converted to JSON) def call(payload:, headers:, env:, config:) if payload.nil? || payload.empty? log.error("Payload is empty or nil") + # String errors are JSON-encoded with default format error!("Payload cannot be empty or nil", 400) end @@ -182,7 +217,7 @@ class Example < Hooks::Plugins::Handlers::Base end ``` -You can also use the `error!` method to return a JSON response as well: +**Recommended approach**: Use hash-based error responses for consistent JSON structure: ```ruby class Example < Hooks::Plugins::Handlers::Base @@ -190,13 +225,14 @@ class Example < Hooks::Plugins::Handlers::Base if payload.nil? || payload.empty? log.error("Payload is empty or nil") + # Hash errors are automatically converted to JSON error!({ error: "payload_empty", message: "the payload cannot be empty or nil", success: false, custom_value: "some_custom_value", request_id: env["hooks.request_id"] - }, 500) + }, 400) end return { @@ -206,6 +242,18 @@ class Example < Hooks::Plugins::Handlers::Base end ``` +This will return a properly formatted JSON error response: + +```json +{ + "error": "payload_empty", + "message": "the payload cannot be empty or nil", + "success": false, + "custom_value": "some_custom_value", + "request_id": "uuid-here" +} +``` + ### `#Retryable.with_context(:default)` This method uses a default `Retryable` context to handle retries. It is used to wrap the execution of a block of code that may need to be retried in case of failure. @@ -220,7 +268,7 @@ class Example < Hooks::Plugins::Handlers::Base # @param headers [Hash] HTTP headers # @param env [Hash] A modified Rack environment that contains a lot of context about the request # @param config [Hash] Endpoint configuration - # @return [Hash] Response data + # @return [Hash] Response data (automatically converted to JSON) def call(payload:, headers:, env:, config:) result = Retryable.with_context(:default) do some_operation_that_might_fail() @@ -229,7 +277,9 @@ class Example < Hooks::Plugins::Handlers::Base log.debug("operation result: #{result}") return { - status: "success" + status: "success", + operation_result: result, + processed_at: Time.now.iso8601 } end end diff --git a/lib/hooks/app/api.rb b/lib/hooks/app/api.rb index 131ff79..5ee0c0d 100644 --- a/lib/hooks/app/api.rb +++ b/lib/hooks/app/api.rb @@ -39,8 +39,7 @@ def self.create(config:, endpoints:, log:) content_type :xml, "application/xml" content_type :any, "*/*" - format :txt # TODO: make this configurable via config[:format] (defaults to :json in the future) - default_format :txt # TODO: make this configurable via config[:default_format] (defaults to :json in the future) + default_format config[:default_format] || :json end api_class.class_eval do @@ -118,22 +117,21 @@ def self.create(config:, endpoints:, log:) log.info("successfully processed webhook event with handler: #{handler_class_name}") log.debug("processing duration: #{Time.now - start_time}s") status 200 - content_type "application/json" - response.to_json + response rescue Hooks::Plugins::Handlers::Error => e # Handler called error! method - immediately return error response and exit the request log.debug("handler #{handler_class_name} called `error!` method") - error_response = nil - status e.status case e.body when String + # if error! was called with a string, we assume it's a simple text error + # example: error!("simple text error", 400) -> should return a plain text response content_type "text/plain" error_response = e.body else - content_type "application/json" - error_response = e.body.to_json + # Let Grape handle JSON conversion with the default format + error_response = e.body end return error_response @@ -164,8 +162,7 @@ def self.create(config:, endpoints:, log:) error_response[:handler] = handler_class_name unless config[:production] status determine_error_code(e) - content_type "application/json" - error_response.to_json + error_response end end end diff --git a/lib/hooks/app/endpoints/catchall.rb b/lib/hooks/app/endpoints/catchall.rb index e240e2c..897c24d 100644 --- a/lib/hooks/app/endpoints/catchall.rb +++ b/lib/hooks/app/endpoints/catchall.rb @@ -17,6 +17,13 @@ module App class CatchallEndpoint < Grape::API include Hooks::App::Helpers + # Set up content types and default format to JSON to match main API + content_type :json, "application/json" + content_type :txt, "text/plain" + content_type :xml, "application/xml" + content_type :any, "*/*" + default_format :json + def self.mount_path(config) # :nocov: "#{config[:root_path]}/*path" @@ -81,8 +88,7 @@ def self.route_block(captured_config, captured_logger) log.info("successfully processed webhook event with handler: #{handler_class_name}") log.debug("processing duration: #{Time.now - start_time}s") status 200 - content_type "application/json" - response.to_json + response rescue StandardError => e err_msg = "Error processing webhook event with handler: #{handler_class_name} - #{e.message} " \ "- request_id: #{request_id} - path: #{full_path} - method: #{http_method} - " \ @@ -102,8 +108,7 @@ def self.route_block(captured_config, captured_logger) error_response[:handler] = handler_class_name unless config[:production] status determine_error_code(e) - content_type "application/json" - error_response.to_json + error_response end end end diff --git a/lib/hooks/app/endpoints/health.rb b/lib/hooks/app/endpoints/health.rb index e8c27ba..468bdc7 100644 --- a/lib/hooks/app/endpoints/health.rb +++ b/lib/hooks/app/endpoints/health.rb @@ -6,14 +6,20 @@ module Hooks module App class HealthEndpoint < Grape::API + # Set up content types and default format to JSON + content_type :json, "application/json" + content_type :txt, "text/plain" + content_type :xml, "application/xml" + content_type :any, "*/*" + default_format :json + get do - content_type "application/json" { status: "healthy", timestamp: Time.now.utc.iso8601, version: Hooks::VERSION, uptime_seconds: (Time.now - Hooks::App::API.server_start_time).to_i - }.to_json + } end end end diff --git a/lib/hooks/app/endpoints/version.rb b/lib/hooks/app/endpoints/version.rb index d88dbbc..414c1d8 100644 --- a/lib/hooks/app/endpoints/version.rb +++ b/lib/hooks/app/endpoints/version.rb @@ -6,12 +6,18 @@ module Hooks module App class VersionEndpoint < Grape::API + # Set up content types and default format to JSON + content_type :json, "application/json" + content_type :txt, "text/plain" + content_type :xml, "application/xml" + content_type :any, "*/*" + default_format :json + get do - content_type "application/json" { version: Hooks::VERSION, timestamp: Time.now.utc.iso8601 - }.to_json + } end end end diff --git a/lib/hooks/core/config_loader.rb b/lib/hooks/core/config_loader.rb index 69fd677..3125b08 100644 --- a/lib/hooks/core/config_loader.rb +++ b/lib/hooks/core/config_loader.rb @@ -20,7 +20,8 @@ class ConfigLoader production: true, endpoints_dir: "./config/endpoints", use_catchall_route: false, - normalize_headers: true + normalize_headers: true, + default_format: :json }.freeze SILENCE_CONFIG_LOADER_MESSAGES = ENV.fetch( @@ -141,6 +142,7 @@ def self.load_env_config "HOOKS_ENDPOINTS_DIR" => :endpoints_dir, "HOOKS_USE_CATCHALL_ROUTE" => :use_catchall_route, "HOOKS_NORMALIZE_HEADERS" => :normalize_headers, + "HOOKS_DEFAULT_FORMAT" => :default_format, "HOOKS_SOME_STRING_VAR" => :some_string_var # Added for test } @@ -155,6 +157,9 @@ def self.load_env_config when :use_catchall_route, :normalize_headers # Convert string to boolean env_config[config_key] = %w[true 1 yes on].include?(value.downcase) + when :default_format + # Convert string to symbol + env_config[config_key] = value.to_sym else env_config[config_key] = value end diff --git a/lib/hooks/core/config_validator.rb b/lib/hooks/core/config_validator.rb index c53fe65..0e7807b 100644 --- a/lib/hooks/core/config_validator.rb +++ b/lib/hooks/core/config_validator.rb @@ -27,6 +27,7 @@ class ValidationError < StandardError; end optional(:endpoints_dir).filled(:string) optional(:use_catchall_route).filled(:bool) optional(:normalize_headers).filled(:bool) + optional(:default_format).filled(:symbol, included_in?: %i[json txt xml any]) optional(:ip_filtering).hash do optional(:ip_header).filled(:string) diff --git a/lib/hooks/version.rb b/lib/hooks/version.rb index 7dba295..0420f04 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.3.2".freeze + VERSION = "0.4.0".freeze end diff --git a/spec/acceptance/acceptance_tests.rb b/spec/acceptance/acceptance_tests.rb index 00aa427..c3e7e3f 100644 --- a/spec/acceptance/acceptance_tests.rb +++ b/spec/acceptance/acceptance_tests.rb @@ -28,6 +28,18 @@ def expect_response(response, expected_type, expected_body_content = nil) expect(response.body).to include(expected_body_content) if expected_body_content end + def expect_json_auth_failure(response, expected_request_id: nil) + expect(response).to be_a(Net::HTTPUnauthorized) + expect(response.content_type).to eq("application/json") + + body = parse_json_response(response) + expect(body["error"]).to eq("authentication_failed") + expect(body["message"]).to eq("authentication failed") + expect(body).to have_key("request_id") + expect(body["request_id"]).to be_a(String) + expect(body["request_id"]).to eq(expected_request_id) if expected_request_id + end + def parse_json_response(response) JSON.parse(response.body) end @@ -140,13 +152,13 @@ def expired_unix_timestamp(seconds_ago = 600) headers = json_headers("X-Hub-Signature-256" => "sha256=invalidsignature") response = make_request(:post, "/webhooks/github", payload.to_json, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "receives a POST request but there is no HMAC related header" do payload = { action: "push", repository: { name: "test-repo" } } response = make_request(:post, "/webhooks/github", payload.to_json, json_headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "receives a POST request but it uses the wrong algo" do @@ -155,7 +167,7 @@ def expired_unix_timestamp(seconds_ago = 600) signature = generate_hmac_signature(json_payload, FAKE_HMAC_SECRET, "sha512", "sha512=") headers = json_headers("X-Hub-Signature-256" => signature) response = make_request(:post, "/webhooks/github", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "successfully processes a valid POST request with HMAC signature" do @@ -212,7 +224,7 @@ def expired_unix_timestamp(seconds_ago = 600) signature = generate_hmac_with_timestamp(json_payload, "bad-hmac-secret", timestamp) headers = json_headers("X-HMAC-Signature" => signature, "X-HMAC-Timestamp" => timestamp) response = make_request(:post, "/webhooks/hmac_with_timestamp", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "fails due to missing timestamp header" do @@ -221,7 +233,7 @@ def expired_unix_timestamp(seconds_ago = 600) signature = generate_hmac_with_timestamp(json_payload, FAKE_ALT_HMAC_SECRET, current_timestamp) headers = json_headers("X-HMAC-Signature" => signature) response = make_request(:post, "/webhooks/hmac_with_timestamp", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "fails due to invalid timestamp format" do @@ -231,7 +243,7 @@ def expired_unix_timestamp(seconds_ago = 600) signature = generate_hmac_with_timestamp(json_payload, FAKE_ALT_HMAC_SECRET, invalid_timestamp) headers = json_headers("X-HMAC-Signature" => signature, "X-HMAC-Timestamp" => invalid_timestamp) response = make_request(:post, "/webhooks/hmac_with_timestamp", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "rejects request with timestamp manipulation attack" do @@ -244,7 +256,7 @@ def expired_unix_timestamp(seconds_ago = 600) signature = generate_hmac_with_timestamp(json_payload, FAKE_ALT_HMAC_SECRET, original_timestamp) headers = json_headers("X-HMAC-Signature" => signature, "X-HMAC-Timestamp" => manipulated_timestamp) response = make_request(:post, "/webhooks/hmac_with_timestamp", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "fails because the timestamp is too old" do @@ -254,7 +266,7 @@ def expired_unix_timestamp(seconds_ago = 600) signature = generate_hmac_with_timestamp(json_payload, FAKE_ALT_HMAC_SECRET, expired_ts) headers = json_headers("X-HMAC-Signature" => signature, "X-HMAC-Timestamp" => expired_ts) response = make_request(:post, "/webhooks/hmac_with_timestamp", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "fails because the wrong HMAC algorithm is used" do @@ -265,7 +277,7 @@ def expired_unix_timestamp(seconds_ago = 600) signature = signature.gsub("sha256=", "sha512=") headers = json_headers("X-HMAC-Signature" => signature, "X-HMAC-Timestamp" => timestamp) response = make_request(:post, "/webhooks/hmac_with_timestamp", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end end @@ -289,7 +301,7 @@ def expired_unix_timestamp(seconds_ago = 600) signature = generate_slack_signature(json_payload, FAKE_ALT_HMAC_SECRET, expired_ts) headers = json_headers("Signature-256" => signature, "X-Timestamp" => expired_ts) response = make_request(:post, "/webhooks/slack", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "rejects request with missing timestamp header" do @@ -299,7 +311,7 @@ def expired_unix_timestamp(seconds_ago = 600) signature = generate_slack_signature(json_payload, FAKE_ALT_HMAC_SECRET, timestamp) headers = json_headers("Signature-256" => signature) response = make_request(:post, "/webhooks/slack", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "rejects request with invalid timestamp format" do @@ -309,7 +321,7 @@ def expired_unix_timestamp(seconds_ago = 600) signature = generate_slack_signature(json_payload, FAKE_ALT_HMAC_SECRET, invalid_timestamp) headers = json_headers("Signature-256" => signature, "X-Timestamp" => invalid_timestamp) response = make_request(:post, "/webhooks/slack", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "successfully processes request with ISO 8601 UTC timestamp" do @@ -355,7 +367,7 @@ def expired_unix_timestamp(seconds_ago = 600) signature = generate_slack_signature(json_payload, FAKE_ALT_HMAC_SECRET, non_utc_timestamp) headers = json_headers("Signature-256" => signature, "X-Timestamp" => non_utc_timestamp) response = make_request(:post, "/webhooks/slack", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "rejects request with timestamp manipulation attack" do @@ -368,7 +380,7 @@ def expired_unix_timestamp(seconds_ago = 600) signature = generate_slack_signature(json_payload, FAKE_ALT_HMAC_SECRET, original_timestamp) headers = json_headers("Signature-256" => signature, "X-Timestamp" => manipulated_timestamp) response = make_request(:post, "/webhooks/slack", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end end @@ -377,7 +389,7 @@ def expired_unix_timestamp(seconds_ago = 600) payload = { event: "user.login", user: { id: "12345" } } headers = json_headers("Authorization" => "badvalue") response = make_request(:post, "/webhooks/okta", payload.to_json, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "successfully processes a valid POST request with shared secret" do @@ -420,14 +432,14 @@ def expired_unix_timestamp(seconds_ago = 600) payload = {}.to_json headers = { "Authorization" => "Bearer wrong-secret" } response = make_request(:post, "/webhooks/with_custom_auth_plugin", payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "rejects requests with missing credentials using custom auth plugin" do payload = {}.to_json headers = {} response = make_request(:post, "/webhooks/with_custom_auth_plugin", payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end end @@ -518,12 +530,12 @@ def expired_unix_timestamp(seconds_ago = 600) it "sends a POST request to the /webhooks/boomtown_with_error endpoint and it explodes with a simple text error" do payload = { boom_simple_text: true }.to_json response = make_request(:post, "/webhooks/boomtown_with_error", payload, json_headers) - expect_response(response, Net::HTTPInternalServerError, "boomtown_with_error: the payload triggered a simple text boomtown error") + expect_response(response, Net::HTTPInternalServerError) - body = response.body - expect(body).to eq("boomtown_with_error: the payload triggered a simple text boomtown error") expect(response.content_type).to eq("text/plain") expect(response.code).to eq("500") + + expect(response.body).to match(/boomtown_with_error: the payload triggered a simple text boomtown error/) end end @@ -546,14 +558,14 @@ def expired_unix_timestamp(seconds_ago = 600) json_payload = payload.to_json headers = json_headers("Tailscale-Webhook-Signature" => "t=1663781880,v1=invalidsignature") response = make_request(:post, "/webhooks/tailscale", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "rejects request with missing signature header" do payload = { event: "user.login", user: { id: "12345" } } json_payload = payload.to_json response = make_request(:post, "/webhooks/tailscale", json_payload, json_headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end it "rejects request with wrong signature algorithm" do @@ -565,7 +577,7 @@ def expired_unix_timestamp(seconds_ago = 600) wrong_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha1"), FAKE_ALT_HMAC_SECRET, signing_payload) headers = json_headers("Tailscale-Webhook-Signature" => "t=#{timestamp},v1=#{wrong_signature}") response = make_request(:post, "/webhooks/tailscale", json_payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end end @@ -584,7 +596,7 @@ def expired_unix_timestamp(seconds_ago = 600) payload = {}.to_json headers = { "Content-Type" => "application/json", "X-Forwarded-For" => "123.456.789.000" } response = make_request(:post, "/webhooks/ip_filtering_example", payload, headers) - expect_response(response, Net::HTTPUnauthorized, "authentication failed") + expect_json_auth_failure(response) end end diff --git a/spec/acceptance/config/hooks.yaml b/spec/acceptance/config/hooks.yaml index 474691d..831614e 100644 --- a/spec/acceptance/config/hooks.yaml +++ b/spec/acceptance/config/hooks.yaml @@ -6,6 +6,8 @@ instruments_plugin_dir: ./spec/acceptance/plugins/instruments log_level: debug +default_format: json # default response format for the server + # Request handling request_limit: 1048576 # 1MB max body size request_timeout: 15 # 15 seconds timeout diff --git a/spec/unit/lib/hooks/core/config_loader_spec.rb b/spec/unit/lib/hooks/core/config_loader_spec.rb index bab4713..95c2072 100644 --- a/spec/unit/lib/hooks/core/config_loader_spec.rb +++ b/spec/unit/lib/hooks/core/config_loader_spec.rb @@ -19,7 +19,8 @@ production: true, endpoints_dir: "./config/endpoints", use_catchall_route: false, - normalize_headers: true + normalize_headers: true, + default_format: :json ) end end @@ -197,6 +198,14 @@ expect(config[:normalize_headers]).to be true expect(config[:some_string_var]).to eq("test_value") # Check the string var end + + it "converts format environment variables to symbols" do + ENV["HOOKS_DEFAULT_FORMAT"] = "xml" + + config = described_class.load + + expect(config[:default_format]).to eq(:xml) + end end context "with auth plugin directory configuration" do