Skip to content
Merged
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
105 changes: 105 additions & 0 deletions script/load_test
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "net/http"
require "json"
require "uri"

# Configuration
TARGET_URL = "http://0.0.0.0:8080/webhooks/hello"
REQUEST_COUNT = 10_000 # Total number of requests to send
EMPTY_JSON_BODY = "{}"

# Parse the target URL
uri = URI.parse(TARGET_URL)

# Initialize statistics tracking
response_times = []
success_count = 0
error_count = 0

puts "Starting load test..."
puts "Target: #{TARGET_URL}"
puts "Requests: #{REQUEST_COUNT}"
puts "Payload: #{EMPTY_JSON_BODY}"
puts ""

# Perform the load test
REQUEST_COUNT.times do |i|
start_time = Time.now

begin
# Create HTTP connection
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = false if uri.scheme == "http"

# Create POST request
request = Net::HTTP::Post.new(uri.path)
request["Content-Type"] = "application/json"
request.body = EMPTY_JSON_BODY

# Send request and measure time
response = http.request(request)
end_time = Time.now

response_time_ms = ((end_time - start_time) * 1000).round(2)
response_times << response_time_ms

if response.code.to_i >= 200 && response.code.to_i < 300
success_count += 1
else
error_count += 1
end

# Progress indicator
if (i + 1) % 100 == 0
puts "Completed #{i + 1}/#{REQUEST_COUNT} requests"
end

rescue => e
end_time = Time.now
response_time_ms = ((end_time - start_time) * 1000).round(2)
response_times << response_time_ms
error_count += 1
puts "Error on request #{i + 1}: #{e.message}"
end
end

puts ""
puts "Load test completed!"
puts ""

# Calculate statistics
if response_times.any?
sorted_times = response_times.sort
average_time = (response_times.sum / response_times.length).round(2)
min_time = sorted_times.first
max_time = sorted_times.last
median_time = if sorted_times.length.odd?
sorted_times[sorted_times.length / 2]
else
((sorted_times[sorted_times.length / 2 - 1] + sorted_times[sorted_times.length / 2]) / 2.0).round(2)
end

# Calculate percentiles
p95_index = (sorted_times.length * 0.95).ceil - 1
p99_index = (sorted_times.length * 0.99).ceil - 1
p95_time = sorted_times[p95_index]
p99_time = sorted_times[p99_index]

puts "=== RESULTS SUMMARY ==="
puts "Total requests: #{REQUEST_COUNT}"
puts "Successful requests: #{success_count}"
puts "Failed requests: #{error_count}"
puts "Success rate: #{((success_count.to_f / REQUEST_COUNT) * 100).round(2)}%"
puts ""
puts "=== RESPONSE TIME STATISTICS (ms) ==="
puts "Average: #{average_time} ms"
puts "Minimum: #{min_time} ms"
puts "Maximum: #{max_time} ms"
puts "Median: #{median_time} ms"
puts "95th percentile: #{p95_time} ms"
puts "99th percentile: #{p99_time} ms"
else
puts "No response times recorded!"
end
2 changes: 2 additions & 0 deletions spec/acceptance/config/endpoints/hello.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
path: /hello
handler: Hello
3 changes: 3 additions & 0 deletions spec/acceptance/config/puma.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
require "json"

bind "tcp://0.0.0.0:8080"

# single mode: https://github.com/puma/puma/blob/master/docs/deployment.md#single-vs-cluster-mode
workers 0

threads 0, 16 # the default

log_formatter do |msg|
timestamp = Time.now.strftime("%Y-%m-%dT%H:%M:%S.%L%z")
{
Expand Down
11 changes: 11 additions & 0 deletions spec/acceptance/plugins/handlers/hello.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class Hello < Hooks::Plugins::Handlers::Base
def call(payload:, headers:, config:)
{
status: "success",
handler: self.class.name,
timestamp: Time.now.iso8601
}
end
end
26 changes: 18 additions & 8 deletions spec/unit/lib/hooks/plugins/auth/hmac_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,16 @@ def valid_with(args = {})
auth: {
header: header,
algorithm: "sha256",
format: "signature_only"
format: "signature_only",
secret_env_key: "HMAC_TEST_SECRET"
}
}
end
let(:signature) { OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, payload) }
let(:headers) { { header => signature } }

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

it "returns false for an invalid hash-only signature" do
Expand All @@ -104,13 +105,14 @@ def valid_with(args = {})
format: "version=signature",
version_prefix: "v0",
payload_template: payload_template,
timestamp_tolerance: 300
timestamp_tolerance: 300,
secret_env_key: "HMAC_TEST_SECRET"
}
}
end

it "returns true for a valid versioned signature with valid timestamp" do
# TODO
expect(valid_with(headers:, config:)).to be true
end

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

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

it "uses defaults and validates correctly" do
# TODO
expect(valid_with(headers:, config:)).to be true
end
end

Expand Down Expand Up @@ -404,7 +406,8 @@ def valid_with(args = {})
format: "version=signature",
version_prefix: "v0",
payload_template: "v0:{timestamp}:{body}",
timestamp_tolerance: 300
timestamp_tolerance: 300,
secret_env_key: "HMAC_TEST_SECRET"
}
}
end
Expand Down Expand Up @@ -464,7 +467,14 @@ def valid_with(args = {})
end

it "returns true when timestamp header name case differs due to normalization" do
# TODO
timestamp = Time.now.to_i.to_s
signing_payload = "v0:#{timestamp}:#{payload}"
signature = "v0=" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, signing_payload)

# Use uppercase timestamp header name in the request headers
headers = { header => signature, timestamp_header.upcase => timestamp }

expect(valid_with(headers:, config: base_config)).to be true
end
end

Expand Down