Skip to content

Commit a7e400e

Browse files
CopilotGrantBirki
andcommitted
Add comprehensive unit tests for Core::LoggerFactory and Core::SignalHandler
Co-authored-by: GrantBirki <[email protected]>
1 parent 239932a commit a7e400e

File tree

3 files changed

+497
-1
lines changed

3 files changed

+497
-1
lines changed
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../../../spec_helper"
4+
require "stringio"
5+
6+
describe Hooks::Core::LoggerFactory do
7+
describe ".create" do
8+
context "with default parameters" do
9+
it "creates a logger with INFO level and JSON formatter" do
10+
logger = described_class.create
11+
12+
expect(logger).to be_a(Logger)
13+
expect(logger.level).to eq(Logger::INFO)
14+
end
15+
16+
it "logs to STDOUT by default" do
17+
logger = described_class.create
18+
19+
# The internal instance variable should be set to STDOUT
20+
expect(logger.instance_variable_get(:@logdev).dev).to eq($stdout)
21+
end
22+
end
23+
24+
context "with custom log level" do
25+
it "creates logger with DEBUG level" do
26+
logger = described_class.create(log_level: "debug")
27+
28+
expect(logger.level).to eq(Logger::DEBUG)
29+
end
30+
31+
it "creates logger with WARN level" do
32+
logger = described_class.create(log_level: "warn")
33+
34+
expect(logger.level).to eq(Logger::WARN)
35+
end
36+
37+
it "creates logger with ERROR level" do
38+
logger = described_class.create(log_level: "error")
39+
40+
expect(logger.level).to eq(Logger::ERROR)
41+
end
42+
43+
it "creates logger with INFO level for invalid level" do
44+
logger = described_class.create(log_level: "invalid")
45+
46+
expect(logger.level).to eq(Logger::INFO)
47+
end
48+
49+
it "handles nil log level gracefully" do
50+
logger = described_class.create(log_level: nil)
51+
52+
expect(logger.level).to eq(Logger::INFO)
53+
end
54+
55+
it "handles case insensitive log levels" do
56+
logger = described_class.create(log_level: "DEBUG")
57+
58+
expect(logger.level).to eq(Logger::DEBUG)
59+
end
60+
end
61+
62+
context "with custom logger" do
63+
it "returns the custom logger instance" do
64+
custom_logger = Logger.new(StringIO.new)
65+
custom_logger.level = Logger::WARN
66+
67+
result = described_class.create(custom_logger: custom_logger)
68+
69+
expect(result).to be(custom_logger)
70+
expect(result.level).to eq(Logger::WARN)
71+
end
72+
73+
it "ignores log_level parameter when custom_logger is provided" do
74+
custom_logger = Logger.new(StringIO.new)
75+
custom_logger.level = Logger::ERROR
76+
77+
result = described_class.create(log_level: "debug", custom_logger: custom_logger)
78+
79+
expect(result).to be(custom_logger)
80+
expect(result.level).to eq(Logger::ERROR) # Should remain unchanged
81+
end
82+
end
83+
84+
context "JSON formatting" do
85+
let(:output) { StringIO.new }
86+
let(:logger) do
87+
logger = described_class.create(log_level: "debug")
88+
logger.instance_variable_set(:@logdev, Logger::LogDevice.new(output))
89+
logger
90+
end
91+
92+
it "formats log messages as JSON" do
93+
logger.info("Test message")
94+
95+
output.rewind
96+
log_line = output.read
97+
parsed = JSON.parse(log_line)
98+
99+
expect(parsed).to include(
100+
"level" => "info",
101+
"message" => "Test message"
102+
)
103+
expect(parsed["timestamp"]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z/)
104+
end
105+
106+
it "includes request context when available" do
107+
Thread.current[:hooks_request_context] = {
108+
"request_id" => "test-123",
109+
"endpoint" => "/webhook/test"
110+
}
111+
112+
logger.warn("Context test")
113+
114+
output.rewind
115+
log_line = output.read
116+
parsed = JSON.parse(log_line)
117+
118+
expect(parsed).to include(
119+
"level" => "warn",
120+
"message" => "Context test",
121+
"request_id" => "test-123",
122+
"endpoint" => "/webhook/test"
123+
)
124+
ensure
125+
Thread.current[:hooks_request_context] = nil
126+
end
127+
128+
it "works without request context" do
129+
Thread.current[:hooks_request_context] = nil
130+
131+
logger.error("No context test")
132+
133+
output.rewind
134+
log_line = output.read
135+
parsed = JSON.parse(log_line)
136+
137+
expect(parsed).to include(
138+
"level" => "error",
139+
"message" => "No context test"
140+
)
141+
expect(parsed).not_to have_key("request_id")
142+
end
143+
144+
it "handles different severity levels correctly" do
145+
["debug", "info", "warn", "error"].each do |level|
146+
output.truncate(0)
147+
output.rewind
148+
149+
logger.send(level, "#{level} message")
150+
151+
output.rewind
152+
log_line = output.read
153+
parsed = JSON.parse(log_line)
154+
155+
expect(parsed["level"]).to eq(level)
156+
expect(parsed["message"]).to eq("#{level} message")
157+
end
158+
end
159+
end
160+
end
161+
162+
describe ".parse_log_level" do
163+
it "converts string log levels to Logger constants" do
164+
expect(described_class.send(:parse_log_level, "debug")).to eq(Logger::DEBUG)
165+
expect(described_class.send(:parse_log_level, "info")).to eq(Logger::INFO)
166+
expect(described_class.send(:parse_log_level, "warn")).to eq(Logger::WARN)
167+
expect(described_class.send(:parse_log_level, "error")).to eq(Logger::ERROR)
168+
end
169+
170+
it "handles case insensitive input" do
171+
expect(described_class.send(:parse_log_level, "DEBUG")).to eq(Logger::DEBUG)
172+
expect(described_class.send(:parse_log_level, "Info")).to eq(Logger::INFO)
173+
expect(described_class.send(:parse_log_level, "WARN")).to eq(Logger::WARN)
174+
expect(described_class.send(:parse_log_level, "Error")).to eq(Logger::ERROR)
175+
end
176+
177+
it "defaults to INFO for invalid levels" do
178+
expect(described_class.send(:parse_log_level, "invalid")).to eq(Logger::INFO)
179+
expect(described_class.send(:parse_log_level, "")).to eq(Logger::INFO)
180+
expect(described_class.send(:parse_log_level, nil)).to eq(Logger::INFO)
181+
end
182+
end
183+
184+
describe ".json_formatter" do
185+
let(:formatter) { described_class.send(:json_formatter) }
186+
let(:test_time) { Time.parse("2023-01-01T12:00:00Z") }
187+
188+
it "returns a proc" do
189+
expect(formatter).to be_a(Proc)
190+
end
191+
192+
it "formats log entry as JSON with newline" do
193+
result = formatter.call("INFO", test_time, nil, "Test message")
194+
parsed = JSON.parse(result.chomp)
195+
196+
expect(parsed).to eq({
197+
"timestamp" => "2023-01-01T12:00:00Z",
198+
"level" => "info",
199+
"message" => "Test message"
200+
})
201+
expect(result).to end_with("\n")
202+
end
203+
204+
it "includes thread context when available" do
205+
Thread.current[:hooks_request_context] = { "user_id" => 123 }
206+
207+
result = formatter.call("WARN", test_time, nil, "Warning message")
208+
parsed = JSON.parse(result.chomp)
209+
210+
expect(parsed).to eq({
211+
"timestamp" => "2023-01-01T12:00:00Z",
212+
"level" => "warn",
213+
"message" => "Warning message",
214+
"user_id" => 123
215+
})
216+
ensure
217+
Thread.current[:hooks_request_context] = nil
218+
end
219+
220+
it "handles complex message objects" do
221+
complex_message = { error: "Something failed", details: { code: 500 } }
222+
223+
result = formatter.call("ERROR", test_time, nil, complex_message)
224+
parsed = JSON.parse(result.chomp)
225+
226+
# JSON parsing converts symbol keys to strings
227+
expect(parsed["message"]).to eq({
228+
"error" => "Something failed",
229+
"details" => { "code" => 500 }
230+
})
231+
end
232+
end
233+
end
234+
235+
describe Hooks::Core::LogContext do
236+
after do
237+
Thread.current[:hooks_request_context] = nil
238+
end
239+
240+
describe ".set" do
241+
it "sets request context in thread local storage" do
242+
context = { "request_id" => "test-123", "user" => "testuser" }
243+
244+
described_class.set(context)
245+
246+
expect(Thread.current[:hooks_request_context]).to eq(context)
247+
end
248+
249+
it "overwrites existing context" do
250+
Thread.current[:hooks_request_context] = { "old" => "data" }
251+
252+
new_context = { "new" => "data" }
253+
described_class.set(new_context)
254+
255+
expect(Thread.current[:hooks_request_context]).to eq(new_context)
256+
end
257+
end
258+
259+
describe ".clear" do
260+
it "clears request context" do
261+
Thread.current[:hooks_request_context] = { "test" => "data" }
262+
263+
described_class.clear
264+
265+
expect(Thread.current[:hooks_request_context]).to be_nil
266+
end
267+
268+
it "works when context is already nil" do
269+
Thread.current[:hooks_request_context] = nil
270+
271+
expect { described_class.clear }.not_to raise_error
272+
expect(Thread.current[:hooks_request_context]).to be_nil
273+
end
274+
end
275+
276+
describe ".with" do
277+
it "sets context for block execution then restores original" do
278+
original_context = { "original" => "value" }
279+
Thread.current[:hooks_request_context] = original_context
280+
281+
block_context = { "block" => "value" }
282+
context_during_block = nil
283+
284+
described_class.with(block_context) do
285+
context_during_block = Thread.current[:hooks_request_context]
286+
end
287+
288+
expect(context_during_block).to eq(block_context)
289+
expect(Thread.current[:hooks_request_context]).to eq(original_context)
290+
end
291+
292+
it "restores context even if block raises exception" do
293+
original_context = { "original" => "value" }
294+
Thread.current[:hooks_request_context] = original_context
295+
296+
block_context = { "block" => "value" }
297+
298+
expect {
299+
described_class.with(block_context) do
300+
raise StandardError, "Test error"
301+
end
302+
}.to raise_error(StandardError, "Test error")
303+
304+
expect(Thread.current[:hooks_request_context]).to eq(original_context)
305+
end
306+
307+
it "works when original context is nil" do
308+
Thread.current[:hooks_request_context] = nil
309+
310+
block_context = { "block" => "value" }
311+
context_during_block = nil
312+
313+
described_class.with(block_context) do
314+
context_during_block = Thread.current[:hooks_request_context]
315+
end
316+
317+
expect(context_during_block).to eq(block_context)
318+
expect(Thread.current[:hooks_request_context]).to be_nil
319+
end
320+
321+
it "yields to the block" do
322+
yielded_value = nil
323+
324+
described_class.with({}) do |arg|
325+
yielded_value = arg
326+
end
327+
328+
expect(yielded_value).to be_nil # with doesn't pass arguments
329+
end
330+
end
331+
end

0 commit comments

Comments
 (0)