Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions lib/shopify_api/webhooks/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,27 @@ class Request

sig { override.returns(String) }
def hmac
Digest.hexencode(Base64.decode64(T.cast(@headers["x-shopify-hmac-sha256"], String)))
Digest.hexencode(Base64.decode64(T.cast(shopify_header("hmac-sha256"), String)))
end

sig { returns(String) }
def topic
T.cast(@headers["x-shopify-topic"], String)
T.cast(shopify_header("topic"), String)
end

sig { returns(String) }
def shop
T.cast(@headers["x-shopify-shop-domain"], String)
T.cast(shopify_header("shop-domain"), String)
end

sig { returns(String) }
def api_version
T.cast(@headers["x-shopify-api-version"], String)
T.cast(shopify_header("api-version"), String)
end

sig { returns(String) }
def webhook_id
T.cast(@headers["x-shopify-webhook-id"], String)
T.cast(shopify_header("webhook-id"), String)
end

sig { override.returns(String) }
Expand All @@ -48,11 +48,11 @@ def initialize(raw_body:, headers:)
headers = headers.to_h { |k, v| [k.to_s.downcase.sub("http_", "").gsub("_", "-"), v] }

missing_headers = []
[
"x-shopify-topic",
"x-shopify-hmac-sha256",
"x-shopify-shop-domain",
].each { |header| missing_headers << header unless headers.include?(header) }
["topic", "hmac-sha256", "shop-domain"].each do |name|
unless headers.key?("shopify-#{name}") || headers.key?("x-shopify-#{name}")
missing_headers << "shopify-#{name} or x-shopify-#{name}"
end
end
unless missing_headers.empty?
raise Errors::InvalidWebhookError,
"Missing one or more of the required HTTP headers to process webhooks: #{missing_headers}"
Expand All @@ -61,6 +61,13 @@ def initialize(raw_body:, headers:)
@headers = headers
@raw_body = raw_body
end

private

sig { params(name: String).returns(T.untyped) }
def shopify_header(name)
@headers["shopify-#{name}"] || @headers["x-shopify-#{name}"]
end
end
end
end
38 changes: 38 additions & 0 deletions test/webhooks/registry_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,44 @@ def test_process_with_response_as_struct
assert(handler_called)
end

def test_process_with_new_format_headers
handler_called = false

handler = TestHelpers::FakeWebhookHandler.new(
lambda do |data|
assert_equal(@topic, data.topic)
assert_equal(@shop, data.shop)
assert_equal({}, data.body)
assert_equal("b1234-eefd-4c9e-9520-049845a02082", data.webhook_id)
assert_equal("2024-01", data.api_version)
handler_called = true
end,
)

ShopifyAPI::Webhooks::Registry.add_registration(
topic: @topic, path: "path", delivery_method: :http, handler: handler,
)

hmac = OpenSSL::HMAC.digest(
OpenSSL::Digest.new("sha256"),
ShopifyAPI::Context.api_secret_key,
"{}",
)

new_format_headers = {
"shopify-topic" => @topic,
"shopify-hmac-sha256" => Base64.encode64(hmac),
"shopify-shop-domain" => @shop,
"shopify-webhook-id" => "b1234-eefd-4c9e-9520-049845a02082",
"shopify-api-version" => "2024-01",
}

webhook_request = ShopifyAPI::Webhooks::Request.new(raw_body: "{}", headers: new_format_headers)
ShopifyAPI::Webhooks::Registry.process(webhook_request)

assert(handler_called)
end

def test_process_hmac_validation_fails
headers = {
"x-shopify-topic" => "some/topic",
Expand Down
52 changes: 52 additions & 0 deletions test/webhooks/request_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,58 @@ def test_with_symbol_headers

assert(ShopifyAPI::Webhooks::Request.new(raw_body: "{}", headers: headers))
end

def test_create_webhook_request_with_new_format_headers
headers = {
"shopify-topic" => "some/topic",
"shopify-hmac-sha256" => "some_hmac",
"shopify-shop-domain" => "shop.myshopify.com",
}

assert(ShopifyAPI::Webhooks::Request.new(raw_body: "{}", headers: headers))
end

def test_create_webhook_request_with_both_header_formats
headers = {
"x-shopify-topic" => "some/topic",
"x-shopify-hmac-sha256" => "some_hmac",
"x-shopify-shop-domain" => "shop.myshopify.com",
"shopify-topic" => "some/topic",
"shopify-hmac-sha256" => "some_hmac",
"shopify-shop-domain" => "shop.myshopify.com",
}

assert(ShopifyAPI::Webhooks::Request.new(raw_body: "{}", headers: headers))
end

def test_accessor_values_with_new_format_headers
hmac_value = Base64.encode64("test_hmac_bytes")
headers = {
"shopify-topic" => "orders/create",
"shopify-hmac-sha256" => hmac_value,
"shopify-shop-domain" => "test-shop.myshopify.com",
"shopify-api-version" => "2024-01",
"shopify-webhook-id" => "b1234-eefd-4c9e-9520-049845a02082",
}

request = ShopifyAPI::Webhooks::Request.new(raw_body: "{}", headers: headers)

assert_equal("orders/create", request.topic)
assert_equal("test-shop.myshopify.com", request.shop)
assert_equal("2024-01", request.api_version)
assert_equal("b1234-eefd-4c9e-9520-049845a02082", request.webhook_id)
assert_equal(Digest.hexencode(Base64.decode64(hmac_value)), request.hmac)
end

def test_error_when_headers_missing_in_either_format
error = assert_raises(ShopifyAPI::Errors::InvalidWebhookError) do
ShopifyAPI::Webhooks::Request.new(raw_body: "{}", headers: {})
end

assert_includes(error.message, "shopify-topic or x-shopify-topic")
assert_includes(error.message, "shopify-hmac-sha256 or x-shopify-hmac-sha256")
assert_includes(error.message, "shopify-shop-domain or x-shopify-shop-domain")
end
end
end
end