Major changes: - Add internal/logging package with field constants, context propagation, sensitive data auto-redaction, and per-component log levels - Add worker timeout constants (TimeoutQuickOp, TimeoutHealthCheck, etc.) - Extend SDLC with callback handlers, generate endpoints, and executor - Add new cookbook trees for aeries and slackpath progression - Add skeleton templates for queue, realtime, and microservices - Add worker component template with async job processing - Refactor services and handlers to use new logging infrastructure - Split component.go into component_infra.go and component_listing.go Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
302 lines
7.5 KiB
Go
302 lines
7.5 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|