Files
aether/store/memory_test.go
Claude Code 8c5ac500b6
Some checks failed
CI / build (pull_request) Successful in 22s
CI / integration (pull_request) Failing after 2m1s
feat(event-sourcing): Publish EventStored after successful SaveEvent
Add EventStored internal event published to the EventBus when events are
successfully persisted. This allows observability components (metrics,
projections, audit systems) to react to persisted events without coupling
to application code.

Implementation:
- Add EventTypeEventStored constant to define the event type
- Update InMemoryEventStore with optional EventBroadcaster support
- Add NewInMemoryEventStoreWithBroadcaster constructor
- Update JetStreamEventStore with EventBroadcaster support
- Add NewJetStreamEventStoreWithBroadcaster constructor
- Implement publishEventStored() helper method
- Publish EventStored containing EventID, ActorID, Version, Timestamp
- Only publish on successful SaveEvent (not on version conflicts)
- Automatically recorded in metrics through normal Publish flow

Test coverage:
- EventStored published after successful SaveEvent
- No EventStored published on version conflict
- Multiple EventStored events published in order
- SaveEvent works correctly without broadcaster (nil-safe)

Closes #61

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-01-13 21:45:11 +01:00

2086 lines
53 KiB
Go

package store
import (
"errors"
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"git.flowmade.one/flowmade-one/aether"
)
// === Event Store Tests ===
func TestNewInMemoryEventStore(t *testing.T) {
store := NewInMemoryEventStore()
if store == nil {
t.Fatal("NewInMemoryEventStore returned nil")
}
if store.events == nil {
t.Error("events map is nil")
}
}
func TestSaveEvent_SingleEvent(t *testing.T) {
store := NewInMemoryEventStore()
event := &aether.Event{
ID: "evt-123",
EventType: "OrderPlaced",
ActorID: "order-456",
Version: 1,
Data: map[string]interface{}{
"total": 100.50,
},
Timestamp: time.Now(),
}
err := store.SaveEvent(event)
if err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
// Verify event was 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))
}
if events[0].ID != "evt-123" {
t.Errorf("event ID mismatch: got %q, want %q", events[0].ID, "evt-123")
}
}
func TestSaveEvent_MultipleEvents(t *testing.T) {
store := NewInMemoryEventStore()
for i := 1; i <= 5; i++ {
event := &aether.Event{
ID: fmt.Sprintf("evt-%d", i),
EventType: "OrderUpdated",
ActorID: "order-456",
Version: int64(i),
Data: map[string]interface{}{},
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) != 5 {
t.Errorf("expected 5 events, got %d", len(events))
}
}
func TestSaveEvent_MultipleActors(t *testing.T) {
store := NewInMemoryEventStore()
// Save events for different actors
actors := []string{"actor-1", "actor-2", "actor-3"}
for _, actorID := range actors {
for i := 1; i <= 3; i++ {
event := &aether.Event{
ID: fmt.Sprintf("evt-%s-%d", actorID, i),
EventType: "TestEvent",
ActorID: actorID,
Version: int64(i),
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
}
}
// Verify each actor has its own events
for _, actorID := range actors {
events, err := store.GetEvents(actorID, 0)
if err != nil {
t.Fatalf("GetEvents failed for %s: %v", actorID, err)
}
if len(events) != 3 {
t.Errorf("expected 3 events for %s, got %d", actorID, len(events))
}
for _, event := range events {
if event.ActorID != actorID {
t.Errorf("event has wrong ActorID: got %q, want %q", event.ActorID, actorID)
}
}
}
}
func TestSaveEvent_PreservesAllFields(t *testing.T) {
store := NewInMemoryEventStore()
ts := time.Date(2026, 1, 9, 12, 0, 0, 0, time.UTC)
event := &aether.Event{
ID: "evt-123",
EventType: "OrderPlaced",
ActorID: "order-456",
CommandID: "cmd-789",
Version: 42,
Data: map[string]interface{}{
"total": 100.50,
"currency": "USD",
},
Timestamp: ts,
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
events, err := store.GetEvents("order-456", 0)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
retrieved := events[0]
if retrieved.ID != event.ID {
t.Errorf("ID mismatch: got %q, want %q", retrieved.ID, event.ID)
}
if retrieved.EventType != event.EventType {
t.Errorf("EventType mismatch: got %q, want %q", retrieved.EventType, event.EventType)
}
if retrieved.ActorID != event.ActorID {
t.Errorf("ActorID mismatch: got %q, want %q", retrieved.ActorID, event.ActorID)
}
if retrieved.CommandID != event.CommandID {
t.Errorf("CommandID mismatch: got %q, want %q", retrieved.CommandID, event.CommandID)
}
if retrieved.Version != event.Version {
t.Errorf("Version mismatch: got %d, want %d", retrieved.Version, event.Version)
}
if !retrieved.Timestamp.Equal(event.Timestamp) {
t.Errorf("Timestamp mismatch: got %v, want %v", retrieved.Timestamp, event.Timestamp)
}
if retrieved.Data["total"] != event.Data["total"] {
t.Errorf("Data.total mismatch: got %v, want %v", retrieved.Data["total"], event.Data["total"])
}
if retrieved.Data["currency"] != event.Data["currency"] {
t.Errorf("Data.currency mismatch: got %v, want %v", retrieved.Data["currency"], event.Data["currency"])
}
}
func TestGetEvents_RetrievesInOrder(t *testing.T) {
store := NewInMemoryEventStore()
// Save events in order
for i := 1; i <= 10; i++ {
event := &aether.Event{
ID: fmt.Sprintf("evt-%d", i),
EventType: "TestEvent",
ActorID: "actor-123",
Version: int64(i),
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
}
events, err := store.GetEvents("actor-123", 0)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
// Verify events are returned in insertion order
for i, event := range events {
expectedID := fmt.Sprintf("evt-%d", i+1)
if event.ID != expectedID {
t.Errorf("event %d: got ID %q, want %q", i, event.ID, expectedID)
}
expectedVersion := int64(i + 1)
if event.Version != expectedVersion {
t.Errorf("event %d: got Version %d, want %d", i, event.Version, expectedVersion)
}
}
}
func TestGetEvents_FromVersionFilters(t *testing.T) {
store := NewInMemoryEventStore()
// Save events with versions 1-10
for i := 1; i <= 10; i++ {
event := &aether.Event{
ID: fmt.Sprintf("evt-%d", i),
EventType: "TestEvent",
ActorID: "actor-123",
Version: int64(i),
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
}
testCases := []struct {
name string
fromVersion int64
expected int
minVersion int64
}{
{"from version 0", 0, 10, 1},
{"from version 1", 1, 10, 1},
{"from version 5", 5, 6, 5},
{"from version 10", 10, 1, 10},
{"from version 11", 11, 0, 0},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
events, err := store.GetEvents("actor-123", tc.fromVersion)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
if len(events) != tc.expected {
t.Errorf("expected %d events, got %d", tc.expected, len(events))
}
// Verify all returned events have version >= fromVersion
for _, event := range events {
if event.Version < tc.fromVersion {
t.Errorf("event version %d is less than fromVersion %d", event.Version, tc.fromVersion)
}
}
// Verify minimum version if events exist
if len(events) > 0 && events[0].Version != tc.minVersion {
t.Errorf("first event version: got %d, want %d", events[0].Version, tc.minVersion)
}
})
}
}
func TestGetEvents_FromVersionZero(t *testing.T) {
store := NewInMemoryEventStore()
event := &aether.Event{
ID: "evt-1",
EventType: "TestEvent",
ActorID: "actor-123",
Version: 1,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
// fromVersion 0 should return all events
events, err := store.GetEvents("actor-123", 0)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
if len(events) != 1 {
t.Errorf("expected 1 event with fromVersion 0, got %d", len(events))
}
}
func TestGetLatestVersion_ReturnsCorrectVersion(t *testing.T) {
store := NewInMemoryEventStore()
// Save events with strictly increasing versions
versions := []int64{1, 2, 3, 4, 5}
for i, version := range versions {
event := &aether.Event{
ID: fmt.Sprintf("evt-%d", i),
EventType: "TestEvent",
ActorID: "actor-123",
Version: version,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
}
latestVersion, err := store.GetLatestVersion("actor-123")
if err != nil {
t.Fatalf("GetLatestVersion failed: %v", err)
}
// Should return the highest version (5)
if latestVersion != 5 {
t.Errorf("expected latest version 5, got %d", latestVersion)
}
}
func TestGetLatestVersion_SingleEvent(t *testing.T) {
store := NewInMemoryEventStore()
event := &aether.Event{
ID: "evt-1",
EventType: "TestEvent",
ActorID: "actor-123",
Version: 42,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
latestVersion, err := store.GetLatestVersion("actor-123")
if err != nil {
t.Fatalf("GetLatestVersion failed: %v", err)
}
if latestVersion != 42 {
t.Errorf("expected latest version 42, got %d", latestVersion)
}
}
func TestGetLatestVersion_UpdatesAfterNewEvent(t *testing.T) {
store := NewInMemoryEventStore()
// Save first event
event1 := &aether.Event{
ID: "evt-1",
EventType: "TestEvent",
ActorID: "actor-123",
Version: 1,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event1); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
version1, err := store.GetLatestVersion("actor-123")
if err != nil {
t.Fatalf("GetLatestVersion failed: %v", err)
}
if version1 != 1 {
t.Errorf("expected version 1, got %d", version1)
}
// Save second event with higher version
event2 := &aether.Event{
ID: "evt-2",
EventType: "TestEvent",
ActorID: "actor-123",
Version: 10,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event2); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
version2, err := store.GetLatestVersion("actor-123")
if err != nil {
t.Fatalf("GetLatestVersion failed: %v", err)
}
if version2 != 10 {
t.Errorf("expected version 10, got %d", version2)
}
}
func TestGetEvents_NonExistentActor(t *testing.T) {
store := NewInMemoryEventStore()
// Save event for one actor
event := &aether.Event{
ID: "evt-1",
EventType: "TestEvent",
ActorID: "actor-123",
Version: 1,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
// Get events for non-existent actor
events, err := store.GetEvents("non-existent-actor", 0)
if err != nil {
t.Fatalf("GetEvents should not error for non-existent actor: %v", err)
}
if events == nil {
t.Error("GetEvents should return non-nil slice for non-existent actor")
}
if len(events) != 0 {
t.Errorf("expected 0 events for non-existent actor, got %d", len(events))
}
}
func TestGetLatestVersion_NonExistentActor(t *testing.T) {
store := NewInMemoryEventStore()
// Get latest version for non-existent actor
version, err := store.GetLatestVersion("non-existent-actor")
if err != nil {
t.Fatalf("GetLatestVersion should not error for non-existent actor: %v", err)
}
if version != 0 {
t.Errorf("expected version 0 for non-existent actor, got %d", version)
}
}
func TestGetLatestVersion_EmptyStore(t *testing.T) {
store := NewInMemoryEventStore()
version, err := store.GetLatestVersion("any-actor")
if err != nil {
t.Fatalf("GetLatestVersion should not error for empty store: %v", err)
}
if version != 0 {
t.Errorf("expected version 0 for empty store, got %d", version)
}
}
func TestGetEvents_EmptyStore(t *testing.T) {
store := NewInMemoryEventStore()
events, err := store.GetEvents("any-actor", 0)
if err != nil {
t.Fatalf("GetEvents should not error for empty store: %v", err)
}
if events == nil {
t.Error("GetEvents should return non-nil slice for empty store")
}
if len(events) != 0 {
t.Errorf("expected 0 events for empty store, got %d", len(events))
}
}
func TestConcurrentSaveEvent(t *testing.T) {
store := NewInMemoryEventStore()
numGoroutines := 100
eventsPerGoroutine := 10
var wg sync.WaitGroup
wg.Add(numGoroutines)
for g := 0; g < numGoroutines; g++ {
go func(goroutineID int) {
defer wg.Done()
for i := 0; i < eventsPerGoroutine; i++ {
event := &aether.Event{
ID: fmt.Sprintf("evt-%d-%d", goroutineID, i),
EventType: "TestEvent",
ActorID: fmt.Sprintf("actor-%d", goroutineID),
Version: int64(i + 1),
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Errorf("SaveEvent failed: %v", err)
}
}
}(g)
}
wg.Wait()
// Verify each actor has the correct number of events
for g := 0; g < numGoroutines; g++ {
actorID := fmt.Sprintf("actor-%d", g)
events, err := store.GetEvents(actorID, 0)
if err != nil {
t.Errorf("GetEvents failed for %s: %v", actorID, err)
continue
}
if len(events) != eventsPerGoroutine {
t.Errorf("expected %d events for %s, got %d", eventsPerGoroutine, actorID, len(events))
}
}
}
func TestConcurrentSaveAndGet(t *testing.T) {
store := NewInMemoryEventStore()
// Pre-populate with some events
for i := 1; i <= 10; i++ {
event := &aether.Event{
ID: fmt.Sprintf("evt-%d", i),
EventType: "TestEvent",
ActorID: "actor-123",
Version: int64(i),
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
}
var wg sync.WaitGroup
numReaders := 50
numWriters := 10
readsPerReader := 100
writesPerWriter := 10
// Start readers
wg.Add(numReaders)
for r := 0; r < numReaders; r++ {
go func() {
defer wg.Done()
for i := 0; i < readsPerReader; i++ {
events, err := store.GetEvents("actor-123", 0)
if err != nil {
t.Errorf("GetEvents failed: %v", err)
}
if len(events) < 10 {
t.Errorf("expected at least 10 events, got %d", len(events))
}
_, err = store.GetLatestVersion("actor-123")
if err != nil {
t.Errorf("GetLatestVersion failed: %v", err)
}
}
}()
}
// Start writers - each writer gets their own actor to avoid version conflicts
wg.Add(numWriters)
for w := 0; w < numWriters; w++ {
go func(writerID int) {
defer wg.Done()
actorID := fmt.Sprintf("writer-actor-%d", writerID)
for i := 0; i < writesPerWriter; i++ {
event := &aether.Event{
ID: fmt.Sprintf("evt-writer-%d-%d", writerID, i),
EventType: "TestEvent",
ActorID: actorID,
Version: int64(i + 1),
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Errorf("SaveEvent failed: %v", err)
}
}
}(w)
}
wg.Wait()
// Verify actor-123 still has its original events
events, err := store.GetEvents("actor-123", 0)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
if len(events) != 10 {
t.Errorf("expected 10 events for actor-123, got %d", len(events))
}
// Verify each writer's actor has the correct events
for w := 0; w < numWriters; w++ {
actorID := fmt.Sprintf("writer-actor-%d", w)
events, err := store.GetEvents(actorID, 0)
if err != nil {
t.Errorf("GetEvents failed for %s: %v", actorID, err)
continue
}
if len(events) != writesPerWriter {
t.Errorf("expected %d events for %s, got %d", writesPerWriter, actorID, len(events))
}
}
}
func TestConcurrentGetLatestVersion(t *testing.T) {
store := NewInMemoryEventStore()
// Save initial event
event := &aether.Event{
ID: "evt-1",
EventType: "TestEvent",
ActorID: "actor-123",
Version: 100,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
var wg sync.WaitGroup
numGoroutines := 100
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
version, err := store.GetLatestVersion("actor-123")
if err != nil {
t.Errorf("GetLatestVersion failed: %v", err)
}
if version < 100 {
t.Errorf("expected version >= 100, got %d", version)
}
}
}()
}
wg.Wait()
}
func TestEventStoreInterface(t *testing.T) {
// Verify InMemoryEventStore implements EventStore interface
var _ aether.EventStore = (*InMemoryEventStore)(nil)
}
func TestSaveEvent_NilData(t *testing.T) {
store := NewInMemoryEventStore()
event := &aether.Event{
ID: "evt-nil",
EventType: "NilDataEvent",
ActorID: "actor-123",
Version: 1,
Data: nil,
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed with nil data: %v", err)
}
events, err := store.GetEvents("actor-123", 0)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
if events[0].Data != nil {
t.Errorf("expected nil Data, got %v", events[0].Data)
}
}
func TestSaveEvent_EmptyData(t *testing.T) {
store := NewInMemoryEventStore()
event := &aether.Event{
ID: "evt-empty",
EventType: "EmptyDataEvent",
ActorID: "actor-123",
Version: 1,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed with empty data: %v", err)
}
events, err := store.GetEvents("actor-123", 0)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
if len(events[0].Data) != 0 {
t.Errorf("expected empty Data map, got %v", events[0].Data)
}
}
func TestGetEvents_VersionEdgeCases(t *testing.T) {
store := NewInMemoryEventStore()
// Save events with edge case versions (strictly increasing)
versions := []int64{1, 100, 9223372036854775807} // one, 100, MaxInt64
for i, version := range versions {
event := &aether.Event{
ID: fmt.Sprintf("evt-%d", i),
EventType: "TestEvent",
ActorID: "actor-123",
Version: version,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed for version %d: %v", version, err)
}
}
// Test fromVersion 0 returns all
events, err := store.GetEvents("actor-123", 0)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
if len(events) != 3 {
t.Errorf("expected 3 events, got %d", len(events))
}
// Test fromVersion MaxInt64 returns only that event
events, err = store.GetEvents("actor-123", 9223372036854775807)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
if len(events) != 1 {
t.Errorf("expected 1 event, got %d", len(events))
}
}
func TestGetLatestVersion_VersionEdgeCases(t *testing.T) {
store := NewInMemoryEventStore()
// Save event with MaxInt64 version
event := &aether.Event{
ID: "evt-max",
EventType: "TestEvent",
ActorID: "actor-123",
Version: 9223372036854775807,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
latestVersion, err := store.GetLatestVersion("actor-123")
if err != nil {
t.Fatalf("GetLatestVersion failed: %v", err)
}
if latestVersion != 9223372036854775807 {
t.Errorf("expected MaxInt64 version, got %d", latestVersion)
}
}
func TestSaveEvent_SpecialActorIDs(t *testing.T) {
store := NewInMemoryEventStore()
specialIDs := []string{
"simple",
"with-dashes",
"with_underscores",
"with.dots",
"with:colons",
"with/slashes",
"user@example.com",
"unicode-世界",
"",
}
for _, actorID := range specialIDs {
event := &aether.Event{
ID: fmt.Sprintf("evt-%s", actorID),
EventType: "TestEvent",
ActorID: actorID,
Version: 1,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Errorf("SaveEvent failed for actorID %q: %v", actorID, err)
continue
}
events, err := store.GetEvents(actorID, 0)
if err != nil {
t.Errorf("GetEvents failed for actorID %q: %v", actorID, err)
continue
}
if len(events) != 1 {
t.Errorf("expected 1 event for actorID %q, got %d", actorID, len(events))
}
}
}
// === Version Validation Tests ===
func TestSaveEvent_RejectsLowerVersion(t *testing.T) {
store := NewInMemoryEventStore()
// Save first event with version 5
event1 := &aether.Event{
ID: "evt-1",
EventType: "TestEvent",
ActorID: "actor-123",
Version: 5,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event1); err != nil {
t.Fatalf("SaveEvent failed for first event: %v", err)
}
// Attempt to save event with lower version (should fail)
event2 := &aether.Event{
ID: "evt-2",
EventType: "TestEvent",
ActorID: "actor-123",
Version: 3,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
err := store.SaveEvent(event2)
if err == nil {
t.Fatal("expected error when saving event with lower version, got nil")
}
// Verify it's a VersionConflictError
if !errors.Is(err, aether.ErrVersionConflict) {
t.Errorf("expected ErrVersionConflict, got %v", err)
}
var versionErr *aether.VersionConflictError
if !errors.As(err, &versionErr) {
t.Fatalf("expected VersionConflictError, got %T", err)
}
if versionErr.ActorID != "actor-123" {
t.Errorf("ActorID mismatch: got %q, want %q", versionErr.ActorID, "actor-123")
}
if versionErr.CurrentVersion != 5 {
t.Errorf("CurrentVersion mismatch: got %d, want %d", versionErr.CurrentVersion, 5)
}
if versionErr.AttemptedVersion != 3 {
t.Errorf("AttemptedVersion mismatch: got %d, want %d", versionErr.AttemptedVersion, 3)
}
}
func TestSaveEvent_RejectsEqualVersion(t *testing.T) {
store := NewInMemoryEventStore()
// Save first event with version 5
event1 := &aether.Event{
ID: "evt-1",
EventType: "TestEvent",
ActorID: "actor-123",
Version: 5,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event1); err != nil {
t.Fatalf("SaveEvent failed for first event: %v", err)
}
// Attempt to save event with equal version (should fail)
event2 := &aether.Event{
ID: "evt-2",
EventType: "TestEvent",
ActorID: "actor-123",
Version: 5,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
err := store.SaveEvent(event2)
if err == nil {
t.Fatal("expected error when saving event with equal version, got nil")
}
if !errors.Is(err, aether.ErrVersionConflict) {
t.Errorf("expected ErrVersionConflict, got %v", err)
}
}
func TestSaveEvent_RejectsZeroVersion(t *testing.T) {
store := NewInMemoryEventStore()
// Version 0 should be rejected (not strictly greater than initial 0)
event := &aether.Event{
ID: "evt-1",
EventType: "TestEvent",
ActorID: "actor-new",
Version: 0,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
err := store.SaveEvent(event)
if err == nil {
t.Fatal("expected error when saving event with version 0, got nil")
}
if !errors.Is(err, aether.ErrVersionConflict) {
t.Errorf("expected ErrVersionConflict, got %v", err)
}
}
func TestSaveEvent_RejectsNegativeVersion(t *testing.T) {
store := NewInMemoryEventStore()
event := &aether.Event{
ID: "evt-neg",
EventType: "TestEvent",
ActorID: "actor-123",
Version: -1,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
err := store.SaveEvent(event)
if err == nil {
t.Fatal("expected error when saving event with negative version, got nil")
}
if !errors.Is(err, aether.ErrVersionConflict) {
t.Errorf("expected ErrVersionConflict, got %v", err)
}
}
func TestSaveEvent_ConcurrentWritesToSameActor(t *testing.T) {
store := NewInMemoryEventStore()
numGoroutines := 100
var successCount int64
var conflictCount int64
var wg sync.WaitGroup
// All goroutines try to save version 1
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(id int) {
defer wg.Done()
event := &aether.Event{
ID: fmt.Sprintf("evt-%d", id),
EventType: "TestEvent",
ActorID: "actor-contested",
Version: 1,
Data: map[string]interface{}{"goroutine": id},
Timestamp: time.Now(),
}
err := store.SaveEvent(event)
if err == nil {
atomic.AddInt64(&successCount, 1)
} else if errors.Is(err, aether.ErrVersionConflict) {
atomic.AddInt64(&conflictCount, 1)
} else {
t.Errorf("unexpected error: %v", err)
}
}(i)
}
wg.Wait()
// Exactly one should succeed, rest should conflict
if successCount != 1 {
t.Errorf("expected exactly 1 success, got %d", successCount)
}
if conflictCount != int64(numGoroutines-1) {
t.Errorf("expected %d conflicts, got %d", numGoroutines-1, conflictCount)
}
// Verify only one event was stored
events, err := store.GetEvents("actor-contested", 0)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
if len(events) != 1 {
t.Errorf("expected 1 event, got %d", len(events))
}
}
func BenchmarkSaveEvent(b *testing.B) {
store := NewInMemoryEventStore()
b.ResetTimer()
for i := 0; i < b.N; i++ {
event := &aether.Event{
ID: fmt.Sprintf("evt-%d", i),
EventType: "BenchmarkEvent",
ActorID: "actor-123",
Version: int64(i + 1),
Data: map[string]interface{}{"value": i},
Timestamp: time.Now(),
}
store.SaveEvent(event)
}
}
func BenchmarkGetEvents(b *testing.B) {
store := NewInMemoryEventStore()
// Pre-populate with events
for i := 0; i < 1000; i++ {
event := &aether.Event{
ID: fmt.Sprintf("evt-%d", i),
EventType: "BenchmarkEvent",
ActorID: "actor-123",
Version: int64(i + 1),
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
store.SaveEvent(event)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
store.GetEvents("actor-123", 0)
}
}
func BenchmarkGetLatestVersion(b *testing.B) {
store := NewInMemoryEventStore()
// Pre-populate with events
for i := 0; i < 1000; i++ {
event := &aether.Event{
ID: fmt.Sprintf("evt-%d", i),
EventType: "BenchmarkEvent",
ActorID: "actor-123",
Version: int64(i + 1),
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
store.SaveEvent(event)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
store.GetLatestVersion("actor-123")
}
}
// === Snapshot Store Tests ===
func TestSaveSnapshot_PersistsCorrectly(t *testing.T) {
store := NewInMemoryEventStore()
snapshot := &aether.ActorSnapshot{
ActorID: "actor-123",
Version: 10,
State: map[string]interface{}{
"balance": 100.50,
"status": "active",
},
Timestamp: time.Date(2026, 1, 9, 12, 0, 0, 0, time.UTC),
}
err := store.SaveSnapshot(snapshot)
if err != nil {
t.Fatalf("SaveSnapshot failed: %v", err)
}
// Verify snapshot was persisted by retrieving it
retrieved, err := store.GetLatestSnapshot("actor-123")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
if retrieved == nil {
t.Fatal("expected snapshot to be persisted, got nil")
}
if retrieved.ActorID != snapshot.ActorID {
t.Errorf("ActorID mismatch: got %q, want %q", retrieved.ActorID, snapshot.ActorID)
}
if retrieved.Version != snapshot.Version {
t.Errorf("Version mismatch: got %d, want %d", retrieved.Version, snapshot.Version)
}
if retrieved.State["balance"] != snapshot.State["balance"] {
t.Errorf("State.balance mismatch: got %v, want %v", retrieved.State["balance"], snapshot.State["balance"])
}
if retrieved.State["status"] != snapshot.State["status"] {
t.Errorf("State.status mismatch: got %v, want %v", retrieved.State["status"], snapshot.State["status"])
}
if !retrieved.Timestamp.Equal(snapshot.Timestamp) {
t.Errorf("Timestamp mismatch: got %v, want %v", retrieved.Timestamp, snapshot.Timestamp)
}
}
func TestSaveSnapshot_NilSnapshot(t *testing.T) {
store := NewInMemoryEventStore()
err := store.SaveSnapshot(nil)
if err == nil {
t.Error("expected error when saving nil snapshot, got nil")
}
}
func TestSaveSnapshot_MultipleSnapshots(t *testing.T) {
store := NewInMemoryEventStore()
// Save multiple snapshots for the same actor
for i := 1; i <= 5; i++ {
snapshot := &aether.ActorSnapshot{
ActorID: "actor-multi",
Version: int64(i * 10),
State: map[string]interface{}{
"iteration": i,
},
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Fatalf("SaveSnapshot failed for version %d: %v", i*10, err)
}
}
// Verify all snapshots were saved by checking the latest
retrieved, err := store.GetLatestSnapshot("actor-multi")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
if retrieved.Version != 50 {
t.Errorf("expected latest version 50, got %d", retrieved.Version)
}
}
func TestGetLatestSnapshot_ReturnsMostRecent(t *testing.T) {
store := NewInMemoryEventStore()
// Save snapshots in non-sequential order to test version comparison
versions := []int64{5, 15, 10, 25, 20}
for _, v := range versions {
snapshot := &aether.ActorSnapshot{
ActorID: "actor-latest",
Version: v,
State: map[string]interface{}{
"version": v,
},
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Fatalf("SaveSnapshot failed for version %d: %v", v, err)
}
}
latest, err := store.GetLatestSnapshot("actor-latest")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
if latest == nil {
t.Fatal("expected snapshot, got nil")
}
if latest.Version != 25 {
t.Errorf("expected latest version 25, got %d", latest.Version)
}
// Verify the state matches the snapshot with version 25
if latest.State["version"].(int64) != 25 {
t.Errorf("expected state.version to be 25, got %v", latest.State["version"])
}
}
func TestGetLatestSnapshot_NoSnapshotExists(t *testing.T) {
store := NewInMemoryEventStore()
// Query for a non-existent actor
snapshot, err := store.GetLatestSnapshot("non-existent-actor")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
if snapshot != nil {
t.Errorf("expected nil for non-existent actor, got %+v", snapshot)
}
}
func TestGetLatestSnapshot_EmptyActorID(t *testing.T) {
store := NewInMemoryEventStore()
// Save a snapshot with empty actor ID
snapshot := &aether.ActorSnapshot{
ActorID: "",
Version: 1,
State: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Fatalf("SaveSnapshot failed: %v", err)
}
// Retrieve with empty actor ID
retrieved, err := store.GetLatestSnapshot("")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
if retrieved == nil {
t.Fatal("expected snapshot with empty actorID, got nil")
}
}
func TestSnapshotVersioning_RespectedAcrossActors(t *testing.T) {
store := NewInMemoryEventStore()
// Save snapshots for different actors
actors := []string{"actor-a", "actor-b", "actor-c"}
for i, actorID := range actors {
snapshot := &aether.ActorSnapshot{
ActorID: actorID,
Version: int64((i + 1) * 100), // Different versions per actor
State: map[string]interface{}{
"actor": actorID,
},
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Fatalf("SaveSnapshot failed for %s: %v", actorID, err)
}
}
// Verify each actor has their own snapshot with correct version
for i, actorID := range actors {
snapshot, err := store.GetLatestSnapshot(actorID)
if err != nil {
t.Fatalf("GetLatestSnapshot failed for %s: %v", actorID, err)
}
expectedVersion := int64((i + 1) * 100)
if snapshot.Version != expectedVersion {
t.Errorf("actor %s: expected version %d, got %d", actorID, expectedVersion, snapshot.Version)
}
}
}
func TestSnapshotVersioning_LowerVersionAfterHigher(t *testing.T) {
store := NewInMemoryEventStore()
// Save a high version first
highSnapshot := &aether.ActorSnapshot{
ActorID: "actor-order",
Version: 100,
State: map[string]interface{}{
"marker": "high",
},
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(highSnapshot); err != nil {
t.Fatalf("SaveSnapshot failed: %v", err)
}
// Save a lower version after
lowSnapshot := &aether.ActorSnapshot{
ActorID: "actor-order",
Version: 50,
State: map[string]interface{}{
"marker": "low",
},
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(lowSnapshot); err != nil {
t.Fatalf("SaveSnapshot failed: %v", err)
}
// GetLatestSnapshot should return the higher version (100), not the most recently saved
latest, err := store.GetLatestSnapshot("actor-order")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
if latest.Version != 100 {
t.Errorf("expected version 100, got %d", latest.Version)
}
if latest.State["marker"] != "high" {
t.Errorf("expected marker 'high', got %v", latest.State["marker"])
}
}
func TestSnapshotDataIntegrity_ComplexState(t *testing.T) {
store := NewInMemoryEventStore()
complexState := map[string]interface{}{
"string": "hello",
"integer": 42,
"float": 3.14159,
"boolean": true,
"null": nil,
"array": []interface{}{"a", "b", "c"},
"nested": map[string]interface{}{
"level1": map[string]interface{}{
"level2": "deep value",
},
},
}
snapshot := &aether.ActorSnapshot{
ActorID: "actor-complex",
Version: 1,
State: complexState,
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Fatalf("SaveSnapshot failed: %v", err)
}
retrieved, err := store.GetLatestSnapshot("actor-complex")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
// Verify all fields
if retrieved.State["string"] != "hello" {
t.Errorf("string mismatch: got %v", retrieved.State["string"])
}
if retrieved.State["integer"] != 42 {
t.Errorf("integer mismatch: got %v", retrieved.State["integer"])
}
if retrieved.State["float"] != 3.14159 {
t.Errorf("float mismatch: got %v", retrieved.State["float"])
}
if retrieved.State["boolean"] != true {
t.Errorf("boolean mismatch: got %v", retrieved.State["boolean"])
}
if retrieved.State["null"] != nil {
t.Errorf("null mismatch: got %v", retrieved.State["null"])
}
// Verify array
arr, ok := retrieved.State["array"].([]interface{})
if !ok {
t.Fatal("array is not []interface{}")
}
if len(arr) != 3 || arr[0] != "a" || arr[1] != "b" || arr[2] != "c" {
t.Errorf("array mismatch: got %v", arr)
}
// Verify nested structure
nested, ok := retrieved.State["nested"].(map[string]interface{})
if !ok {
t.Fatal("nested is not map[string]interface{}")
}
level1, ok := nested["level1"].(map[string]interface{})
if !ok {
t.Fatal("level1 is not map[string]interface{}")
}
if level1["level2"] != "deep value" {
t.Errorf("nested value mismatch: got %v", level1["level2"])
}
}
func TestSnapshotDataIntegrity_SpecialCharacters(t *testing.T) {
store := NewInMemoryEventStore()
specialState := map[string]interface{}{
"unicode": "Hello, 世界!",
"emoji": "😀🚀",
"newlines": "line1\nline2\r\nline3",
"tabs": "col1\tcol2",
"quotes": `"double" and 'single'`,
"backslash": `path\to\file`,
}
snapshot := &aether.ActorSnapshot{
ActorID: "actor-special",
Version: 1,
State: specialState,
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Fatalf("SaveSnapshot failed: %v", err)
}
retrieved, err := store.GetLatestSnapshot("actor-special")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
for key, expected := range specialState {
if retrieved.State[key] != expected {
t.Errorf("State[%q] mismatch: got %q, want %q", key, retrieved.State[key], expected)
}
}
}
func TestSnapshotDataIntegrity_EmptyState(t *testing.T) {
store := NewInMemoryEventStore()
snapshot := &aether.ActorSnapshot{
ActorID: "actor-empty",
Version: 1,
State: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Fatalf("SaveSnapshot failed: %v", err)
}
retrieved, err := store.GetLatestSnapshot("actor-empty")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
if len(retrieved.State) != 0 {
t.Errorf("expected empty state, got %v", retrieved.State)
}
}
func TestSnapshotDataIntegrity_NilState(t *testing.T) {
store := NewInMemoryEventStore()
snapshot := &aether.ActorSnapshot{
ActorID: "actor-nil",
Version: 1,
State: nil,
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Fatalf("SaveSnapshot failed: %v", err)
}
retrieved, err := store.GetLatestSnapshot("actor-nil")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
if retrieved.State != nil {
t.Errorf("expected nil state, got %v", retrieved.State)
}
}
func TestSnapshotDataIntegrity_LargeState(t *testing.T) {
store := NewInMemoryEventStore()
// Create a large state with many entries using unique keys
largeState := make(map[string]interface{})
for i := 0; i < 1000; i++ {
largeState[fmt.Sprintf("key-%d", i)] = i
}
snapshot := &aether.ActorSnapshot{
ActorID: "actor-large",
Version: 1,
State: largeState,
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Fatalf("SaveSnapshot failed: %v", err)
}
retrieved, err := store.GetLatestSnapshot("actor-large")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
if len(retrieved.State) != len(largeState) {
t.Errorf("state size mismatch: got %d, want %d", len(retrieved.State), len(largeState))
}
}
func TestSnapshotDataIntegrity_TimestampPreserved(t *testing.T) {
store := NewInMemoryEventStore()
// Test various timestamps
timestamps := []time.Time{
time.Date(2026, 1, 9, 12, 0, 0, 0, time.UTC),
time.Date(2020, 6, 15, 23, 59, 59, 999999999, time.UTC),
time.Time{}, // Zero time
}
for i, ts := range timestamps {
actorID := fmt.Sprintf("actor-ts-%d", i)
snapshot := &aether.ActorSnapshot{
ActorID: actorID,
Version: 1,
State: map[string]interface{}{},
Timestamp: ts,
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Fatalf("SaveSnapshot failed: %v", err)
}
retrieved, err := store.GetLatestSnapshot(actorID)
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
if !retrieved.Timestamp.Equal(ts) {
t.Errorf("timestamp %d mismatch: got %v, want %v", i, retrieved.Timestamp, ts)
}
}
}
func TestSnapshotVersioning_ZeroVersion(t *testing.T) {
store := NewInMemoryEventStore()
snapshot := &aether.ActorSnapshot{
ActorID: "actor-zero-version",
Version: 0,
State: map[string]interface{}{"initial": true},
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Fatalf("SaveSnapshot failed: %v", err)
}
retrieved, err := store.GetLatestSnapshot("actor-zero-version")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
if retrieved.Version != 0 {
t.Errorf("expected version 0, got %d", retrieved.Version)
}
}
func TestSnapshotVersioning_LargeVersion(t *testing.T) {
store := NewInMemoryEventStore()
largeVersion := int64(9223372036854775807) // MaxInt64
snapshot := &aether.ActorSnapshot{
ActorID: "actor-large-version",
Version: largeVersion,
State: map[string]interface{}{"maxed": true},
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Fatalf("SaveSnapshot failed: %v", err)
}
retrieved, err := store.GetLatestSnapshot("actor-large-version")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
if retrieved.Version != largeVersion {
t.Errorf("expected version %d, got %d", largeVersion, retrieved.Version)
}
}
func TestSnapshotStore_ImplementsInterface(t *testing.T) {
// Verify InMemoryEventStore implements SnapshotStore interface
var _ aether.SnapshotStore = (*InMemoryEventStore)(nil)
}
func TestConcurrentSaveSnapshot(t *testing.T) {
store := NewInMemoryEventStore()
numGoroutines := 100
snapshotsPerGoroutine := 10
var wg sync.WaitGroup
wg.Add(numGoroutines)
for g := 0; g < numGoroutines; g++ {
go func(goroutineID int) {
defer wg.Done()
for i := 0; i < snapshotsPerGoroutine; i++ {
snapshot := &aether.ActorSnapshot{
ActorID: fmt.Sprintf("actor-%d", goroutineID),
Version: int64(i + 1),
State: map[string]interface{}{
"goroutine": goroutineID,
"iteration": i,
},
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Errorf("SaveSnapshot failed: %v", err)
}
}
}(g)
}
wg.Wait()
// Verify each actor has snapshots
for g := 0; g < numGoroutines; g++ {
actorID := fmt.Sprintf("actor-%d", g)
snapshot, err := store.GetLatestSnapshot(actorID)
if err != nil {
t.Errorf("GetLatestSnapshot failed for %s: %v", actorID, err)
continue
}
if snapshot == nil {
t.Errorf("expected snapshot for %s, got nil", actorID)
continue
}
if snapshot.Version != int64(snapshotsPerGoroutine) {
t.Errorf("expected latest version %d for %s, got %d", snapshotsPerGoroutine, actorID, snapshot.Version)
}
}
}
func TestConcurrentSaveAndGetSnapshot(t *testing.T) {
store := NewInMemoryEventStore()
// Pre-populate with initial snapshot
initialSnapshot := &aether.ActorSnapshot{
ActorID: "actor-123",
Version: 1,
State: map[string]interface{}{
"initial": true,
},
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(initialSnapshot); err != nil {
t.Fatalf("SaveSnapshot failed: %v", err)
}
var wg sync.WaitGroup
numReaders := 50
numWriters := 10
readsPerReader := 100
writesPerWriter := 10
// Start readers
wg.Add(numReaders)
for r := 0; r < numReaders; r++ {
go func() {
defer wg.Done()
for i := 0; i < readsPerReader; i++ {
snapshot, err := store.GetLatestSnapshot("actor-123")
if err != nil {
t.Errorf("GetLatestSnapshot failed: %v", err)
}
if snapshot == nil {
t.Error("expected snapshot, got nil")
}
}
}()
}
// Start writers
wg.Add(numWriters)
for w := 0; w < numWriters; w++ {
go func(writerID int) {
defer wg.Done()
for i := 0; i < writesPerWriter; i++ {
snapshot := &aether.ActorSnapshot{
ActorID: "actor-123",
Version: int64(100 + writerID*writesPerWriter + i),
State: map[string]interface{}{
"writer": writerID,
"index": i,
},
Timestamp: time.Now(),
}
if err := store.SaveSnapshot(snapshot); err != nil {
t.Errorf("SaveSnapshot failed: %v", err)
}
}
}(w)
}
wg.Wait()
// Verify final state - should have the highest version
snapshot, err := store.GetLatestSnapshot("actor-123")
if err != nil {
t.Fatalf("GetLatestSnapshot failed: %v", err)
}
if snapshot == nil {
t.Fatal("expected snapshot, got nil")
}
// The highest version should be around 100 + (numWriters-1)*writesPerWriter + (writesPerWriter-1)
// which is 100 + 9*10 + 9 = 199
expectedMaxVersion := int64(100 + (numWriters-1)*writesPerWriter + (writesPerWriter - 1))
if snapshot.Version != expectedMaxVersion {
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)
}
}
}
// === EventStored Publishing Tests ===
func TestSaveEvent_WithBroadcaster_PublishesEventStored(t *testing.T) {
// Create a mock broadcaster to capture published events
broadcaster := aether.NewEventBus()
store := NewInMemoryEventStoreWithBroadcaster(broadcaster, "test-namespace")
// Subscribe to EventStored events
ch := broadcaster.Subscribe("test-namespace")
defer broadcaster.Unsubscribe("test-namespace", ch)
event := &aether.Event{
ID: "evt-123",
EventType: "OrderPlaced",
ActorID: "order-456",
Version: 1,
Data: map[string]interface{}{
"total": 100.50,
},
Timestamp: time.Now(),
}
// Save event
err := store.SaveEvent(event)
if err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
// Check if EventStored was published
select {
case publishedEvent := <-ch:
if publishedEvent == nil {
t.Fatal("received nil event from broadcaster")
}
if publishedEvent.EventType != aether.EventTypeEventStored {
t.Errorf("expected EventType %q, got %q", aether.EventTypeEventStored, publishedEvent.EventType)
}
if publishedEvent.ActorID != "order-456" {
t.Errorf("expected ActorID %q, got %q", "order-456", publishedEvent.ActorID)
}
if publishedEvent.Version != 1 {
t.Errorf("expected Version 1, got %d", publishedEvent.Version)
}
// Check data contains original event info
if publishedEvent.Data["eventId"] != "evt-123" {
t.Errorf("expected eventId %q, got %q", "evt-123", publishedEvent.Data["eventId"])
}
case <-time.After(100 * time.Millisecond):
t.Fatal("timeout waiting for EventStored event")
}
}
func TestSaveEvent_VersionConflict_NoEventStored(t *testing.T) {
broadcaster := aether.NewEventBus()
store := NewInMemoryEventStoreWithBroadcaster(broadcaster, "test-namespace")
// Subscribe to EventStored events
ch := broadcaster.Subscribe("test-namespace")
defer broadcaster.Unsubscribe("test-namespace", ch)
// Save first event
event1 := &aether.Event{
ID: "evt-1",
EventType: "OrderPlaced",
ActorID: "order-456",
Version: 1,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
err := store.SaveEvent(event1)
if err != nil {
t.Fatalf("SaveEvent(event1) failed: %v", err)
}
// Drain the first EventStored event
select {
case <-ch:
case <-time.After(100 * time.Millisecond):
t.Fatal("timeout waiting for first EventStored event")
}
// Try to save event with non-increasing version (should fail)
event2 := &aether.Event{
ID: "evt-2",
EventType: "OrderPlaced",
ActorID: "order-456",
Version: 1, // Same version, should conflict
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
err = store.SaveEvent(event2)
if !errors.Is(err, aether.ErrVersionConflict) {
t.Fatalf("expected ErrVersionConflict, got %v", err)
}
// Verify no EventStored event was published
select {
case <-ch:
t.Fatal("expected no EventStored event, but received one")
case <-time.After(50 * time.Millisecond):
// Expected - no event published
}
}
func TestSaveEvent_MultipleEvents_PublishesMultipleEventStored(t *testing.T) {
broadcaster := aether.NewEventBus()
store := NewInMemoryEventStoreWithBroadcaster(broadcaster, "test-namespace")
// Subscribe to EventStored events
ch := broadcaster.Subscribe("test-namespace")
defer broadcaster.Unsubscribe("test-namespace", ch)
// Save multiple events
for i := int64(1); i <= 3; i++ {
event := &aether.Event{
ID: fmt.Sprintf("evt-%d", i),
EventType: "OrderPlaced",
ActorID: "order-456",
Version: i,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
err := store.SaveEvent(event)
if err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
}
// Verify we received 3 EventStored events in order
for i := int64(1); i <= 3; i++ {
select {
case publishedEvent := <-ch:
if publishedEvent == nil {
t.Fatal("received nil event from broadcaster")
}
if publishedEvent.Version != i {
t.Errorf("expected Version %d, got %d", i, publishedEvent.Version)
}
case <-time.After(100 * time.Millisecond):
t.Fatalf("timeout waiting for EventStored event %d", i)
}
}
}
func TestSaveEvent_WithoutBroadcaster_NoPanic(t *testing.T) {
// Test that SaveEvent works without a broadcaster (nil broadcaster)
store := NewInMemoryEventStore()
event := &aether.Event{
ID: "evt-123",
EventType: "OrderPlaced",
ActorID: "order-456",
Version: 1,
Data: map[string]interface{}{
"total": 100.50,
},
Timestamp: time.Now(),
}
// This should not panic even though broadcaster is nil
err := store.SaveEvent(event)
if err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
// Verify event was saved
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))
}
}