Add ReplayError and ReplayResult types to capture information about malformed events encountered during replay. This allows callers to inspect and handle corrupted data rather than having it silently skipped. Key changes: - Add ReplayError type with sequence number, raw data, and underlying error - Add ReplayResult type containing both successfully parsed events and errors - Add EventStoreWithErrors interface for stores that can report replay errors - Implement GetEventsWithErrors on JetStreamEventStore - Update GetEvents to maintain backward compatibility (still skips malformed) - Add comprehensive unit tests for the new types This addresses the issue of silent data loss during event-sourced replay by giving callers visibility into data quality issues. Closes #39 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1338 lines
34 KiB
Go
1338 lines
34 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
}
|