package aether import ( "encoding/json" "errors" "fmt" "strings" "testing" "time" ) func TestEvent_JSONSerialization(t *testing.T) { ts := time.Date(2026, 1, 9, 12, 0, 0, 0, time.UTC) event := &Event{ ID: "evt-123", EventType: "OrderPlaced", ActorID: "order-456", CommandID: "cmd-789", Version: 1, Data: map[string]interface{}{ "total": 100.50, "currency": "USD", }, Timestamp: ts, } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event: %v", err) } if decoded.ID != event.ID { t.Errorf("ID mismatch: got %q, want %q", decoded.ID, event.ID) } if decoded.EventType != event.EventType { t.Errorf("EventType mismatch: got %q, want %q", decoded.EventType, event.EventType) } if decoded.ActorID != event.ActorID { t.Errorf("ActorID mismatch: got %q, want %q", decoded.ActorID, event.ActorID) } if decoded.CommandID != event.CommandID { t.Errorf("CommandID mismatch: got %q, want %q", decoded.CommandID, event.CommandID) } if decoded.Version != event.Version { t.Errorf("Version mismatch: got %d, want %d", decoded.Version, event.Version) } if !decoded.Timestamp.Equal(event.Timestamp) { t.Errorf("Timestamp mismatch: got %v, want %v", decoded.Timestamp, event.Timestamp) } if decoded.Data["total"] != event.Data["total"] { t.Errorf("Data.total mismatch: got %v, want %v", decoded.Data["total"], event.Data["total"]) } if decoded.Data["currency"] != event.Data["currency"] { t.Errorf("Data.currency mismatch: got %v, want %v", decoded.Data["currency"], event.Data["currency"]) } } func TestEvent_JSONFieldNames(t *testing.T) { event := &Event{ ID: "evt-123", EventType: "TestEvent", ActorID: "actor-456", CommandID: "cmd-789", Version: 1, Data: map[string]interface{}{}, Timestamp: time.Now(), } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event: %v", err) } jsonStr := string(data) expectedFields := []string{ `"id"`, `"eventType"`, `"actorId"`, `"commandId"`, `"version"`, `"data"`, `"timestamp"`, } for _, field := range expectedFields { if !strings.Contains(jsonStr, field) { t.Errorf("expected JSON field %s not found in: %s", field, jsonStr) } } } func TestEvent_OmitEmptyCommandID(t *testing.T) { event := &Event{ ID: "evt-123", EventType: "TestEvent", ActorID: "actor-456", Version: 1, Data: map[string]interface{}{}, Timestamp: time.Now(), } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event: %v", err) } if strings.Contains(string(data), `"commandId"`) { t.Error("expected commandId to be omitted when empty") } } func TestActorSnapshot_JSONSerialization(t *testing.T) { ts := time.Date(2026, 1, 9, 12, 0, 0, 0, time.UTC) snapshot := &ActorSnapshot{ ActorID: "actor-123", Version: 10, State: map[string]interface{}{ "balance": 500.00, "status": "active", "metadata": map[string]interface{}{"region": "us-west"}, }, Timestamp: ts, } data, err := json.Marshal(snapshot) if err != nil { t.Fatalf("failed to marshal ActorSnapshot: %v", err) } var decoded ActorSnapshot if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal ActorSnapshot: %v", err) } if decoded.ActorID != snapshot.ActorID { t.Errorf("ActorID mismatch: got %q, want %q", decoded.ActorID, snapshot.ActorID) } if decoded.Version != snapshot.Version { t.Errorf("Version mismatch: got %d, want %d", decoded.Version, snapshot.Version) } if !decoded.Timestamp.Equal(snapshot.Timestamp) { t.Errorf("Timestamp mismatch: got %v, want %v", decoded.Timestamp, snapshot.Timestamp) } if decoded.State["balance"] != snapshot.State["balance"] { t.Errorf("State.balance mismatch: got %v, want %v", decoded.State["balance"], snapshot.State["balance"]) } if decoded.State["status"] != snapshot.State["status"] { t.Errorf("State.status mismatch: got %v, want %v", decoded.State["status"], snapshot.State["status"]) } } func TestActorSnapshot_JSONFieldNames(t *testing.T) { snapshot := &ActorSnapshot{ ActorID: "actor-123", Version: 1, State: map[string]interface{}{}, Timestamp: time.Now(), } data, err := json.Marshal(snapshot) if err != nil { t.Fatalf("failed to marshal ActorSnapshot: %v", err) } jsonStr := string(data) expectedFields := []string{ `"actorId"`, `"version"`, `"state"`, `"timestamp"`, } for _, field := range expectedFields { if !strings.Contains(jsonStr, field) { t.Errorf("expected JSON field %s not found in: %s", field, jsonStr) } } } func TestEvent_EmptyData(t *testing.T) { event := &Event{ ID: "evt-empty", EventType: "EmptyEvent", ActorID: "actor-123", Version: 1, Data: map[string]interface{}{}, Timestamp: time.Now(), } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event with empty data: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event with empty data: %v", err) } if len(decoded.Data) != 0 { t.Errorf("expected empty Data map, got %v", decoded.Data) } } func TestEvent_NilData(t *testing.T) { event := &Event{ ID: "evt-nil", EventType: "NilDataEvent", ActorID: "actor-123", Version: 1, Data: nil, Timestamp: time.Now(), } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event with nil data: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event with nil data: %v", err) } if decoded.Data != nil { t.Errorf("expected nil Data, got %v", decoded.Data) } } func TestActorSnapshot_EmptyState(t *testing.T) { snapshot := &ActorSnapshot{ ActorID: "actor-empty", Version: 1, State: map[string]interface{}{}, Timestamp: time.Now(), } data, err := json.Marshal(snapshot) if err != nil { t.Fatalf("failed to marshal ActorSnapshot with empty state: %v", err) } var decoded ActorSnapshot if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal ActorSnapshot with empty state: %v", err) } if len(decoded.State) != 0 { t.Errorf("expected empty State map, got %v", decoded.State) } } func TestActorSnapshot_NilState(t *testing.T) { snapshot := &ActorSnapshot{ ActorID: "actor-nil", Version: 1, State: nil, Timestamp: time.Now(), } data, err := json.Marshal(snapshot) if err != nil { t.Fatalf("failed to marshal ActorSnapshot with nil state: %v", err) } var decoded ActorSnapshot if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal ActorSnapshot with nil state: %v", err) } if decoded.State != nil { t.Errorf("expected nil State, got %v", decoded.State) } } func TestEvent_LargePayload(t *testing.T) { largeString := strings.Repeat("x", 1024*1024) // 1MB string event := &Event{ ID: "evt-large", EventType: "LargePayloadEvent", ActorID: "actor-123", Version: 1, Data: map[string]interface{}{ "largeField": largeString, }, Timestamp: time.Now(), } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event with large payload: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event with large payload: %v", err) } if decoded.Data["largeField"] != largeString { t.Error("large payload was not preserved correctly") } } func TestActorSnapshot_LargePayload(t *testing.T) { largeString := strings.Repeat("y", 1024*1024) // 1MB string snapshot := &ActorSnapshot{ ActorID: "actor-large", Version: 1, State: map[string]interface{}{ "largeField": largeString, }, Timestamp: time.Now(), } data, err := json.Marshal(snapshot) if err != nil { t.Fatalf("failed to marshal ActorSnapshot with large payload: %v", err) } var decoded ActorSnapshot if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal ActorSnapshot with large payload: %v", err) } if decoded.State["largeField"] != largeString { t.Error("large payload was not preserved correctly") } } func TestEvent_SpecialCharacters(t *testing.T) { specialStrings := map[string]interface{}{ "unicode": "Hello, \u4e16\u754c! \U0001F600", "newlines": "line1\nline2\r\nline3", "tabs": "col1\tcol2\tcol3", "quotes": `"quoted" and 'single'`, "backslash": `path\to\file`, "nullchar": "before\x00after", "html": "", "json_chars": `{"nested": "value"}`, } event := &Event{ ID: "evt-special", EventType: "Special\nChars", ActorID: "actor-\t123", Version: 1, Data: specialStrings, Timestamp: time.Now(), } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event with special characters: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event with special characters: %v", err) } for key, expected := range specialStrings { if decoded.Data[key] != expected { t.Errorf("Data[%q] mismatch: got %q, want %q", key, decoded.Data[key], expected) } } if decoded.EventType != event.EventType { t.Errorf("EventType with special chars mismatch: got %q, want %q", decoded.EventType, event.EventType) } if decoded.ActorID != event.ActorID { t.Errorf("ActorID with special chars mismatch: got %q, want %q", decoded.ActorID, event.ActorID) } } func TestActorSnapshot_SpecialCharacters(t *testing.T) { specialState := map[string]interface{}{ "unicode": "\u00e9\u00e8\u00ea", "emoji": "\U0001F4BB\U0001F680", "brackets": "[]{}()", } snapshot := &ActorSnapshot{ ActorID: "actor-\u00e9", Version: 1, State: specialState, Timestamp: time.Now(), } data, err := json.Marshal(snapshot) if err != nil { t.Fatalf("failed to marshal ActorSnapshot with special characters: %v", err) } var decoded ActorSnapshot if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal ActorSnapshot with special characters: %v", err) } for key, expected := range specialState { if decoded.State[key] != expected { t.Errorf("State[%q] mismatch: got %q, want %q", key, decoded.State[key], expected) } } } func TestEvent_TimestampTimezones(t *testing.T) { timezones := []struct { name string loc *time.Location }{ {"UTC", time.UTC}, {"FixedEast", time.FixedZone("EST", -5*3600)}, {"FixedWest", time.FixedZone("PST", -8*3600)}, {"FixedPositive", time.FixedZone("JST", 9*3600)}, } for _, tz := range timezones { t.Run(tz.name, func(t *testing.T) { ts := time.Date(2026, 6, 15, 14, 30, 45, 123456789, tz.loc) event := &Event{ ID: "evt-tz", EventType: "TimezoneTest", ActorID: "actor-123", Version: 1, Data: map[string]interface{}{}, Timestamp: ts, } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event: %v", err) } // JSON timestamp preserves the instant in time (Unix time) // but may normalize to UTC on decode if !decoded.Timestamp.Equal(ts) { t.Errorf("Timestamp instant mismatch: got %v, want %v", decoded.Timestamp.Unix(), ts.Unix()) } }) } } func TestActorSnapshot_TimestampTimezones(t *testing.T) { timezones := []struct { name string loc *time.Location }{ {"UTC", time.UTC}, {"FixedEast", time.FixedZone("CET", 1*3600)}, {"FixedWest", time.FixedZone("HST", -10*3600)}, } for _, tz := range timezones { t.Run(tz.name, func(t *testing.T) { ts := time.Date(2026, 12, 31, 23, 59, 59, 999999999, tz.loc) snapshot := &ActorSnapshot{ ActorID: "actor-tz", Version: 100, State: map[string]interface{}{}, Timestamp: ts, } data, err := json.Marshal(snapshot) if err != nil { t.Fatalf("failed to marshal ActorSnapshot: %v", err) } var decoded ActorSnapshot if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal ActorSnapshot: %v", err) } if !decoded.Timestamp.Equal(ts) { t.Errorf("Timestamp instant mismatch: got %v, want %v", decoded.Timestamp.Unix(), ts.Unix()) } }) } } func TestEvent_TimestampNanosecondPrecision(t *testing.T) { ts := time.Date(2026, 1, 9, 12, 0, 0, 123456789, time.UTC) event := &Event{ ID: "evt-nano", EventType: "NanoTest", ActorID: "actor-123", Version: 1, Data: map[string]interface{}{}, Timestamp: ts, } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event: %v", err) } // Go's time.Time JSON marshaling uses RFC3339Nano which preserves nanoseconds if decoded.Timestamp.Nanosecond() != ts.Nanosecond() { t.Errorf("Nanosecond precision lost: got %d, want %d", decoded.Timestamp.Nanosecond(), ts.Nanosecond()) } } func TestEvent_ZeroTimestamp(t *testing.T) { event := &Event{ ID: "evt-zero", EventType: "ZeroTimeTest", ActorID: "actor-123", Version: 1, Data: map[string]interface{}{}, Timestamp: time.Time{}, } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event with zero timestamp: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event with zero timestamp: %v", err) } if !decoded.Timestamp.IsZero() { t.Errorf("expected zero timestamp, got %v", decoded.Timestamp) } } func TestEvent_NestedData(t *testing.T) { nestedData := map[string]interface{}{ "level1": map[string]interface{}{ "level2": map[string]interface{}{ "level3": map[string]interface{}{ "value": "deeply nested", }, }, }, "array": []interface{}{"a", "b", "c"}, "mixed": []interface{}{ map[string]interface{}{"key": "value"}, 123.0, true, nil, }, } event := &Event{ ID: "evt-nested", EventType: "NestedDataEvent", ActorID: "actor-123", Version: 1, Data: nestedData, Timestamp: time.Now(), } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event with nested data: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event with nested data: %v", err) } // Verify nested structure is preserved level1, ok := decoded.Data["level1"].(map[string]interface{}) if !ok { t.Fatal("level1 is not a map") } level2, ok := level1["level2"].(map[string]interface{}) if !ok { t.Fatal("level2 is not a map") } level3, ok := level2["level3"].(map[string]interface{}) if !ok { t.Fatal("level3 is not a map") } if level3["value"] != "deeply nested" { t.Errorf("deeply nested value mismatch: got %v", level3["value"]) } } func TestEvent_NumericTypes(t *testing.T) { event := &Event{ ID: "evt-numeric", EventType: "NumericTest", ActorID: "actor-123", Version: 1, Data: map[string]interface{}{ "integer": 42, "float": 3.14159, "negative": -100, "zero": 0, "large": 1e15, }, Timestamp: time.Now(), } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event with numeric data: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event with numeric data: %v", err) } // JSON numbers decode as float64 if decoded.Data["integer"].(float64) != 42 { t.Errorf("integer mismatch: got %v", decoded.Data["integer"]) } if decoded.Data["float"].(float64) != 3.14159 { t.Errorf("float mismatch: got %v", decoded.Data["float"]) } } func TestEvent_BooleanAndNull(t *testing.T) { event := &Event{ ID: "evt-bool-null", EventType: "BoolNullTest", ActorID: "actor-123", Version: 1, Data: map[string]interface{}{ "trueVal": true, "falseVal": false, "nullVal": nil, }, Timestamp: time.Now(), } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event: %v", err) } if decoded.Data["trueVal"] != true { t.Errorf("trueVal mismatch: got %v", decoded.Data["trueVal"]) } if decoded.Data["falseVal"] != false { t.Errorf("falseVal mismatch: got %v", decoded.Data["falseVal"]) } if decoded.Data["nullVal"] != nil { t.Errorf("nullVal mismatch: got %v", decoded.Data["nullVal"]) } } func TestEvent_VersionEdgeCases(t *testing.T) { testCases := []struct { name string version int64 }{ {"zero", 0}, {"one", 1}, {"large", 9223372036854775807}, // MaxInt64 {"negative", -1}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { event := &Event{ ID: "evt-version", EventType: "VersionTest", ActorID: "actor-123", Version: tc.version, Data: map[string]interface{}{}, Timestamp: time.Now(), } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event: %v", err) } if decoded.Version != tc.version { t.Errorf("Version mismatch: got %d, want %d", decoded.Version, tc.version) } }) } } func TestActorSnapshot_VersionEdgeCases(t *testing.T) { testCases := []struct { name string version int64 }{ {"zero", 0}, {"one", 1}, {"large", 9223372036854775807}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { snapshot := &ActorSnapshot{ ActorID: "actor-version", Version: tc.version, State: map[string]interface{}{}, Timestamp: time.Now(), } data, err := json.Marshal(snapshot) if err != nil { t.Fatalf("failed to marshal ActorSnapshot: %v", err) } var decoded ActorSnapshot if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal ActorSnapshot: %v", err) } if decoded.Version != tc.version { t.Errorf("Version mismatch: got %d, want %d", decoded.Version, tc.version) } }) } } // Tests for Event Metadata support func TestEvent_MetadataJSONSerialization(t *testing.T) { ts := time.Date(2026, 1, 9, 12, 0, 0, 0, time.UTC) event := &Event{ ID: "evt-meta", EventType: "OrderPlaced", ActorID: "order-456", Version: 1, Data: map[string]interface{}{"total": 100.50}, Metadata: map[string]string{ "correlationId": "corr-123", "causationId": "cause-456", "userId": "user-789", }, Timestamp: ts, } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event with metadata: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event with metadata: %v", err) } if decoded.Metadata == nil { t.Fatal("expected Metadata to be present after unmarshal") } if decoded.Metadata["correlationId"] != "corr-123" { t.Errorf("correlationId mismatch: got %q, want %q", decoded.Metadata["correlationId"], "corr-123") } if decoded.Metadata["causationId"] != "cause-456" { t.Errorf("causationId mismatch: got %q, want %q", decoded.Metadata["causationId"], "cause-456") } if decoded.Metadata["userId"] != "user-789" { t.Errorf("userId mismatch: got %q, want %q", decoded.Metadata["userId"], "user-789") } } func TestEvent_MetadataOmitEmpty(t *testing.T) { event := &Event{ ID: "evt-no-meta", EventType: "TestEvent", ActorID: "actor-123", Version: 1, Data: map[string]interface{}{}, Metadata: nil, Timestamp: time.Now(), } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event: %v", err) } if strings.Contains(string(data), `"metadata"`) { t.Error("expected metadata to be omitted when nil") } } func TestEvent_EmptyMetadata(t *testing.T) { event := &Event{ ID: "evt-empty-meta", EventType: "TestEvent", ActorID: "actor-123", Version: 1, Data: map[string]interface{}{}, Metadata: map[string]string{}, Timestamp: time.Now(), } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event with empty metadata: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event with empty metadata: %v", err) } // Empty map should be omitted with omitempty if strings.Contains(string(data), `"metadata"`) { t.Error("expected empty metadata to be omitted") } } func TestEvent_SetMetadata(t *testing.T) { event := &Event{ ID: "evt-set-meta", EventType: "TestEvent", ActorID: "actor-123", Version: 1, } // Metadata should be nil initially if event.Metadata != nil { t.Error("expected Metadata to be nil initially") } // SetMetadata should initialize the map event.SetMetadata("key1", "value1") if event.Metadata == nil { t.Fatal("expected Metadata to be initialized after SetMetadata") } if event.Metadata["key1"] != "value1" { t.Errorf("key1 mismatch: got %q, want %q", event.Metadata["key1"], "value1") } // SetMetadata should work with existing map event.SetMetadata("key2", "value2") if event.Metadata["key2"] != "value2" { t.Errorf("key2 mismatch: got %q, want %q", event.Metadata["key2"], "value2") } if event.Metadata["key1"] != "value1" { t.Errorf("key1 was overwritten: got %q", event.Metadata["key1"]) } } func TestEvent_GetMetadata(t *testing.T) { event := &Event{ ID: "evt-get-meta", EventType: "TestEvent", ActorID: "actor-123", Version: 1, } // GetMetadata on nil map should return empty string if got := event.GetMetadata("nonexistent"); got != "" { t.Errorf("expected empty string for nil Metadata, got %q", got) } // Initialize metadata and test event.Metadata = map[string]string{"key": "value"} if got := event.GetMetadata("key"); got != "value" { t.Errorf("GetMetadata mismatch: got %q, want %q", got, "value") } // Non-existent key should return empty string if got := event.GetMetadata("nonexistent"); got != "" { t.Errorf("expected empty string for nonexistent key, got %q", got) } } func TestEvent_CorrelationIDHelpers(t *testing.T) { event := &Event{ ID: "evt-corr", EventType: "TestEvent", ActorID: "actor-123", Version: 1, } // GetCorrelationID on nil metadata if got := event.GetCorrelationID(); got != "" { t.Errorf("expected empty correlation ID, got %q", got) } // SetCorrelationID event.SetCorrelationID("corr-abc-123") if got := event.GetCorrelationID(); got != "corr-abc-123" { t.Errorf("GetCorrelationID mismatch: got %q, want %q", got, "corr-abc-123") } // Verify it uses the correct metadata key if event.Metadata[MetadataKeyCorrelationID] != "corr-abc-123" { t.Errorf("expected correlationId key to be set") } } func TestEvent_CausationIDHelpers(t *testing.T) { event := &Event{ ID: "evt-cause", EventType: "TestEvent", ActorID: "actor-123", Version: 1, } // GetCausationID on nil metadata if got := event.GetCausationID(); got != "" { t.Errorf("expected empty causation ID, got %q", got) } // SetCausationID event.SetCausationID("cause-xyz-789") if got := event.GetCausationID(); got != "cause-xyz-789" { t.Errorf("GetCausationID mismatch: got %q, want %q", got, "cause-xyz-789") } // Verify it uses the correct metadata key if event.Metadata[MetadataKeyCausationID] != "cause-xyz-789" { t.Errorf("expected causationId key to be set") } } func TestEvent_UserIDHelpers(t *testing.T) { event := &Event{ ID: "evt-user", EventType: "TestEvent", ActorID: "actor-123", Version: 1, } event.SetUserID("user-john-doe") if got := event.GetUserID(); got != "user-john-doe" { t.Errorf("GetUserID mismatch: got %q, want %q", got, "user-john-doe") } if event.Metadata[MetadataKeyUserID] != "user-john-doe" { t.Errorf("expected userId key to be set") } } func TestEvent_TraceIDHelpers(t *testing.T) { event := &Event{ ID: "evt-trace", EventType: "TestEvent", ActorID: "actor-123", Version: 1, } event.SetTraceID("trace-abc-def-ghi") if got := event.GetTraceID(); got != "trace-abc-def-ghi" { t.Errorf("GetTraceID mismatch: got %q, want %q", got, "trace-abc-def-ghi") } if event.Metadata[MetadataKeyTraceID] != "trace-abc-def-ghi" { t.Errorf("expected traceId key to be set") } } func TestEvent_SpanIDHelpers(t *testing.T) { event := &Event{ ID: "evt-span", EventType: "TestEvent", ActorID: "actor-123", Version: 1, } event.SetSpanID("span-123-456") if got := event.GetSpanID(); got != "span-123-456" { t.Errorf("GetSpanID mismatch: got %q, want %q", got, "span-123-456") } if event.Metadata[MetadataKeySpanID] != "span-123-456" { t.Errorf("expected spanId key to be set") } } func TestEvent_WithMetadataFrom(t *testing.T) { source := &Event{ ID: "evt-source", EventType: "SourceEvent", ActorID: "actor-123", Version: 1, Metadata: map[string]string{ "correlationId": "corr-123", "causationId": "cause-456", "customKey": "customValue", }, } target := &Event{ ID: "evt-target", EventType: "TargetEvent", ActorID: "actor-456", Version: 2, } // Copy metadata from source to target target.WithMetadataFrom(source) if target.Metadata == nil { t.Fatal("expected Metadata to be initialized after WithMetadataFrom") } if target.Metadata["correlationId"] != "corr-123" { t.Errorf("correlationId not copied: got %q", target.Metadata["correlationId"]) } if target.Metadata["causationId"] != "cause-456" { t.Errorf("causationId not copied: got %q", target.Metadata["causationId"]) } if target.Metadata["customKey"] != "customValue" { t.Errorf("customKey not copied: got %q", target.Metadata["customKey"]) } } func TestEvent_WithMetadataFromNilSource(t *testing.T) { target := &Event{ ID: "evt-target", EventType: "TargetEvent", ActorID: "actor-456", Version: 2, } // WithMetadataFrom should handle nil source gracefully target.WithMetadataFrom(nil) if target.Metadata != nil { t.Error("expected Metadata to remain nil after WithMetadataFrom(nil)") } // WithMetadataFrom should handle source with nil metadata source := &Event{ ID: "evt-source", EventType: "SourceEvent", ActorID: "actor-123", Version: 1, Metadata: nil, } target.WithMetadataFrom(source) if target.Metadata != nil { t.Error("expected Metadata to remain nil when source has nil Metadata") } } func TestEvent_WithMetadataFromPreservesExisting(t *testing.T) { source := &Event{ ID: "evt-source", EventType: "SourceEvent", ActorID: "actor-123", Version: 1, Metadata: map[string]string{ "sourceKey": "sourceValue", }, } target := &Event{ ID: "evt-target", EventType: "TargetEvent", ActorID: "actor-456", Version: 2, Metadata: map[string]string{ "existingKey": "existingValue", }, } target.WithMetadataFrom(source) // Both keys should be present if target.Metadata["existingKey"] != "existingValue" { t.Errorf("existingKey was lost: got %q", target.Metadata["existingKey"]) } if target.Metadata["sourceKey"] != "sourceValue" { t.Errorf("sourceKey not copied: got %q", target.Metadata["sourceKey"]) } } func TestEvent_MetadataSpecialCharacters(t *testing.T) { event := &Event{ ID: "evt-special-meta", EventType: "TestEvent", ActorID: "actor-123", Version: 1, Metadata: map[string]string{ "unicode": "Hello, \u4e16\u754c!", "newlines": "line1\nline2", "quotes": `"quoted"`, "backslash": `path\to\file`, "empty": "", "whitespace": " spaces ", }, } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event with special metadata: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event with special metadata: %v", err) } for key, expected := range event.Metadata { if decoded.Metadata[key] != expected { t.Errorf("Metadata[%q] mismatch: got %q, want %q", key, decoded.Metadata[key], expected) } } } func TestMetadataKeyConstants(t *testing.T) { // Verify the constant values are as expected if MetadataKeyCorrelationID != "correlationId" { t.Errorf("MetadataKeyCorrelationID wrong: got %q", MetadataKeyCorrelationID) } if MetadataKeyCausationID != "causationId" { t.Errorf("MetadataKeyCausationID wrong: got %q", MetadataKeyCausationID) } if MetadataKeyUserID != "userId" { t.Errorf("MetadataKeyUserID wrong: got %q", MetadataKeyUserID) } if MetadataKeyTraceID != "traceId" { t.Errorf("MetadataKeyTraceID wrong: got %q", MetadataKeyTraceID) } if MetadataKeySpanID != "spanId" { t.Errorf("MetadataKeySpanID wrong: got %q", MetadataKeySpanID) } } func TestEvent_MetadataJSONFieldName(t *testing.T) { event := &Event{ ID: "evt-field-name", EventType: "TestEvent", ActorID: "actor-123", Version: 1, Data: map[string]interface{}{}, Metadata: map[string]string{ "key": "value", }, Timestamp: time.Now(), } data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event: %v", err) } // Check for correct JSON field name (camelCase) if !strings.Contains(string(data), `"metadata"`) { t.Errorf("expected 'metadata' JSON field, got: %s", string(data)) } } func TestEvent_MetadataAllHelpersRoundTrip(t *testing.T) { // Test that all helper methods work together in a roundtrip event := &Event{ ID: "evt-all-helpers", EventType: "TestEvent", ActorID: "actor-123", Version: 1, Data: map[string]interface{}{}, Timestamp: time.Now(), } event.SetCorrelationID("corr-1") event.SetCausationID("cause-2") event.SetUserID("user-3") event.SetTraceID("trace-4") event.SetSpanID("span-5") data, err := json.Marshal(event) if err != nil { t.Fatalf("failed to marshal Event: %v", err) } var decoded Event if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("failed to unmarshal Event: %v", err) } if decoded.GetCorrelationID() != "corr-1" { t.Errorf("GetCorrelationID mismatch: got %q", decoded.GetCorrelationID()) } if decoded.GetCausationID() != "cause-2" { t.Errorf("GetCausationID mismatch: got %q", decoded.GetCausationID()) } if decoded.GetUserID() != "user-3" { t.Errorf("GetUserID mismatch: got %q", decoded.GetUserID()) } if decoded.GetTraceID() != "trace-4" { t.Errorf("GetTraceID mismatch: got %q", decoded.GetTraceID()) } if decoded.GetSpanID() != "span-5" { t.Errorf("GetSpanID mismatch: got %q", decoded.GetSpanID()) } } // Tests for ReplayError and ReplayResult types func TestReplayError_Error(t *testing.T) { err := &ReplayError{ SequenceNumber: 42, RawData: []byte(`invalid json`), Err: json.Unmarshal([]byte(`{`), &struct{}{}), } errMsg := err.Error() if !strings.Contains(errMsg, "42") { t.Errorf("expected error message to contain sequence number, got: %s", errMsg) } if !strings.Contains(errMsg, "unmarshal") || !strings.Contains(errMsg, "failed") { t.Errorf("expected error message to contain 'failed' and 'unmarshal', got: %s", errMsg) } } func TestReplayError_Unwrap(t *testing.T) { innerErr := json.Unmarshal([]byte(`{`), &struct{}{}) err := &ReplayError{ SequenceNumber: 1, RawData: []byte(`{`), Err: innerErr, } unwrapped := err.Unwrap() if unwrapped != innerErr { t.Errorf("expected Unwrap to return inner error") } } func TestReplayResult_HasErrors(t *testing.T) { tests := []struct { name string result *ReplayResult expected bool }{ { name: "no errors", result: &ReplayResult{Events: []*Event{}, Errors: []ReplayError{}}, expected: false, }, { name: "nil errors slice", result: &ReplayResult{Events: []*Event{}, Errors: nil}, expected: false, }, { name: "has errors", result: &ReplayResult{ Events: []*Event{}, Errors: []ReplayError{ {SequenceNumber: 1, RawData: []byte(`bad`), Err: nil}, }, }, expected: true, }, { name: "has events and errors", result: &ReplayResult{ Events: []*Event{{ID: "evt-1"}}, Errors: []ReplayError{ {SequenceNumber: 2, RawData: []byte(`bad`), Err: nil}, }, }, expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.result.HasErrors(); got != tt.expected { t.Errorf("HasErrors() = %v, want %v", got, tt.expected) } }) } } func TestReplayResult_EmptyResult(t *testing.T) { result := &ReplayResult{ Events: []*Event{}, Errors: []ReplayError{}, } if result.HasErrors() { t.Error("expected HasErrors() to return false for empty result") } if len(result.Events) != 0 { t.Errorf("expected 0 events, got %d", len(result.Events)) } } func TestReplayError_WithZeroSequence(t *testing.T) { err := &ReplayError{ SequenceNumber: 0, RawData: []byte(`corrupted`), Err: json.Unmarshal([]byte(`not-json`), &struct{}{}), } errMsg := err.Error() if !strings.Contains(errMsg, "sequence 0") { t.Errorf("expected error message to contain 'sequence 0', got: %s", errMsg) } } func TestReplayError_WithLargeRawData(t *testing.T) { largeData := make([]byte, 1024*1024) // 1MB for i := range largeData { largeData[i] = 'x' } err := &ReplayError{ SequenceNumber: 999, RawData: largeData, Err: json.Unmarshal(largeData, &struct{}{}), } // Should be able to create the error without issues if len(err.RawData) != 1024*1024 { t.Errorf("expected RawData to be preserved, got length %d", len(err.RawData)) } // Error() should still work _ = err.Error() } // Tests for VersionConflictError func TestVersionConflictError_Error(t *testing.T) { err := &VersionConflictError{ ActorID: "order-123", AttemptedVersion: 3, CurrentVersion: 5, } errMsg := err.Error() // Verify error message contains all context if !strings.Contains(errMsg, "order-123") { t.Errorf("error message should contain ActorID, got: %s", errMsg) } if !strings.Contains(errMsg, "3") { t.Errorf("error message should contain AttemptedVersion, got: %s", errMsg) } if !strings.Contains(errMsg, "5") { t.Errorf("error message should contain CurrentVersion, got: %s", errMsg) } if !strings.Contains(errMsg, "version conflict") { t.Errorf("error message should contain 'version conflict', got: %s", errMsg) } } func TestVersionConflictError_Fields(t *testing.T) { err := &VersionConflictError{ ActorID: "actor-456", AttemptedVersion: 10, CurrentVersion: 8, } if err.ActorID != "actor-456" { t.Errorf("ActorID mismatch: got %q, want %q", err.ActorID, "actor-456") } if err.AttemptedVersion != 10 { t.Errorf("AttemptedVersion mismatch: got %d, want %d", err.AttemptedVersion, 10) } if err.CurrentVersion != 8 { t.Errorf("CurrentVersion mismatch: got %d, want %d", err.CurrentVersion, 8) } } func TestVersionConflictError_Unwrap(t *testing.T) { err := &VersionConflictError{ ActorID: "actor-789", AttemptedVersion: 2, CurrentVersion: 1, } unwrapped := err.Unwrap() if unwrapped != ErrVersionConflict { t.Errorf("Unwrap should return ErrVersionConflict sentinel") } } func TestVersionConflictError_ErrorsIs(t *testing.T) { err := &VersionConflictError{ ActorID: "test-actor", AttemptedVersion: 5, CurrentVersion: 4, } // Test that errors.Is works with sentinel if !errors.Is(err, ErrVersionConflict) { t.Error("errors.Is(err, ErrVersionConflict) should return true") } // Test that other errors don't match if errors.Is(err, errors.New("other error")) { t.Error("errors.Is should not match unrelated errors") } } func TestVersionConflictError_ErrorsAs(t *testing.T) { originalErr := &VersionConflictError{ ActorID: "actor-unwrap", AttemptedVersion: 7, CurrentVersion: 6, } var versionErr *VersionConflictError if !errors.As(originalErr, &versionErr) { t.Fatalf("errors.As should succeed with VersionConflictError") } // Verify fields are accessible through unwrapped error if versionErr.ActorID != "actor-unwrap" { t.Errorf("ActorID mismatch after As: got %q", versionErr.ActorID) } if versionErr.AttemptedVersion != 7 { t.Errorf("AttemptedVersion mismatch after As: got %d", versionErr.AttemptedVersion) } if versionErr.CurrentVersion != 6 { t.Errorf("CurrentVersion mismatch after As: got %d", versionErr.CurrentVersion) } } func TestVersionConflictError_CanReadCurrentVersion(t *testing.T) { // This test verifies that applications can read CurrentVersion for retry strategies err := &VersionConflictError{ ActorID: "order-abc", AttemptedVersion: 2, CurrentVersion: 10, } var versionErr *VersionConflictError if !errors.As(err, &versionErr) { t.Fatal("failed to unwrap VersionConflictError") } // Application can use CurrentVersion to decide retry strategy nextVersion := versionErr.CurrentVersion + 1 if nextVersion != 11 { t.Errorf("application should be able to compute next version: got %d, want 11", nextVersion) } // Application can log detailed context logMsg := fmt.Sprintf("Version conflict for actor %q: attempted %d, current %d, will retry with %d", versionErr.ActorID, versionErr.AttemptedVersion, versionErr.CurrentVersion, nextVersion) if !strings.Contains(logMsg, "order-abc") { t.Errorf("application context logging failed: %s", logMsg) } } func TestVersionConflictError_EdgeCases(t *testing.T) { testCases := []struct { name string actorID string attemp int64 current int64 }{ {"zero current", "actor-1", 1, 0}, {"large numbers", "actor-2", 1000000, 999999}, {"max int64", "actor-3", 9223372036854775807, 9223372036854775806}, {"negative attempt", "actor-4", -1, -2}, {"empty actor id", "", 1, 0}, {"special chars in actor id", "actor@#$%", 2, 1}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { err := &VersionConflictError{ ActorID: tc.actorID, AttemptedVersion: tc.attemp, CurrentVersion: tc.current, } // Should not panic msg := err.Error() if msg == "" { t.Error("Error() should return non-empty string") } // Should be wrapped correctly if err.Unwrap() != ErrVersionConflict { t.Error("Unwrap should return ErrVersionConflict") } // errors.Is should work if !errors.Is(err, ErrVersionConflict) { t.Error("errors.Is should work for edge case") } }) } } func TestErrVersionConflict_Sentinel(t *testing.T) { // Verify the sentinel error is correctly defined if ErrVersionConflict == nil { t.Fatal("ErrVersionConflict should not be nil") } expectedMsg := "version conflict" if ErrVersionConflict.Error() != expectedMsg { t.Errorf("ErrVersionConflict message mismatch: got %q, want %q", ErrVersionConflict.Error(), expectedMsg) } // Test that it's usable with errors.Is if !errors.Is(ErrVersionConflict, ErrVersionConflict) { t.Error("ErrVersionConflict should match itself with errors.Is") } }