- Add gotestsum for real-time test progress visibility - Parallelize 174 tests across 14 files for faster execution - Add -short mode support skipping 21 heavy integration tests - Create testutil/db.go helper for future transaction rollbacks - Fix data race in notification_service_test.go - Fix 4 CrowdSec LAPI test failures with permissive validator Performance improvements: - Tests now run in parallel (174 tests with t.Parallel()) - Quick feedback loop via -short mode - Zero race conditions detected - Coverage maintained at 87.7% Closes test optimization initiative
170 lines
4.8 KiB
Go
170 lines
4.8 KiB
Go
package security
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestAuditEvent_JSONSerialization tests that audit events serialize correctly to JSON.
|
|
func TestAuditEvent_JSONSerialization(t *testing.T) {
|
|
t.Parallel()
|
|
event := AuditEvent{
|
|
Timestamp: "2025-12-31T12:00:00Z",
|
|
Action: "url_validation",
|
|
Host: "example.com",
|
|
RequestID: "test-123",
|
|
Result: "blocked",
|
|
ResolvedIPs: []string{"192.168.1.1", "10.0.0.1"},
|
|
BlockedReason: "private_ip",
|
|
UserID: "user123",
|
|
SourceIP: "203.0.113.1",
|
|
}
|
|
|
|
// Serialize to JSON
|
|
jsonBytes, err := json.Marshal(event)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal AuditEvent: %v", err)
|
|
}
|
|
|
|
// Verify all fields are present
|
|
jsonStr := string(jsonBytes)
|
|
expectedFields := []string{
|
|
"timestamp", "action", "host", "request_id", "result",
|
|
"resolved_ips", "blocked_reason", "user_id", "source_ip",
|
|
}
|
|
|
|
for _, field := range expectedFields {
|
|
if !strings.Contains(jsonStr, field) {
|
|
t.Errorf("JSON output missing field: %s", field)
|
|
}
|
|
}
|
|
|
|
// Deserialize and verify
|
|
var decoded AuditEvent
|
|
err = json.Unmarshal(jsonBytes, &decoded)
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal AuditEvent: %v", err)
|
|
}
|
|
|
|
if decoded.Timestamp != event.Timestamp {
|
|
t.Errorf("Timestamp mismatch: got %s, want %s", decoded.Timestamp, event.Timestamp)
|
|
}
|
|
if decoded.UserID != event.UserID {
|
|
t.Errorf("UserID mismatch: got %s, want %s", decoded.UserID, event.UserID)
|
|
}
|
|
if len(decoded.ResolvedIPs) != len(event.ResolvedIPs) {
|
|
t.Errorf("ResolvedIPs length mismatch: got %d, want %d", len(decoded.ResolvedIPs), len(event.ResolvedIPs))
|
|
}
|
|
}
|
|
|
|
// TestAuditLogger_LogURLValidation tests audit logging of URL validation events.
|
|
func TestAuditLogger_LogURLValidation(t *testing.T) {
|
|
t.Parallel()
|
|
logger := NewAuditLogger()
|
|
|
|
event := AuditEvent{
|
|
Action: "url_test",
|
|
Host: "malicious.com",
|
|
RequestID: "req-456",
|
|
Result: "blocked",
|
|
ResolvedIPs: []string{"169.254.169.254"},
|
|
BlockedReason: "metadata_endpoint",
|
|
UserID: "attacker",
|
|
SourceIP: "198.51.100.1",
|
|
}
|
|
|
|
// This will log to standard logger, which we can't easily capture in tests
|
|
// But we can verify it doesn't panic
|
|
logger.LogURLValidation(event)
|
|
|
|
// Verify timestamp was auto-added if missing
|
|
event2 := AuditEvent{
|
|
Action: "test",
|
|
Host: "test.com",
|
|
}
|
|
logger.LogURLValidation(event2)
|
|
}
|
|
|
|
// TestAuditLogger_LogURLTest tests the convenience method for URL tests.
|
|
func TestAuditLogger_LogURLTest(t *testing.T) {
|
|
t.Parallel()
|
|
logger := NewAuditLogger()
|
|
|
|
// Should not panic
|
|
logger.LogURLTest("example.com", "req-789", "user456", "192.0.2.1", "allowed")
|
|
}
|
|
|
|
// TestAuditLogger_LogSSRFBlock tests the convenience method for SSRF blocks.
|
|
func TestAuditLogger_LogSSRFBlock(t *testing.T) {
|
|
t.Parallel()
|
|
logger := NewAuditLogger()
|
|
|
|
resolvedIPs := []string{"10.0.0.1", "192.168.1.1"}
|
|
|
|
// Should not panic
|
|
logger.LogSSRFBlock("internal.local", resolvedIPs, "private_ip", "user123", "203.0.113.5")
|
|
}
|
|
|
|
// TestGlobalAuditLogger tests the global audit logger functions.
|
|
func TestGlobalAuditLogger(t *testing.T) {
|
|
t.Parallel()
|
|
// Test global functions don't panic
|
|
LogURLTest("test.com", "req-global", "user-global", "192.0.2.10", "allowed")
|
|
LogSSRFBlock("blocked.local", []string{"127.0.0.1"}, "loopback", "user-global", "198.51.100.10")
|
|
}
|
|
|
|
// TestAuditEvent_RequiredFields tests that required fields are enforced.
|
|
func TestAuditEvent_RequiredFields(t *testing.T) {
|
|
t.Parallel()
|
|
// CRITICAL: UserID field must be present for attribution
|
|
event := AuditEvent{
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
|
Action: "ssrf_block",
|
|
Host: "malicious.com",
|
|
RequestID: "req-security",
|
|
Result: "blocked",
|
|
ResolvedIPs: []string{"192.168.1.1"},
|
|
BlockedReason: "private_ip",
|
|
UserID: "attacker123", // REQUIRED per Supervisor review
|
|
SourceIP: "203.0.113.100",
|
|
}
|
|
|
|
jsonBytes, err := json.Marshal(event)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal: %v", err)
|
|
}
|
|
|
|
// Verify UserID is in JSON output
|
|
if !strings.Contains(string(jsonBytes), "attacker123") {
|
|
t.Errorf("UserID not found in audit log JSON")
|
|
}
|
|
}
|
|
|
|
// TestAuditLogger_TimestampFormat tests that timestamps use RFC3339 format.
|
|
func TestAuditLogger_TimestampFormat(t *testing.T) {
|
|
t.Parallel()
|
|
logger := NewAuditLogger()
|
|
|
|
event := AuditEvent{
|
|
Action: "test",
|
|
Host: "test.com",
|
|
// Timestamp intentionally omitted to test auto-generation
|
|
}
|
|
|
|
// Capture the event by marshaling after logging
|
|
// In real scenario, LogURLValidation sets the timestamp
|
|
if event.Timestamp == "" {
|
|
event.Timestamp = time.Now().UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
// Parse the timestamp to verify it's valid RFC3339
|
|
_, err := time.Parse(time.RFC3339, event.Timestamp)
|
|
if err != nil {
|
|
t.Errorf("Invalid timestamp format: %s, error: %v", event.Timestamp, err)
|
|
}
|
|
|
|
logger.LogURLValidation(event)
|
|
}
|