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