Files
aether/event_test.go
Claude Code 46e1c44017
Some checks failed
CI / build (pull_request) Successful in 21s
CI / integration (pull_request) Failing after 1m59s
CI / build (push) Successful in 21s
CI / integration (push) Has been cancelled
test(event): Add comprehensive VersionConflictError tests and retry pattern examples
Implement comprehensive tests for VersionConflictError in event_test.go covering:
- Error message formatting with all context fields
- Field accessibility (ActorID, AttemptedVersion, CurrentVersion)
- Unwrap method for error wrapping
- errors.Is sentinel checking
- errors.As type assertion
- Application's ability to read CurrentVersion for retry strategies
- Edge cases including special characters and large version numbers

Add examples/ directory with standard retry patterns:
- SimpleRetryPattern: Basic retry with exponential backoff
- ConflictDetailedRetryPattern: Intelligent retry with conflict analysis
- JitterRetryPattern: Prevent thundering herd with randomized backoff
- AdaptiveRetryPattern: Adjust backoff based on contention level
- EventualConsistencyPattern: Asynchronous retry via queue
- CircuitBreakerPattern: Prevent cascading failures

Includes comprehensive documentation in examples/README.md explaining each
pattern's use cases, performance characteristics, and implementation guidance.

Closes #62

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 21:46:21 +01:00

1527 lines
39 KiB
Go

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": "<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)
}
})
}
}
// 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")
}
}