From 97ff1c3346217a20a2c8253ae5fecb5bcbd3865b Mon Sep 17 00:00:00 2001 From: Hugo Nijhuis Date: Fri, 9 Jan 2026 17:53:05 +0100 Subject: [PATCH] Add event metadata support for distributed tracing and auditing - Add Metadata field (map[string]string) to Event struct with omitempty - Add helper methods for common metadata: SetCorrelationID/GetCorrelationID, SetCausationID/GetCausationID, SetUserID/GetUserID, SetTraceID/GetTraceID, SetSpanID/GetSpanID - Add WithMetadataFrom helper for copying metadata between events - Add metadata key constants for standard fields - Add comprehensive unit tests for metadata serialization and helpers - Add store tests verifying metadata persistence Closes #7 Co-Authored-By: Claude Opus 4.5 --- event.go | 94 +++++++++ event_test.go | 468 +++++++++++++++++++++++++++++++++++++++++++ store/memory_test.go | 219 ++++++++++++++++++++ 3 files changed, 781 insertions(+) diff --git a/event.go b/event.go index c7980da..6fe2bdb 100644 --- a/event.go +++ b/event.go @@ -12,9 +12,103 @@ type Event struct { CommandID string `json:"commandId,omitempty"` // Correlation ID for command that triggered this event Version int64 `json:"version"` Data map[string]interface{} `json:"data"` + Metadata map[string]string `json:"metadata,omitempty"` // Optional metadata for tracing and auditing Timestamp time.Time `json:"timestamp"` } +// Common metadata keys for distributed tracing and auditing +const ( + // MetadataKeyCorrelationID identifies related events across services + MetadataKeyCorrelationID = "correlationId" + // MetadataKeyCausationID identifies the event that caused this event + MetadataKeyCausationID = "causationId" + // MetadataKeyUserID identifies the user who triggered this event + MetadataKeyUserID = "userId" + // MetadataKeyTraceID for distributed tracing integration (e.g., OpenTelemetry) + MetadataKeyTraceID = "traceId" + // MetadataKeySpanID for distributed tracing integration + MetadataKeySpanID = "spanId" +) + +// SetMetadata sets a metadata key-value pair, initializing the map if needed +func (e *Event) SetMetadata(key, value string) { + if e.Metadata == nil { + e.Metadata = make(map[string]string) + } + e.Metadata[key] = value +} + +// GetMetadata returns the value for a metadata key, or empty string if not found +func (e *Event) GetMetadata(key string) string { + if e.Metadata == nil { + return "" + } + return e.Metadata[key] +} + +// SetCorrelationID sets the correlation ID metadata +func (e *Event) SetCorrelationID(correlationID string) { + e.SetMetadata(MetadataKeyCorrelationID, correlationID) +} + +// GetCorrelationID returns the correlation ID metadata +func (e *Event) GetCorrelationID() string { + return e.GetMetadata(MetadataKeyCorrelationID) +} + +// SetCausationID sets the causation ID metadata +func (e *Event) SetCausationID(causationID string) { + e.SetMetadata(MetadataKeyCausationID, causationID) +} + +// GetCausationID returns the causation ID metadata +func (e *Event) GetCausationID() string { + return e.GetMetadata(MetadataKeyCausationID) +} + +// SetUserID sets the user ID metadata +func (e *Event) SetUserID(userID string) { + e.SetMetadata(MetadataKeyUserID, userID) +} + +// GetUserID returns the user ID metadata +func (e *Event) GetUserID() string { + return e.GetMetadata(MetadataKeyUserID) +} + +// SetTraceID sets the trace ID metadata for distributed tracing +func (e *Event) SetTraceID(traceID string) { + e.SetMetadata(MetadataKeyTraceID, traceID) +} + +// GetTraceID returns the trace ID metadata +func (e *Event) GetTraceID() string { + return e.GetMetadata(MetadataKeyTraceID) +} + +// SetSpanID sets the span ID metadata for distributed tracing +func (e *Event) SetSpanID(spanID string) { + e.SetMetadata(MetadataKeySpanID, spanID) +} + +// GetSpanID returns the span ID metadata +func (e *Event) GetSpanID() string { + return e.GetMetadata(MetadataKeySpanID) +} + +// WithMetadataFrom copies metadata from another event (useful for event chaining) +func (e *Event) WithMetadataFrom(source *Event) { + if source == nil || source.Metadata == nil { + return + } + if e.Metadata == nil { + e.Metadata = make(map[string]string) + } + for k, v := range source.Metadata { + e.Metadata[k] = v + } +} + // ActorSnapshot represents a point-in-time state snapshot type ActorSnapshot struct { ActorID string `json:"actorId"` diff --git a/event_test.go b/event_test.go index 0aee98c..777c7a9 100644 --- a/event_test.go +++ b/event_test.go @@ -740,3 +740,471 @@ func TestActorSnapshot_VersionEdgeCases(t *testing.T) { }) } } + +// 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()) + } +} diff --git a/store/memory_test.go b/store/memory_test.go index 20b7639..706e6c3 100644 --- a/store/memory_test.go +++ b/store/memory_test.go @@ -1488,3 +1488,222 @@ func TestConcurrentSaveAndGetSnapshot(t *testing.T) { t.Errorf("expected latest version %d, got %d", expectedMaxVersion, snapshot.Version) } } + +// === Event Metadata Persistence Tests === + +func TestSaveEvent_WithMetadata(t *testing.T) { + store := NewInMemoryEventStore() + + event := &aether.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", + "traceId": "trace-abc", + "spanId": "span-def", + }, + Timestamp: time.Now(), + } + + err := store.SaveEvent(event) + if err != nil { + t.Fatalf("SaveEvent failed: %v", err) + } + + // Retrieve and verify metadata is persisted + events, err := store.GetEvents("order-456", 0) + if err != nil { + t.Fatalf("GetEvents failed: %v", err) + } + if len(events) != 1 { + t.Fatalf("expected 1 event, got %d", len(events)) + } + + retrieved := events[0] + if retrieved.Metadata == nil { + t.Fatal("expected Metadata to be persisted") + } + if retrieved.Metadata["correlationId"] != "corr-123" { + t.Errorf("correlationId mismatch: got %q, want %q", retrieved.Metadata["correlationId"], "corr-123") + } + if retrieved.Metadata["causationId"] != "cause-456" { + t.Errorf("causationId mismatch: got %q, want %q", retrieved.Metadata["causationId"], "cause-456") + } + if retrieved.Metadata["userId"] != "user-789" { + t.Errorf("userId mismatch: got %q, want %q", retrieved.Metadata["userId"], "user-789") + } + if retrieved.Metadata["traceId"] != "trace-abc" { + t.Errorf("traceId mismatch: got %q, want %q", retrieved.Metadata["traceId"], "trace-abc") + } + if retrieved.Metadata["spanId"] != "span-def" { + t.Errorf("spanId mismatch: got %q, want %q", retrieved.Metadata["spanId"], "span-def") + } +} + +func TestSaveEvent_WithNilMetadata(t *testing.T) { + store := NewInMemoryEventStore() + + event := &aether.Event{ + ID: "evt-nil-meta", + EventType: "OrderPlaced", + ActorID: "order-456", + Version: 1, + Data: map[string]interface{}{}, + Metadata: nil, + Timestamp: time.Now(), + } + + err := store.SaveEvent(event) + if err != nil { + t.Fatalf("SaveEvent failed: %v", err) + } + + events, err := store.GetEvents("order-456", 0) + if err != nil { + t.Fatalf("GetEvents failed: %v", err) + } + if len(events) != 1 { + t.Fatalf("expected 1 event, got %d", len(events)) + } + + // Nil metadata should remain nil + if events[0].Metadata != nil { + t.Errorf("expected nil Metadata, got %v", events[0].Metadata) + } +} + +func TestSaveEvent_WithEmptyMetadata(t *testing.T) { + store := NewInMemoryEventStore() + + event := &aether.Event{ + ID: "evt-empty-meta", + EventType: "OrderPlaced", + ActorID: "order-456", + Version: 1, + Data: map[string]interface{}{}, + Metadata: map[string]string{}, + Timestamp: time.Now(), + } + + err := store.SaveEvent(event) + if err != nil { + t.Fatalf("SaveEvent failed: %v", err) + } + + events, err := store.GetEvents("order-456", 0) + if err != nil { + t.Fatalf("GetEvents failed: %v", err) + } + if len(events) != 1 { + t.Fatalf("expected 1 event, got %d", len(events)) + } + + // Empty metadata should be preserved (as empty map) + if events[0].Metadata == nil { + t.Error("expected empty Metadata map to be preserved, got nil") + } + if len(events[0].Metadata) != 0 { + t.Errorf("expected empty Metadata, got %d entries", len(events[0].Metadata)) + } +} + +func TestSaveEvent_MetadataWithHelpers(t *testing.T) { + store := NewInMemoryEventStore() + + event := &aether.Event{ + ID: "evt-helpers", + EventType: "OrderPlaced", + ActorID: "order-456", + Version: 1, + Data: map[string]interface{}{}, + Timestamp: time.Now(), + } + + // Use helper methods to set metadata + event.SetCorrelationID("corr-helper") + event.SetCausationID("cause-helper") + event.SetUserID("user-helper") + event.SetTraceID("trace-helper") + event.SetSpanID("span-helper") + + err := store.SaveEvent(event) + if err != nil { + t.Fatalf("SaveEvent failed: %v", err) + } + + events, err := store.GetEvents("order-456", 0) + if err != nil { + t.Fatalf("GetEvents failed: %v", err) + } + if len(events) != 1 { + t.Fatalf("expected 1 event, got %d", len(events)) + } + + retrieved := events[0] + if retrieved.GetCorrelationID() != "corr-helper" { + t.Errorf("GetCorrelationID mismatch: got %q", retrieved.GetCorrelationID()) + } + if retrieved.GetCausationID() != "cause-helper" { + t.Errorf("GetCausationID mismatch: got %q", retrieved.GetCausationID()) + } + if retrieved.GetUserID() != "user-helper" { + t.Errorf("GetUserID mismatch: got %q", retrieved.GetUserID()) + } + if retrieved.GetTraceID() != "trace-helper" { + t.Errorf("GetTraceID mismatch: got %q", retrieved.GetTraceID()) + } + if retrieved.GetSpanID() != "span-helper" { + t.Errorf("GetSpanID mismatch: got %q", retrieved.GetSpanID()) + } +} + +func TestSaveEvent_MetadataPreservedAcrossMultipleEvents(t *testing.T) { + store := NewInMemoryEventStore() + + // Save multiple events with different metadata + for i := 1; i <= 3; i++ { + event := &aether.Event{ + ID: fmt.Sprintf("evt-%d", i), + EventType: "OrderUpdated", + ActorID: "order-456", + Version: int64(i), + Data: map[string]interface{}{}, + Metadata: map[string]string{ + "correlationId": fmt.Sprintf("corr-%d", i), + "eventIndex": fmt.Sprintf("%d", i), + }, + Timestamp: time.Now(), + } + if err := store.SaveEvent(event); err != nil { + t.Fatalf("SaveEvent failed for event %d: %v", i, err) + } + } + + events, err := store.GetEvents("order-456", 0) + if err != nil { + t.Fatalf("GetEvents failed: %v", err) + } + if len(events) != 3 { + t.Fatalf("expected 3 events, got %d", len(events)) + } + + // Verify each event has its own metadata + for i, event := range events { + expectedCorr := fmt.Sprintf("corr-%d", i+1) + expectedIdx := fmt.Sprintf("%d", i+1) + + if event.Metadata["correlationId"] != expectedCorr { + t.Errorf("event %d correlationId mismatch: got %q, want %q", i+1, event.Metadata["correlationId"], expectedCorr) + } + if event.Metadata["eventIndex"] != expectedIdx { + t.Errorf("event %d eventIndex mismatch: got %q, want %q", i+1, event.Metadata["eventIndex"], expectedIdx) + } + } +}