Skip to content

Commit 7b681e8

Browse files
CopilotGrantBirki
andcommitted
Add comprehensive tests for security, log, handlers and endpoints
Co-authored-by: GrantBirki <[email protected]>
1 parent e764b86 commit 7b681e8

File tree

5 files changed

+311
-0
lines changed

5 files changed

+311
-0
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
require "rack/test"
4+
5+
describe Hooks::App::HealthEndpoint do
6+
include Rack::Test::Methods
7+
8+
def app
9+
described_class
10+
end
11+
12+
before do
13+
# Mock API start_time for consistent uptime calculation
14+
allow(Hooks::App::API).to receive(:start_time).and_return(Time.parse("2024-12-31T23:59:00Z"))
15+
end
16+
17+
describe "GET /" do
18+
it "returns health status as JSON" do
19+
get "/"
20+
21+
expect(last_response.status).to eq(200)
22+
expect(last_response.headers["Content-Type"]).to include("application/json")
23+
end
24+
25+
it "includes health status in response" do
26+
get "/"
27+
28+
response_data = JSON.parse(last_response.body)
29+
expect(response_data["status"]).to eq("healthy")
30+
end
31+
32+
it "includes timestamp in ISO8601 format" do
33+
get "/"
34+
35+
response_data = JSON.parse(last_response.body)
36+
expect(response_data["timestamp"]).to eq(TIME_MOCK)
37+
end
38+
39+
it "includes version information" do
40+
get "/"
41+
42+
response_data = JSON.parse(last_response.body)
43+
expect(response_data["version"]).to eq(Hooks::VERSION)
44+
end
45+
46+
it "includes uptime in seconds" do
47+
get "/"
48+
49+
response_data = JSON.parse(last_response.body)
50+
expect(response_data["uptime_seconds"]).to be_a(Integer)
51+
expect(response_data["uptime_seconds"]).to eq(60) # 1 minute difference
52+
end
53+
54+
it "returns valid JSON structure" do
55+
get "/"
56+
57+
expect { JSON.parse(last_response.body) }.not_to raise_error
58+
59+
response_data = JSON.parse(last_response.body)
60+
expect(response_data).to have_key("status")
61+
expect(response_data).to have_key("timestamp")
62+
expect(response_data).to have_key("version")
63+
expect(response_data).to have_key("uptime_seconds")
64+
end
65+
66+
it "calculates uptime correctly" do
67+
# Test with different start time
68+
different_start = Time.parse("2024-12-31T23:58:30Z")
69+
allow(Hooks::App::API).to receive(:start_time).and_return(different_start)
70+
71+
get "/"
72+
73+
response_data = JSON.parse(last_response.body)
74+
expect(response_data["uptime_seconds"]).to eq(90) # 1.5 minutes difference
75+
end
76+
end
77+
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
require "rack/test"
4+
5+
describe Hooks::App::VersionEndpoint do
6+
include Rack::Test::Methods
7+
8+
def app
9+
described_class
10+
end
11+
12+
describe "GET /" do
13+
it "returns version information as JSON" do
14+
get "/"
15+
16+
expect(last_response.status).to eq(200)
17+
expect(last_response.headers["Content-Type"]).to include("application/json")
18+
end
19+
20+
it "includes version number in response" do
21+
get "/"
22+
23+
response_data = JSON.parse(last_response.body)
24+
expect(response_data["version"]).to eq(Hooks::VERSION)
25+
end
26+
27+
it "includes timestamp in ISO8601 format" do
28+
get "/"
29+
30+
response_data = JSON.parse(last_response.body)
31+
expect(response_data["timestamp"]).to eq(TIME_MOCK)
32+
end
33+
34+
it "returns valid JSON structure" do
35+
get "/"
36+
37+
expect { JSON.parse(last_response.body) }.not_to raise_error
38+
39+
response_data = JSON.parse(last_response.body)
40+
expect(response_data).to have_key("version")
41+
expect(response_data).to have_key("timestamp")
42+
end
43+
44+
it "version matches expected format" do
45+
get "/"
46+
47+
response_data = JSON.parse(last_response.body)
48+
expect(response_data["version"]).to match(/^\d+\.\d+\.\d+$/)
49+
end
50+
end
51+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
describe Hooks::Log do
4+
describe ".instance" do
5+
it "can be set and retrieved" do
6+
logger = instance_double(Logger)
7+
described_class.instance = logger
8+
9+
expect(described_class.instance).to eq(logger)
10+
end
11+
12+
it "can be set to nil" do
13+
described_class.instance = nil
14+
15+
expect(described_class.instance).to be_nil
16+
end
17+
18+
it "maintains the same instance when set" do
19+
logger = instance_double(Logger)
20+
described_class.instance = logger
21+
22+
expect(described_class.instance).to be(logger)
23+
end
24+
25+
it "can be overridden" do
26+
first_logger = instance_double(Logger)
27+
second_logger = instance_double(Logger)
28+
29+
described_class.instance = first_logger
30+
expect(described_class.instance).to eq(first_logger)
31+
32+
described_class.instance = second_logger
33+
expect(described_class.instance).to eq(second_logger)
34+
end
35+
end
36+
end
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# frozen_string_literal: true
2+
3+
describe DefaultHandler do
4+
let(:log) { instance_double(Logger).as_null_object }
5+
let(:payload) { { "action" => "opened", "number" => 1 } }
6+
let(:headers) { { "X-GitHub-Event" => "pull_request" } }
7+
let(:config) { { environment: "test" } }
8+
let(:handler) { described_class.new }
9+
10+
before do
11+
allow(handler).to receive(:log).and_return(log)
12+
end
13+
14+
describe "#call" do
15+
context "with valid parameters" do
16+
let(:result) { handler.call(payload:, headers:, config:) }
17+
18+
it "logs that the default handler was invoked" do
19+
result
20+
21+
expect(log).to have_received(:info).with("🔔 Default handler invoked for webhook 🔔")
22+
end
23+
24+
it "logs payload debug information when payload is present" do
25+
result
26+
27+
expect(log).to have_received(:debug).with("received payload: #{payload.inspect}")
28+
end
29+
30+
it "returns a success response hash" do
31+
expect(result).to be_a(Hash)
32+
expect(result).to include(
33+
message: "webhook processed successfully",
34+
handler: "DefaultHandler",
35+
timestamp: TIME_MOCK
36+
)
37+
end
38+
39+
it "includes timestamp in ISO8601 format" do
40+
expect(result[:timestamp]).to eq(TIME_MOCK)
41+
end
42+
end
43+
44+
context "with nil payload" do
45+
let(:payload) { nil }
46+
let(:result) { handler.call(payload:, headers:, config:) }
47+
48+
it "does not log payload debug information" do
49+
result
50+
51+
expect(log).not_to have_received(:debug)
52+
end
53+
54+
it "still returns a success response" do
55+
expect(result).to include(
56+
message: "webhook processed successfully",
57+
handler: "DefaultHandler"
58+
)
59+
end
60+
end
61+
62+
context "with empty payload" do
63+
let(:payload) { {} }
64+
let(:result) { handler.call(payload:, headers:, config:) }
65+
66+
it "logs the empty payload" do
67+
result
68+
69+
expect(log).to have_received(:debug).with("received payload: #{payload.inspect}")
70+
end
71+
end
72+
73+
context "with complex payload" do
74+
let(:payload) do
75+
{
76+
"action" => "opened",
77+
"pull_request" => {
78+
"id" => 123,
79+
"title" => "Test PR",
80+
"body" => "This is a test"
81+
}
82+
}
83+
end
84+
let(:result) { handler.call(payload:, headers:, config:) }
85+
86+
it "logs the complex payload structure" do
87+
result
88+
89+
expect(log).to have_received(:debug).with("received payload: #{payload.inspect}")
90+
end
91+
end
92+
end
93+
94+
describe "inheritance" do
95+
it "inherits from Hooks::Handlers::Base" do
96+
expect(described_class.superclass).to eq(Hooks::Handlers::Base)
97+
end
98+
end
99+
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
describe Hooks::Security do
4+
describe "DANGEROUS_CLASSES" do
5+
it "is frozen to prevent modification" do
6+
expect(described_class::DANGEROUS_CLASSES).to be_frozen
7+
end
8+
9+
it "contains system-access classes" do
10+
expected_classes = %w[
11+
File Dir Kernel Object Class Module Proc Method
12+
IO Socket TCPSocket UDPSocket BasicSocket
13+
Process Thread Fiber Mutex ConditionVariable
14+
Marshal YAML JSON Pathname
15+
]
16+
17+
expect(described_class::DANGEROUS_CLASSES).to match_array(expected_classes)
18+
end
19+
20+
it "contains file system classes" do
21+
expect(described_class::DANGEROUS_CLASSES).to include("File", "Dir", "Pathname")
22+
end
23+
24+
it "contains network classes" do
25+
expect(described_class::DANGEROUS_CLASSES).to include("Socket", "TCPSocket", "UDPSocket", "BasicSocket")
26+
end
27+
28+
it "contains process control classes" do
29+
expect(described_class::DANGEROUS_CLASSES).to include("Process", "Thread", "Fiber")
30+
end
31+
32+
it "contains serialization classes" do
33+
expect(described_class::DANGEROUS_CLASSES).to include("Marshal", "YAML", "JSON")
34+
end
35+
36+
it "contains core Ruby classes that provide system access" do
37+
expect(described_class::DANGEROUS_CLASSES).to include("Kernel", "Object", "Class", "Module")
38+
end
39+
40+
it "prevents empty string attacks" do
41+
expect(described_class::DANGEROUS_CLASSES).not_to include("")
42+
end
43+
44+
it "prevents nil attacks" do
45+
expect(described_class::DANGEROUS_CLASSES).not_to include(nil)
46+
end
47+
end
48+
end

0 commit comments

Comments
 (0)