Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
120
cloud/maplepress-backend/pkg/logger/logger.go
Normal file
120
cloud/maplepress-backend/pkg/logger/logger.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// emojiCore wraps a zapcore.Core to add emoji icon field
|
||||
type emojiCore struct {
|
||||
zapcore.Core
|
||||
}
|
||||
|
||||
func (c *emojiCore) With(fields []zapcore.Field) zapcore.Core {
|
||||
return &emojiCore{c.Core.With(fields)}
|
||||
}
|
||||
|
||||
func (c *emojiCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||
if c.Enabled(entry.Level) {
|
||||
return ce.AddCore(entry, c)
|
||||
}
|
||||
return ce
|
||||
}
|
||||
|
||||
func (c *emojiCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
|
||||
// Only add emoji icon field for warnings and errors
|
||||
// Skip for info and debug to keep output clean
|
||||
var emoji string
|
||||
var addEmoji bool
|
||||
|
||||
switch entry.Level {
|
||||
case zapcore.WarnLevel:
|
||||
emoji = "🟡" // Yellow circle for warnings
|
||||
addEmoji = true
|
||||
case zapcore.ErrorLevel:
|
||||
emoji = "🔴" // Red circle for errors
|
||||
addEmoji = true
|
||||
case zapcore.DPanicLevel:
|
||||
emoji = "🔴" // Red circle for panic
|
||||
addEmoji = true
|
||||
case zapcore.PanicLevel:
|
||||
emoji = "🔴" // Red circle for panic
|
||||
addEmoji = true
|
||||
case zapcore.FatalLevel:
|
||||
emoji = "🔴" // Red circle for fatal
|
||||
addEmoji = true
|
||||
default:
|
||||
// No emoji for debug and info levels
|
||||
addEmoji = false
|
||||
}
|
||||
|
||||
// Only prepend emoji field if we're adding one
|
||||
if addEmoji {
|
||||
fieldsWithEmoji := make([]zapcore.Field, 0, len(fields)+1)
|
||||
fieldsWithEmoji = append(fieldsWithEmoji, zap.String("ico", emoji))
|
||||
fieldsWithEmoji = append(fieldsWithEmoji, fields...)
|
||||
return c.Core.Write(entry, fieldsWithEmoji)
|
||||
}
|
||||
|
||||
// For debug/info, write as-is without emoji
|
||||
return c.Core.Write(entry, fields)
|
||||
}
|
||||
|
||||
// ProvideLogger creates a new zap logger based on configuration
|
||||
func ProvideLogger(cfg *config.Config) (*zap.Logger, error) {
|
||||
var zapConfig zap.Config
|
||||
|
||||
// Set config based on environment
|
||||
if cfg.App.Environment == "production" {
|
||||
zapConfig = zap.NewProductionConfig()
|
||||
} else {
|
||||
zapConfig = zap.NewDevelopmentConfig()
|
||||
}
|
||||
|
||||
// Set log level
|
||||
level, err := zapcore.ParseLevel(cfg.Logger.Level)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid log level %s: %w", cfg.Logger.Level, err)
|
||||
}
|
||||
zapConfig.Level = zap.NewAtomicLevelAt(level)
|
||||
|
||||
// Set encoding format
|
||||
if cfg.Logger.Format == "console" {
|
||||
zapConfig.Encoding = "console"
|
||||
zapConfig.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||
} else {
|
||||
zapConfig.Encoding = "json"
|
||||
}
|
||||
|
||||
// Build logger with environment-specific options
|
||||
var loggerOptions []zap.Option
|
||||
|
||||
// Enable caller information in development for easier debugging
|
||||
if cfg.App.Environment != "production" {
|
||||
loggerOptions = append(loggerOptions, zap.AddCaller())
|
||||
loggerOptions = append(loggerOptions, zap.AddCallerSkip(0))
|
||||
}
|
||||
|
||||
// Add stack traces for error level and above
|
||||
loggerOptions = append(loggerOptions, zap.AddStacktrace(zapcore.ErrorLevel))
|
||||
|
||||
// Wrap core with emoji core to add icon field
|
||||
loggerOptions = append(loggerOptions, zap.WrapCore(func(core zapcore.Core) zapcore.Core {
|
||||
return &emojiCore{core}
|
||||
}))
|
||||
|
||||
logger, err := zapConfig.Build(loggerOptions...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build logger: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("✓ Logger initialized",
|
||||
zap.String("level", cfg.Logger.Level),
|
||||
zap.String("format", cfg.Logger.Format),
|
||||
zap.String("environment", cfg.App.Environment))
|
||||
|
||||
return logger, nil
|
||||
}
|
||||
231
cloud/maplepress-backend/pkg/logger/sanitizer.go
Normal file
231
cloud/maplepress-backend/pkg/logger/sanitizer.go
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// SensitiveFieldRedactor provides methods to redact sensitive data before logging
|
||||
// This addresses CWE-532 (Insertion of Sensitive Information into Log File)
|
||||
type SensitiveFieldRedactor struct {
|
||||
emailRegex *regexp.Regexp
|
||||
}
|
||||
|
||||
// NewSensitiveFieldRedactor creates a new redactor for sensitive data
|
||||
func NewSensitiveFieldRedactor() *SensitiveFieldRedactor {
|
||||
return &SensitiveFieldRedactor{
|
||||
emailRegex: regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`),
|
||||
}
|
||||
}
|
||||
|
||||
// RedactEmail redacts an email address for logging
|
||||
// Example: "john.doe@example.com" -> "jo***@example.com"
|
||||
func (r *SensitiveFieldRedactor) RedactEmail(email string) string {
|
||||
if email == "" {
|
||||
return "[empty]"
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
if !r.emailRegex.MatchString(email) {
|
||||
return "[invalid-email]"
|
||||
}
|
||||
|
||||
parts := strings.Split(email, "@")
|
||||
if len(parts) != 2 {
|
||||
return "[invalid-email]"
|
||||
}
|
||||
|
||||
localPart := parts[0]
|
||||
domain := parts[1]
|
||||
|
||||
// Show first 2 characters of local part, redact the rest
|
||||
if len(localPart) <= 2 {
|
||||
return "**@" + domain
|
||||
}
|
||||
|
||||
return localPart[:2] + "***@" + domain
|
||||
}
|
||||
|
||||
// HashForLogging creates a consistent hash for unique identification without exposing the original value
|
||||
// This allows correlation across log entries without storing PII
|
||||
// Example: "john.doe@example.com" -> "a1b2c3d4"
|
||||
func (r *SensitiveFieldRedactor) HashForLogging(value string) string {
|
||||
if value == "" {
|
||||
return "[empty]"
|
||||
}
|
||||
|
||||
h := sha256.Sum256([]byte(value))
|
||||
// Return first 8 bytes (16 hex characters) for reasonable uniqueness
|
||||
return hex.EncodeToString(h[:8])
|
||||
}
|
||||
|
||||
// RedactTenantSlug redacts a tenant slug for logging
|
||||
// Example: "my-company" -> "my-***"
|
||||
func (r *SensitiveFieldRedactor) RedactTenantSlug(slug string) string {
|
||||
if slug == "" {
|
||||
return "[empty]"
|
||||
}
|
||||
|
||||
if len(slug) <= 3 {
|
||||
return "***"
|
||||
}
|
||||
|
||||
return slug[:2] + "***"
|
||||
}
|
||||
|
||||
// RedactAPIKey redacts an API key for logging
|
||||
// Shows only prefix and last 4 characters
|
||||
// Example: "live_sk_abc123def456ghi789" -> "live_sk_***i789"
|
||||
func (r *SensitiveFieldRedactor) RedactAPIKey(apiKey string) string {
|
||||
if apiKey == "" {
|
||||
return "[empty]"
|
||||
}
|
||||
|
||||
// Show prefix (live_sk_ or test_sk_) and last 4 characters
|
||||
if strings.HasPrefix(apiKey, "live_sk_") || strings.HasPrefix(apiKey, "test_sk_") {
|
||||
prefix := apiKey[:8] // "live_sk_" or "test_sk_"
|
||||
if len(apiKey) > 12 {
|
||||
return prefix + "***" + apiKey[len(apiKey)-4:]
|
||||
}
|
||||
return prefix + "***"
|
||||
}
|
||||
|
||||
// For other formats, just show last 4 characters
|
||||
if len(apiKey) > 4 {
|
||||
return "***" + apiKey[len(apiKey)-4:]
|
||||
}
|
||||
|
||||
return "***"
|
||||
}
|
||||
|
||||
// RedactJWTToken redacts a JWT token for logging
|
||||
// Shows only first and last 8 characters
|
||||
func (r *SensitiveFieldRedactor) RedactJWTToken(token string) string {
|
||||
if token == "" {
|
||||
return "[empty]"
|
||||
}
|
||||
|
||||
if len(token) < 16 {
|
||||
return "***"
|
||||
}
|
||||
|
||||
return token[:8] + "..." + token[len(token)-8:]
|
||||
}
|
||||
|
||||
// RedactIPAddress partially redacts an IP address
|
||||
// IPv4: "192.168.1.100" -> "192.168.*.*"
|
||||
// IPv6: Redacts last 4 groups
|
||||
func (r *SensitiveFieldRedactor) RedactIPAddress(ip string) string {
|
||||
if ip == "" {
|
||||
return "[empty]"
|
||||
}
|
||||
|
||||
// IPv4
|
||||
if strings.Contains(ip, ".") {
|
||||
parts := strings.Split(ip, ".")
|
||||
if len(parts) == 4 {
|
||||
return parts[0] + "." + parts[1] + ".*.*"
|
||||
}
|
||||
}
|
||||
|
||||
// IPv6
|
||||
if strings.Contains(ip, ":") {
|
||||
parts := strings.Split(ip, ":")
|
||||
if len(parts) >= 4 {
|
||||
return strings.Join(parts[:4], ":") + ":****"
|
||||
}
|
||||
}
|
||||
|
||||
return "***"
|
||||
}
|
||||
|
||||
// Zap Field Helpers - Provide convenient zap.Field constructors
|
||||
|
||||
// SafeEmail creates a zap field with redacted email
|
||||
func SafeEmail(key string, email string) zapcore.Field {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
return zap.String(key, redactor.RedactEmail(email))
|
||||
}
|
||||
|
||||
// EmailHash creates a zap field with hashed email for correlation
|
||||
func EmailHash(email string) zapcore.Field {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
return zap.String("email_hash", redactor.HashForLogging(email))
|
||||
}
|
||||
|
||||
// HashString hashes a string value for safe logging
|
||||
// Returns the hash string directly (not a zap.Field)
|
||||
func HashString(value string) string {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
return redactor.HashForLogging(value)
|
||||
}
|
||||
|
||||
// SafeTenantSlug creates a zap field with redacted tenant slug
|
||||
func SafeTenantSlug(key string, slug string) zapcore.Field {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
return zap.String(key, redactor.RedactTenantSlug(slug))
|
||||
}
|
||||
|
||||
// TenantSlugHash creates a zap field with hashed tenant slug for correlation
|
||||
func TenantSlugHash(slug string) zapcore.Field {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
return zap.String("tenant_slug_hash", redactor.HashForLogging(slug))
|
||||
}
|
||||
|
||||
// SafeAPIKey creates a zap field with redacted API key
|
||||
func SafeAPIKey(key string, apiKey string) zapcore.Field {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
return zap.String(key, redactor.RedactAPIKey(apiKey))
|
||||
}
|
||||
|
||||
// SafeJWTToken creates a zap field with redacted JWT token
|
||||
func SafeJWTToken(key string, token string) zapcore.Field {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
return zap.String(key, redactor.RedactJWTToken(token))
|
||||
}
|
||||
|
||||
// SafeIPAddress creates a zap field with redacted IP address
|
||||
func SafeIPAddress(key string, ip string) zapcore.Field {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
return zap.String(key, redactor.RedactIPAddress(ip))
|
||||
}
|
||||
|
||||
// UserIdentifier creates safe identification fields for a user
|
||||
// Includes: user_id (safe), email_hash, email_redacted
|
||||
func UserIdentifier(userID string, email string) []zapcore.Field {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
return []zapcore.Field{
|
||||
zap.String("user_id", userID),
|
||||
zap.String("email_hash", redactor.HashForLogging(email)),
|
||||
zap.String("email_redacted", redactor.RedactEmail(email)),
|
||||
}
|
||||
}
|
||||
|
||||
// TenantIdentifier creates safe identification fields for a tenant
|
||||
// Includes: tenant_id (safe), slug_hash, slug_redacted
|
||||
func TenantIdentifier(tenantID string, slug string) []zapcore.Field {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
return []zapcore.Field{
|
||||
zap.String("tenant_id", tenantID),
|
||||
zap.String("tenant_slug_hash", redactor.HashForLogging(slug)),
|
||||
zap.String("tenant_slug_redacted", redactor.RedactTenantSlug(slug)),
|
||||
}
|
||||
}
|
||||
|
||||
// Constants for field names
|
||||
const (
|
||||
FieldUserID = "user_id"
|
||||
FieldEmailHash = "email_hash"
|
||||
FieldEmailRedacted = "email_redacted"
|
||||
FieldTenantID = "tenant_id"
|
||||
FieldTenantSlugHash = "tenant_slug_hash"
|
||||
FieldTenantSlugRedacted = "tenant_slug_redacted"
|
||||
FieldAPIKeyRedacted = "api_key_redacted"
|
||||
FieldJWTTokenRedacted = "jwt_token_redacted"
|
||||
FieldIPAddressRedacted = "ip_address_redacted"
|
||||
)
|
||||
345
cloud/maplepress-backend/pkg/logger/sanitizer_test.go
Normal file
345
cloud/maplepress-backend/pkg/logger/sanitizer_test.go
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRedactEmail(t *testing.T) {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "normal email",
|
||||
input: "john.doe@example.com",
|
||||
expected: "jo***@example.com",
|
||||
},
|
||||
{
|
||||
name: "short local part",
|
||||
input: "ab@example.com",
|
||||
expected: "**@example.com",
|
||||
},
|
||||
{
|
||||
name: "single character local part",
|
||||
input: "a@example.com",
|
||||
expected: "**@example.com",
|
||||
},
|
||||
{
|
||||
name: "empty email",
|
||||
input: "",
|
||||
expected: "[empty]",
|
||||
},
|
||||
{
|
||||
name: "invalid email",
|
||||
input: "notanemail",
|
||||
expected: "[invalid-email]",
|
||||
},
|
||||
{
|
||||
name: "long email",
|
||||
input: "very.long.email.address@subdomain.example.com",
|
||||
expected: "ve***@subdomain.example.com",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := redactor.RedactEmail(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("RedactEmail(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashForLogging(t *testing.T) {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{
|
||||
name: "email",
|
||||
input: "john.doe@example.com",
|
||||
},
|
||||
{
|
||||
name: "tenant slug",
|
||||
input: "my-company",
|
||||
},
|
||||
{
|
||||
name: "another email",
|
||||
input: "jane.smith@test.com",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hash1 := redactor.HashForLogging(tt.input)
|
||||
hash2 := redactor.HashForLogging(tt.input)
|
||||
|
||||
// Hash should be consistent
|
||||
if hash1 != hash2 {
|
||||
t.Errorf("HashForLogging is not consistent: %q != %q", hash1, hash2)
|
||||
}
|
||||
|
||||
// Hash should be 16 characters (8 bytes in hex)
|
||||
if len(hash1) != 16 {
|
||||
t.Errorf("HashForLogging length = %d, want 16", len(hash1))
|
||||
}
|
||||
|
||||
// Hash should not contain original value
|
||||
if hash1 == tt.input {
|
||||
t.Errorf("HashForLogging returned original value")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Different inputs should produce different hashes
|
||||
hash1 := redactor.HashForLogging("john.doe@example.com")
|
||||
hash2 := redactor.HashForLogging("jane.smith@example.com")
|
||||
if hash1 == hash2 {
|
||||
t.Error("Different inputs produced same hash")
|
||||
}
|
||||
|
||||
// Empty string
|
||||
emptyHash := redactor.HashForLogging("")
|
||||
if emptyHash != "[empty]" {
|
||||
t.Errorf("HashForLogging(\"\") = %q, want [empty]", emptyHash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactTenantSlug(t *testing.T) {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "normal slug",
|
||||
input: "my-company",
|
||||
expected: "my***",
|
||||
},
|
||||
{
|
||||
name: "short slug",
|
||||
input: "abc",
|
||||
expected: "***",
|
||||
},
|
||||
{
|
||||
name: "very short slug",
|
||||
input: "ab",
|
||||
expected: "***",
|
||||
},
|
||||
{
|
||||
name: "empty slug",
|
||||
input: "",
|
||||
expected: "[empty]",
|
||||
},
|
||||
{
|
||||
name: "long slug",
|
||||
input: "very-long-company-name",
|
||||
expected: "ve***",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := redactor.RedactTenantSlug(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("RedactTenantSlug(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactAPIKey(t *testing.T) {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "live API key",
|
||||
input: "live_sk_abc123def456ghi789",
|
||||
expected: "live_sk_***i789",
|
||||
},
|
||||
{
|
||||
name: "test API key",
|
||||
input: "test_sk_xyz789uvw456rst123",
|
||||
expected: "test_sk_***t123",
|
||||
},
|
||||
{
|
||||
name: "short live key",
|
||||
input: "live_sk_abc",
|
||||
expected: "live_sk_***",
|
||||
},
|
||||
{
|
||||
name: "other format",
|
||||
input: "sk_abc123def456",
|
||||
expected: "***f456",
|
||||
},
|
||||
{
|
||||
name: "very short key",
|
||||
input: "abc",
|
||||
expected: "***",
|
||||
},
|
||||
{
|
||||
name: "empty key",
|
||||
input: "",
|
||||
expected: "[empty]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := redactor.RedactAPIKey(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("RedactAPIKey(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactJWTToken(t *testing.T) {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "normal JWT",
|
||||
input: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
||||
expected: "eyJhbGci...P0THsR8U",
|
||||
},
|
||||
{
|
||||
name: "short token",
|
||||
input: "short",
|
||||
expected: "***",
|
||||
},
|
||||
{
|
||||
name: "empty token",
|
||||
input: "",
|
||||
expected: "[empty]",
|
||||
},
|
||||
{
|
||||
name: "minimum length token",
|
||||
input: "1234567890123456",
|
||||
expected: "12345678...90123456",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := redactor.RedactJWTToken(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("RedactJWTToken(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactIPAddress(t *testing.T) {
|
||||
redactor := NewSensitiveFieldRedactor()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "IPv4 address",
|
||||
input: "192.168.1.100",
|
||||
expected: "192.168.*.*",
|
||||
},
|
||||
{
|
||||
name: "IPv4 public",
|
||||
input: "8.8.8.8",
|
||||
expected: "8.8.*.*",
|
||||
},
|
||||
{
|
||||
name: "IPv6 address",
|
||||
input: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
|
||||
expected: "2001:0db8:85a3:0000:****",
|
||||
},
|
||||
{
|
||||
name: "IPv6 shortened",
|
||||
input: "2001:db8::1",
|
||||
expected: "2001:db8::1:****",
|
||||
},
|
||||
{
|
||||
name: "empty IP",
|
||||
input: "",
|
||||
expected: "[empty]",
|
||||
},
|
||||
{
|
||||
name: "invalid IP",
|
||||
input: "notanip",
|
||||
expected: "***",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := redactor.RedactIPAddress(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("RedactIPAddress(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserIdentifier(t *testing.T) {
|
||||
userID := "user_123"
|
||||
email := "john.doe@example.com"
|
||||
|
||||
fields := UserIdentifier(userID, email)
|
||||
|
||||
if len(fields) != 3 {
|
||||
t.Errorf("UserIdentifier returned %d fields, want 3", len(fields))
|
||||
}
|
||||
|
||||
// Check that fields contain expected keys
|
||||
fieldKeys := make(map[string]bool)
|
||||
for _, field := range fields {
|
||||
fieldKeys[field.Key] = true
|
||||
}
|
||||
|
||||
expectedKeys := []string{"user_id", "email_hash", "email_redacted"}
|
||||
for _, key := range expectedKeys {
|
||||
if !fieldKeys[key] {
|
||||
t.Errorf("UserIdentifier missing key: %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTenantIdentifier(t *testing.T) {
|
||||
tenantID := "tenant_123"
|
||||
slug := "my-company"
|
||||
|
||||
fields := TenantIdentifier(tenantID, slug)
|
||||
|
||||
if len(fields) != 3 {
|
||||
t.Errorf("TenantIdentifier returned %d fields, want 3", len(fields))
|
||||
}
|
||||
|
||||
// Check that fields contain expected keys
|
||||
fieldKeys := make(map[string]bool)
|
||||
for _, field := range fields {
|
||||
fieldKeys[field.Key] = true
|
||||
}
|
||||
|
||||
expectedKeys := []string{"tenant_id", "tenant_slug_hash", "tenant_slug_redacted"}
|
||||
for _, key := range expectedKeys {
|
||||
if !fieldKeys[key] {
|
||||
t.Errorf("TenantIdentifier missing key: %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue