- config: env-based loading with startup validation, 13 tests - logger: structured slog wrapper with redaction, 10 tests - errors: unified AppError model with HTTP mapping, 18 tests - 41 tests total, build + vet clean
197 lines
5.5 KiB
Go
197 lines
5.5 KiB
Go
package logger
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestNewTextLogger(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
l := New(&buf, LevelDebug, "text")
|
|
|
|
l.Info("hello world", "key", "value")
|
|
|
|
output := buf.String()
|
|
if !strings.Contains(output, "hello world") {
|
|
t.Errorf("expected output to contain 'hello world', got: %s", output)
|
|
}
|
|
if !strings.Contains(output, "key") {
|
|
t.Errorf("expected output to contain 'key', got: %s", output)
|
|
}
|
|
if !strings.Contains(output, "value") {
|
|
t.Errorf("expected output to contain 'value', got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestNewJSONLogger(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
l := New(&buf, LevelDebug, "json")
|
|
|
|
l.Info("hello world", "key", "value")
|
|
|
|
output := buf.String()
|
|
if !strings.Contains(output, `"msg":"hello world"`) {
|
|
t.Errorf("expected JSON output to contain msg, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestLogLevels(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
l := New(&buf, LevelWarn, "text")
|
|
|
|
l.Debug("debug msg")
|
|
l.Info("info msg")
|
|
l.Warn("warn msg")
|
|
l.Error("error msg")
|
|
|
|
output := buf.String()
|
|
if strings.Contains(output, "debug") {
|
|
t.Error("debug message should be filtered out at WARN level")
|
|
}
|
|
if strings.Contains(output, "info") {
|
|
t.Error("info message should be filtered out at WARN level")
|
|
}
|
|
if !strings.Contains(output, "warn") {
|
|
t.Error("warn message should appear")
|
|
}
|
|
if !strings.Contains(output, "error") {
|
|
t.Error("error message should appear")
|
|
}
|
|
}
|
|
|
|
func TestWithFields(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
l := New(&buf, LevelDebug, "text")
|
|
|
|
child := l.With("request_id", "abc123", "user_id", "user-1")
|
|
child.Info("request processed")
|
|
|
|
output := buf.String()
|
|
if !strings.Contains(output, "request_id") || !strings.Contains(output, "abc123") {
|
|
t.Errorf("expected 'request_id=abc123' in output, got: %s", output)
|
|
}
|
|
if !strings.Contains(output, "user_id") || !strings.Contains(output, "user-1") {
|
|
t.Errorf("expected 'user_id=user-1' in output, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestNewNop(t *testing.T) {
|
|
l := NewNop()
|
|
var buf bytes.Buffer
|
|
// Nop logger should not write anything
|
|
l2 := New(&buf, LevelDebug, "text")
|
|
l2.Info("should appear")
|
|
if !strings.Contains(buf.String(), "should appear") {
|
|
t.Error("normal logger should write")
|
|
}
|
|
// Nop logger itself is fine to call
|
|
l.Info("should not appear")
|
|
l.Error("should not appear")
|
|
}
|
|
|
|
func TestParseLevel(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected Level
|
|
hasError bool
|
|
}{
|
|
{"debug", LevelDebug, false},
|
|
{"DEBUG", LevelDebug, false},
|
|
{"info", LevelInfo, false},
|
|
{"warn", LevelWarn, false},
|
|
{"error", LevelError, false},
|
|
{"ERROR", LevelError, false},
|
|
{"invalid", LevelInfo, true},
|
|
{"", LevelInfo, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
level, err := ParseLevel(tt.input)
|
|
if tt.hasError && err == nil {
|
|
t.Errorf("ParseLevel(%q) expected error but got none", tt.input)
|
|
}
|
|
if !tt.hasError && err != nil {
|
|
t.Errorf("ParseLevel(%q) unexpected error: %v", tt.input, err)
|
|
}
|
|
if level != tt.expected {
|
|
t.Errorf("ParseLevel(%q) = %v, want %v", tt.input, level, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRedaction(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
l := New(&buf, LevelDebug, "text")
|
|
|
|
l.Info("auth attempt", "api_key", "sk-abcdefgh12345678", "user_id", "user-1")
|
|
|
|
output := buf.String()
|
|
if strings.Contains(output, "sk-abcdefgh12345678") {
|
|
t.Errorf("API key should be redacted, got: %s", output)
|
|
}
|
|
if !strings.Contains(output, "sk-") && !strings.Contains(output, "****") {
|
|
t.Errorf("expected redacted value with mask, got: %s", output)
|
|
}
|
|
if !strings.Contains(output, "user-1") {
|
|
t.Errorf("non-sensitive field should NOT be redacted, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestRedactSensitiveKeys(t *testing.T) {
|
|
sensitiveTests := []string{"api_key", "API_KEY", "password", "secret", "token", "jwt_secret", "admin_password", "llm_api_key", "dashscope_api_key", "access_token"}
|
|
nonSensitiveTests := []string{"user_id", "request_id", "model", "port", "host", "query", "duration_ms", "count"}
|
|
|
|
for _, key := range sensitiveTests {
|
|
val := redactValue(key, "super-secret-value-12345")
|
|
if s, ok := val.(string); ok {
|
|
if s == "super-secret-value-12345" {
|
|
t.Errorf("key %q should be redacted, got raw value instead", key)
|
|
}
|
|
if !strings.Contains(s, "****") {
|
|
t.Errorf("key %q redacted value should contain '****', got: %s", key, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, key := range nonSensitiveTests {
|
|
val := redactValue(key, "normal-value")
|
|
if s, ok := val.(string); ok {
|
|
if s != "normal-value" {
|
|
t.Errorf("key %q should NOT be redacted, got: %s", key, s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRedactShortValue(t *testing.T) {
|
|
// Short values (< 4 chars) should be fully masked
|
|
val := redactValue("api_key", "ab")
|
|
if s, ok := val.(string); ok {
|
|
if s != "****" {
|
|
t.Errorf("short value should be fully masked, got: %s", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOddNumberOfArgs(t *testing.T) {
|
|
// Go's slog vet check prevents odd numbers of arguments at compile time.
|
|
// Our wrapper detects this and appends a !MISSING placeholder internally.
|
|
// This test verifies buildAttrs handles odd args correctly.
|
|
var buf bytes.Buffer
|
|
l := New(&buf, LevelDebug, "text")
|
|
attrs := l.buildAttrs("key1", "value1", "key2")
|
|
if len(attrs) != 2 {
|
|
t.Errorf("expected 2 attrs, got %d", len(attrs))
|
|
}
|
|
if attrs[1].Key != "key2" {
|
|
t.Errorf("expected attrs[1].Key=key2, got %s", attrs[1].Key)
|
|
}
|
|
// The value for key2 should be "!MISSING"
|
|
if attrs[1].Value.String() != "!MISSING" {
|
|
t.Errorf("expected !MISSING placeholder, got %s", attrs[1].Value.String())
|
|
}
|
|
}
|