fix: correct docstring reference, wrap init errors, add control char escape tests
- commit
- 3865a9d1710ea443e8be9f76261b461ae5e519e3
- author
- brice · · 2026-04-03 17:52 UTC
- parent
- 6b418bd
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +22 -3
M internal/adapters/in/http/middleware/accesslog.go +1 -1
| @@ -15,7 +15,7 @@ import ( | |||
| 15 | 15 | // to the provided writer. It runs alongside (not instead of) RequestLogger. | |
| 16 | 16 | // | |
| 17 | 17 | // When excludeHealthChecks is true, requests from the Gordon health prober | |
| 18 | - | // (UA prefix domain.HealthCheckUserAgentPrefix) or from loopback IPs are not logged. | |
| 18 | + | // (UA prefix out.HealthCheckUserAgentPrefix) or from loopback IPs are not logged. | |
| 19 | 19 | // | |
| 20 | 20 | // Write failures are reported as warnings through the application logger and | |
| 21 | 21 | // never fail the HTTP response. | |
| @@ -15,7 +15,7 @@ import ( | |||||
| 15 | // to the provided writer. It runs alongside (not instead of) RequestLogger. | 15 | // to the provided writer. It runs alongside (not instead of) RequestLogger. | ||
| 16 | // | 16 | // | ||
| 17 | // When excludeHealthChecks is true, requests from the Gordon health prober | 17 | // When excludeHealthChecks is true, requests from the Gordon health prober | ||
| 18 | - | // (UA prefix domain.HealthCheckUserAgentPrefix) or from loopback IPs are not logged. | 18 | + | // (UA prefix out.HealthCheckUserAgentPrefix) or from loopback IPs are not logged. |
| 19 | // | 19 | // | ||
| 20 | // Write failures are reported as warnings through the application logger and | 20 | // Write failures are reported as warnings through the application logger and | ||
| 21 | // never fail the HTTP response. | 21 | // never fail the HTTP response. | ||
M internal/adapters/out/accesslog/formatter_test.go +19 -0
| @@ -193,3 +193,22 @@ func TestEscapeQuoted(t *testing.T) { | |||
| 193 | 193 | assert.Equal(t, `back\\slash`, escapeQuoted(`back\slash`)) | |
| 194 | 194 | assert.Equal(t, `\"\\\"`, escapeQuoted(`"\"`)) | |
| 195 | 195 | } | |
| 196 | + | ||
| 197 | + | func TestEscapeQuoted_ControlCharacters(t *testing.T) { | |
| 198 | + | // Named control characters with dedicated escapes | |
| 199 | + | assert.Equal(t, `line\nbreak`, escapeQuoted("line\nbreak")) | |
| 200 | + | assert.Equal(t, `cr\rreturn`, escapeQuoted("cr\rreturn")) | |
| 201 | + | assert.Equal(t, `a\ttab`, escapeQuoted("a\ttab")) | |
| 202 | + | ||
| 203 | + | // NUL, BEL, ESC — hex-escaped | |
| 204 | + | assert.Equal(t, `\x00`, escapeQuoted("\x00")) | |
| 205 | + | assert.Equal(t, `\x07`, escapeQuoted("\x07")) | |
| 206 | + | assert.Equal(t, `\x1b`, escapeQuoted("\x1b")) | |
| 207 | + | ||
| 208 | + | // All control characters 0x00-0x1F should be escaped (none pass through raw) | |
| 209 | + | for b := byte(0); b < 0x20; b++ { | |
| 210 | + | result := escapeQuoted(string([]byte{b})) | |
| 211 | + | assert.NotEqual(t, string([]byte{b}), result, | |
| 212 | + | "control byte 0x%02x should be escaped", b) | |
| 213 | + | } | |
| 214 | + | } | |
| @@ -193,3 +193,22 @@ func TestEscapeQuoted(t *testing.T) { | |||||
| 193 | assert.Equal(t, `back\\slash`, escapeQuoted(`back\slash`)) | 193 | assert.Equal(t, `back\\slash`, escapeQuoted(`back\slash`)) | ||
| 194 | assert.Equal(t, `\"\\\"`, escapeQuoted(`"\"`)) | 194 | assert.Equal(t, `\"\\\"`, escapeQuoted(`"\"`)) | ||
| 195 | } | 195 | } | ||
| 196 | + | ||||
| 197 | + | func TestEscapeQuoted_ControlCharacters(t *testing.T) { | |||
| 198 | + | // Named control characters with dedicated escapes | |||
| 199 | + | assert.Equal(t, `line\nbreak`, escapeQuoted("line\nbreak")) | |||
| 200 | + | assert.Equal(t, `cr\rreturn`, escapeQuoted("cr\rreturn")) | |||
| 201 | + | assert.Equal(t, `a\ttab`, escapeQuoted("a\ttab")) | |||
| 202 | + | ||||
| 203 | + | // NUL, BEL, ESC — hex-escaped | |||
| 204 | + | assert.Equal(t, `\x00`, escapeQuoted("\x00")) | |||
| 205 | + | assert.Equal(t, `\x07`, escapeQuoted("\x07")) | |||
| 206 | + | assert.Equal(t, `\x1b`, escapeQuoted("\x1b")) | |||
| 207 | + | ||||
| 208 | + | // All control characters 0x00-0x1F should be escaped (none pass through raw) | |||
| 209 | + | for b := byte(0); b < 0x20; b++ { | |||
| 210 | + | result := escapeQuoted(string([]byte{b})) | |||
| 211 | + | assert.NotEqual(t, string([]byte{b}), result, | |||
| 212 | + | "control byte 0x%02x should be escaped", b) | |||
| 213 | + | } | |||
| 214 | + | } | |||
M internal/adapters/out/accesslog/writer.go +2 -2
| @@ -52,12 +52,12 @@ type Writer struct { | |||
| 52 | 52 | func New(cfg Config) (*Writer, error) { | |
| 53 | 53 | formatter, err := newFormatter(cfg.Format) | |
| 54 | 54 | if err != nil { | |
| 55 | - | return nil, err | |
| 55 | + | return nil, fmt.Errorf("failed to create formatter: %w", err) | |
| 56 | 56 | } | |
| 57 | 57 | ||
| 58 | 58 | s, err := newSink(cfg) | |
| 59 | 59 | if err != nil { | |
| 60 | - | return nil, err | |
| 60 | + | return nil, fmt.Errorf("failed to create sink: %w", err) | |
| 61 | 61 | } | |
| 62 | 62 | ||
| 63 | 63 | return &Writer{ | |
| @@ -52,12 +52,12 @@ type Writer struct { | |||||
| 52 | func New(cfg Config) (*Writer, error) { | 52 | func New(cfg Config) (*Writer, error) { | ||
| 53 | formatter, err := newFormatter(cfg.Format) | 53 | formatter, err := newFormatter(cfg.Format) | ||
| 54 | if err != nil { | 54 | if err != nil { | ||
| 55 | - | return nil, err | 55 | + | return nil, fmt.Errorf("failed to create formatter: %w", err) |
| 56 | } | 56 | } | ||
| 57 | 57 | ||||
| 58 | s, err := newSink(cfg) | 58 | s, err := newSink(cfg) | ||
| 59 | if err != nil { | 59 | if err != nil { | ||
| 60 | - | return nil, err | 60 | + | return nil, fmt.Errorf("failed to create sink: %w", err) |
| 61 | } | 61 | } | ||
| 62 | 62 | ||||
| 63 | return &Writer{ | 63 | return &Writer{ | ||