package logging import ( "bytes" "encoding/json" "strings" "testing" ) func TestIsSensitiveField(t *testing.T) { tests := []struct { fieldName string want bool }{ // Exact matches (should be sensitive) {"password", true}, {"PASSWORD", true}, {"Password", true}, {"secret", true}, {"token", true}, {"api_key", true}, {"apikey", true}, {"api-key", true}, {"bearer", true}, {"authorization", true}, {"auth_token", true}, {"access_token", true}, {"refresh_token", true}, {"private_key", true}, {"secret_key", true}, {"client_secret", true}, {"cookie", true}, {"session", true}, {"x-api-key", true}, {"credentials", true}, {"credit_card", true}, {"cvv", true}, {"ssn", true}, {"bank_account", true}, // Pattern matches (contain sensitive keywords) {"user_password", true}, {"db_password", true}, {"auth_header", true}, {"secret_value", true}, {"api_key_id", true}, {"bearer_token", true}, // Non-sensitive fields {"name", false}, {"email", false}, {"user_id", false}, {"project_id", false}, {"status", false}, {"created_at", false}, {"component", false}, {"handler", false}, {"duration_ms", false}, {"request_id", false}, } for _, tt := range tests { t.Run(tt.fieldName, func(t *testing.T) { got := IsSensitiveField(tt.fieldName) if got != tt.want { t.Errorf("IsSensitiveField(%q) = %v, want %v", tt.fieldName, got, tt.want) } }) } } func TestRedactValue(t *testing.T) { tests := []struct { name string value string want string }{ // Bearer tokens { name: "bearer token", value: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", want: RedactedValue, }, // AWS access keys { name: "aws access key", value: "AKIAIOSFODNN7EXAMPLE", want: RedactedValue, }, // JWT tokens { name: "jwt token", value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U", want: RedactedValue, }, // Connection strings with passwords { name: "postgres connection string", value: "postgres://user:secretpassword123@localhost:5432/mydb", want: RedactedValue, }, { name: "redis connection string", value: "redis://admin:p@ssw0rd@redis.example.com:6379", want: RedactedValue, }, // Private key header { name: "private key", value: "-----BEGIN RSA PRIVATE KEY-----", want: RedactedValue, }, // API key patterns { name: "api key in string", value: "api_key=abc123xyz456789012345678901234567890", want: RedactedValue, }, // Non-sensitive values should pass through { name: "normal string", value: "hello world", want: "hello world", }, { name: "email", value: "user@example.com", want: "user@example.com", }, { name: "uuid", value: "550e8400-e29b-41d4-a716-446655440000", want: "550e8400-e29b-41d4-a716-446655440000", }, { name: "path", value: "/api/v1/projects/123", want: "/api/v1/projects/123", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := RedactValue(tt.value) if got != tt.want { t.Errorf("RedactValue(%q) = %q, want %q", tt.value, got, tt.want) } }) } } func TestContainsSensitiveData(t *testing.T) { tests := []struct { name string value string want bool }{ {"bearer token", "Authorization: Bearer xyz123", true}, {"jwt", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIn0.abc", true}, {"normal text", "This is a normal log message", false}, {"empty", "", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := ContainsSensitiveData(tt.value) if got != tt.want { t.Errorf("ContainsSensitiveData(%q) = %v, want %v", tt.value, got, tt.want) } }) } } func TestRedactingHandlerFieldNames(t *testing.T) { var buf bytes.Buffer cfg := DefaultConfig() cfg.RedactEnabled = true logger := NewWithWriter(cfg, &buf) // Log with sensitive field names logger.Info("test", "password", "secret123", "api_key", "key456", "name", "visible", ) output := buf.String() // Sensitive fields should be redacted if strings.Contains(output, "secret123") { t.Errorf("password value should be redacted, got: %s", output) } if strings.Contains(output, "key456") { t.Errorf("api_key value should be redacted, got: %s", output) } // Non-sensitive fields should be visible if !strings.Contains(output, "visible") { t.Errorf("non-sensitive field should be visible, got: %s", output) } // Redacted marker should appear if !strings.Contains(output, RedactedValue) { t.Errorf("expected redacted marker in output, got: %s", output) } } func TestRedactingHandlerValues(t *testing.T) { var buf bytes.Buffer cfg := DefaultConfig() cfg.RedactEnabled = true logger := NewWithWriter(cfg, &buf) // Log with sensitive values logger.Info("test", "auth_header", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIn0.abc", ) output := buf.String() // JWT should be redacted if strings.Contains(output, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9") { t.Errorf("JWT token should be redacted, got: %s", output) } } func TestRedactingHandlerPreservesStructure(t *testing.T) { var buf bytes.Buffer cfg := DefaultConfig() cfg.Format = FormatJSON cfg.RedactEnabled = true logger := NewWithWriter(cfg, &buf) logger.Info("test message", "user_id", "123", "password", "secret", "status", "success", ) // Parse JSON to verify structure is preserved var entry map[string]any if err := json.Unmarshal(buf.Bytes(), &entry); err != nil { t.Fatalf("expected valid JSON, got error: %v, output: %s", err, buf.String()) } // Check that fields exist if entry["user_id"] != "123" { t.Errorf("expected user_id=123, got: %v", entry["user_id"]) } if entry["status"] != "success" { t.Errorf("expected status=success, got: %v", entry["status"]) } // Password should be redacted if entry["password"] != RedactedValue { t.Errorf("expected password to be redacted, got: %v", entry["password"]) } } func TestRedactingHandlerDisabled(t *testing.T) { var buf bytes.Buffer cfg := DefaultConfig() cfg.RedactEnabled = false logger := NewWithWriter(cfg, &buf) // With redaction disabled, sensitive values should pass through logger.Info("test", "password", "visible_password") output := buf.String() if !strings.Contains(output, "visible_password") { t.Errorf("with redaction disabled, password should be visible, got: %s", output) } } // TestSecurityCriticalPatterns tests patterns that MUST be redacted func TestSecurityCriticalPatterns(t *testing.T) { criticalPatterns := []struct { name string value string }{ {"AWS access key", "AKIAIOSFODNN7EXAMPLE"}, {"Private key header", "-----BEGIN RSA PRIVATE KEY-----"}, {"Bearer token", "Bearer sk-1234567890abcdef"}, {"Postgres connection", "postgres://admin:secret@db:5432/app"}, {"MongoDB connection", "mongodb://user:pass@mongo:27017/db"}, {"Redis connection", "redis://:password@redis:6379"}, } for _, tt := range criticalPatterns { t.Run(tt.name, func(t *testing.T) { var buf bytes.Buffer cfg := DefaultConfig() cfg.RedactEnabled = true logger := NewWithWriter(cfg, &buf) logger.Info("test", "data", tt.value) output := buf.String() if strings.Contains(output, tt.value) && !strings.Contains(tt.value, RedactedValue) { t.Errorf("SECURITY: %s should be redacted, but found in output: %s", tt.name, output) } }) } }