From c2a78c4915f0619d1f9a7de5025d5aaaa020d8cd Mon Sep 17 00:00:00 2001 From: Hugo Nijhuis Date: Fri, 9 Jan 2026 16:44:51 +0100 Subject: [PATCH] Add comprehensive unit tests for Event and ActorSnapshot types Test JSON serialization/deserialization, field names, omitempty behavior, edge cases (empty/nil data, large payloads, special characters including unicode and control chars), timestamp handling across timezones, nanosecond precision, version edge cases, and nested data structures. Closes #1 Co-Authored-By: Claude Opus 4.5 --- event_test.go | 742 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 742 insertions(+) create mode 100644 event_test.go diff --git a/event_test.go b/event_test.go new file mode 100644 index 0000000..0aee98c --- /dev/null +++ b/event_test.go @@ -0,0 +1,742 @@ +package aether + +import ( + "encoding/json" + "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) + } + }) + } +}