Files
Cyrene_v2/backend/internal/logger/logger_test.go
T
Aska 7943e290ec feat(internal): add foundational packages — config, logger, errors
- 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
2026-08-09 20:35:42 +08:00

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())
}
}