Add comprehensive unit tests for Event and ActorSnapshot types
All checks were successful
CI / build (pull_request) Successful in 45s
CI / build (push) Successful in 16s

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 <noreply@anthropic.com>
This commit was merged in pull request #29.
This commit is contained in:
2026-01-09 16:44:51 +01:00
parent 1ce5b3ab77
commit c2a78c4915

742
event_test.go Normal file
View File

@@ -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": "<script>alert('xss')</script>",
"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)
}
})
}
}