Skip to content

Commit 8506e14

Browse files
authored
Merge pull request #26 from github/copilot/fix-25
Complete test coverage for HMAC default auth plugin
2 parents 562d723 + c0d0f45 commit 8506e14

File tree

5 files changed

+139
-8
lines changed

5 files changed

+139
-8
lines changed

script/load_test

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "net/http"
5+
require "json"
6+
require "uri"
7+
8+
# Configuration
9+
TARGET_URL = "http://0.0.0.0:8080/webhooks/hello"
10+
REQUEST_COUNT = 10_000 # Total number of requests to send
11+
EMPTY_JSON_BODY = "{}"
12+
13+
# Parse the target URL
14+
uri = URI.parse(TARGET_URL)
15+
16+
# Initialize statistics tracking
17+
response_times = []
18+
success_count = 0
19+
error_count = 0
20+
21+
puts "Starting load test..."
22+
puts "Target: #{TARGET_URL}"
23+
puts "Requests: #{REQUEST_COUNT}"
24+
puts "Payload: #{EMPTY_JSON_BODY}"
25+
puts ""
26+
27+
# Perform the load test
28+
REQUEST_COUNT.times do |i|
29+
start_time = Time.now
30+
31+
begin
32+
# Create HTTP connection
33+
http = Net::HTTP.new(uri.host, uri.port)
34+
http.use_ssl = false if uri.scheme == "http"
35+
36+
# Create POST request
37+
request = Net::HTTP::Post.new(uri.path)
38+
request["Content-Type"] = "application/json"
39+
request.body = EMPTY_JSON_BODY
40+
41+
# Send request and measure time
42+
response = http.request(request)
43+
end_time = Time.now
44+
45+
response_time_ms = ((end_time - start_time) * 1000).round(2)
46+
response_times << response_time_ms
47+
48+
if response.code.to_i >= 200 && response.code.to_i < 300
49+
success_count += 1
50+
else
51+
error_count += 1
52+
end
53+
54+
# Progress indicator
55+
if (i + 1) % 100 == 0
56+
puts "Completed #{i + 1}/#{REQUEST_COUNT} requests"
57+
end
58+
59+
rescue => e
60+
end_time = Time.now
61+
response_time_ms = ((end_time - start_time) * 1000).round(2)
62+
response_times << response_time_ms
63+
error_count += 1
64+
puts "Error on request #{i + 1}: #{e.message}"
65+
end
66+
end
67+
68+
puts ""
69+
puts "Load test completed!"
70+
puts ""
71+
72+
# Calculate statistics
73+
if response_times.any?
74+
sorted_times = response_times.sort
75+
average_time = (response_times.sum / response_times.length).round(2)
76+
min_time = sorted_times.first
77+
max_time = sorted_times.last
78+
median_time = if sorted_times.length.odd?
79+
sorted_times[sorted_times.length / 2]
80+
else
81+
((sorted_times[sorted_times.length / 2 - 1] + sorted_times[sorted_times.length / 2]) / 2.0).round(2)
82+
end
83+
84+
# Calculate percentiles
85+
p95_index = (sorted_times.length * 0.95).ceil - 1
86+
p99_index = (sorted_times.length * 0.99).ceil - 1
87+
p95_time = sorted_times[p95_index]
88+
p99_time = sorted_times[p99_index]
89+
90+
puts "=== RESULTS SUMMARY ==="
91+
puts "Total requests: #{REQUEST_COUNT}"
92+
puts "Successful requests: #{success_count}"
93+
puts "Failed requests: #{error_count}"
94+
puts "Success rate: #{((success_count.to_f / REQUEST_COUNT) * 100).round(2)}%"
95+
puts ""
96+
puts "=== RESPONSE TIME STATISTICS (ms) ==="
97+
puts "Average: #{average_time} ms"
98+
puts "Minimum: #{min_time} ms"
99+
puts "Maximum: #{max_time} ms"
100+
puts "Median: #{median_time} ms"
101+
puts "95th percentile: #{p95_time} ms"
102+
puts "99th percentile: #{p99_time} ms"
103+
else
104+
puts "No response times recorded!"
105+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
path: /hello
2+
handler: Hello

spec/acceptance/config/puma.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
require "json"
55

66
bind "tcp://0.0.0.0:8080"
7+
78
# single mode: https://github.com/puma/puma/blob/master/docs/deployment.md#single-vs-cluster-mode
89
workers 0
910

11+
threads 0, 16 # the default
12+
1013
log_formatter do |msg|
1114
timestamp = Time.now.strftime("%Y-%m-%dT%H:%M:%S.%L%z")
1215
{
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
class Hello < Hooks::Plugins::Handlers::Base
4+
def call(payload:, headers:, config:)
5+
{
6+
status: "success",
7+
handler: self.class.name,
8+
timestamp: Time.now.iso8601
9+
}
10+
end
11+
end

spec/unit/lib/hooks/plugins/auth/hmac_spec.rb

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,16 @@ def valid_with(args = {})
7070
auth: {
7171
header: header,
7272
algorithm: "sha256",
73-
format: "signature_only"
73+
format: "signature_only",
74+
secret_env_key: "HMAC_TEST_SECRET"
7475
}
7576
}
7677
end
7778
let(:signature) { OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, payload) }
7879
let(:headers) { { header => signature } }
7980

8081
it "returns true for a valid hash-only signature" do
81-
# TODO
82+
expect(valid_with(headers:, config:)).to be true
8283
end
8384

8485
it "returns false for an invalid hash-only signature" do
@@ -104,13 +105,14 @@ def valid_with(args = {})
104105
format: "version=signature",
105106
version_prefix: "v0",
106107
payload_template: payload_template,
107-
timestamp_tolerance: 300
108+
timestamp_tolerance: 300,
109+
secret_env_key: "HMAC_TEST_SECRET"
108110
}
109111
}
110112
end
111113

112114
it "returns true for a valid versioned signature with valid timestamp" do
113-
# TODO
115+
expect(valid_with(headers:, config:)).to be true
114116
end
115117

116118
it "returns false for an expired timestamp" do
@@ -153,10 +155,10 @@ def valid_with(args = {})
153155

154156
context "with missing config values" do
155157
let(:headers) { { "X-Signature" => "sha256=" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, payload) } }
156-
let(:config) { {} }
158+
let(:config) { { auth: { secret_env_key: "HMAC_TEST_SECRET" } } }
157159

158160
it "uses defaults and validates correctly" do
159-
# TODO
161+
expect(valid_with(headers:, config:)).to be true
160162
end
161163
end
162164

@@ -404,7 +406,8 @@ def valid_with(args = {})
404406
format: "version=signature",
405407
version_prefix: "v0",
406408
payload_template: "v0:{timestamp}:{body}",
407-
timestamp_tolerance: 300
409+
timestamp_tolerance: 300,
410+
secret_env_key: "HMAC_TEST_SECRET"
408411
}
409412
}
410413
end
@@ -464,7 +467,14 @@ def valid_with(args = {})
464467
end
465468

466469
it "returns true when timestamp header name case differs due to normalization" do
467-
# TODO
470+
timestamp = Time.now.to_i.to_s
471+
signing_payload = "v0:#{timestamp}:#{payload}"
472+
signature = "v0=" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, signing_payload)
473+
474+
# Use uppercase timestamp header name in the request headers
475+
headers = { header => signature, timestamp_header.upcase => timestamp }
476+
477+
expect(valid_with(headers:, config: base_config)).to be true
468478
end
469479
end
470480

0 commit comments

Comments
 (0)