Files
aether/store/eventstored_test.go
Claude Code 5223cf136a fix: address review feedback
- Removed duplicate blank line in event.go
- Use original event timestamp instead of time.Now() for EventStored
- Fixed MockEventBroadcaster.Subscribe to return nil instead of closed channel
- Added integration tests for EventStored with JetStreamEventStore

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-01-13 22:25:10 +01:00

363 lines
9.3 KiB
Go

package store
import (
"fmt"
"sync"
"testing"
"time"
"git.flowmade.one/flowmade-one/aether"
)
// MockEventBroadcaster captures published events for testing
type MockEventBroadcaster struct {
mu sync.RWMutex
events []*aether.Event
namespaces map[string]int
}
func NewMockEventBroadcaster() *MockEventBroadcaster {
return &MockEventBroadcaster{
events: make([]*aether.Event, 0),
namespaces: make(map[string]int),
}
}
func (m *MockEventBroadcaster) Subscribe(namespacePattern string) <-chan *aether.Event {
return nil
}
func (m *MockEventBroadcaster) SubscribeWithFilter(namespacePattern string, filter *aether.SubscriptionFilter) <-chan *aether.Event {
return nil
}
func (m *MockEventBroadcaster) Unsubscribe(namespacePattern string, ch <-chan *aether.Event) {}
func (m *MockEventBroadcaster) Publish(namespaceID string, event *aether.Event) {
m.mu.Lock()
defer m.mu.Unlock()
m.events = append(m.events, event)
m.namespaces[namespaceID]++
}
func (m *MockEventBroadcaster) Stop() {}
func (m *MockEventBroadcaster) SubscriberCount(namespaceID string) int {
return 0
}
func (m *MockEventBroadcaster) GetPublishedEvents() []*aether.Event {
m.mu.RLock()
defer m.mu.RUnlock()
events := make([]*aether.Event, len(m.events))
copy(events, m.events)
return events
}
// === InMemoryEventStore EventStored Tests ===
func TestEventStored_PublishedOnSaveSuccess(t *testing.T) {
store := NewInMemoryEventStore()
mockBus := NewMockEventBroadcaster()
store.WithEventBus(mockBus)
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 EventStored was published
published := mockBus.GetPublishedEvents()
if len(published) != 1 {
t.Fatalf("expected 1 published event, got %d", len(published))
}
storedEvent := published[0]
if storedEvent.EventType != "EventStored" {
t.Errorf("expected EventType 'EventStored', got %q", storedEvent.EventType)
}
if storedEvent.ActorID != "order-456" {
t.Errorf("expected ActorID 'order-456', got %q", storedEvent.ActorID)
}
if storedEvent.Data["eventId"] != "evt-123" {
t.Errorf("expected eventId 'evt-123', got %v", storedEvent.Data["eventId"])
}
if storedEvent.Data["version"] != int64(1) {
t.Errorf("expected version 1, got %v", storedEvent.Data["version"])
}
}
func TestEventStored_NotPublishedOnVersionConflict(t *testing.T) {
store := NewInMemoryEventStore()
mockBus := NewMockEventBroadcaster()
store.WithEventBus(mockBus)
// Save first event
event1 := &aether.Event{
ID: "evt-1",
EventType: "TestEvent",
ActorID: "actor-1",
Version: 1,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event1); err != nil {
t.Fatalf("First SaveEvent failed: %v", err)
}
// Try to save event with same version (conflict)
event2 := &aether.Event{
ID: "evt-2",
EventType: "TestEvent",
ActorID: "actor-1",
Version: 1, // Same version - should conflict
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
err := store.SaveEvent(event2)
if err == nil {
t.Fatal("expected VersionConflictError, got nil")
}
// Verify only 1 EventStored was published (from first event)
published := mockBus.GetPublishedEvents()
if len(published) != 1 {
t.Fatalf("expected 1 published event after conflict, got %d", len(published))
}
}
func TestEventStored_MultipleEventsPublished(t *testing.T) {
store := NewInMemoryEventStore()
mockBus := NewMockEventBroadcaster()
store.WithEventBus(mockBus)
// Save 5 events
for i := 1; i <= 5; i++ {
event := &aether.Event{
ID: fmt.Sprintf("evt-%d", i),
EventType: "TestEvent",
ActorID: "actor-1",
Version: int64(i),
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent %d failed: %v", i, err)
}
}
// Verify 5 EventStored events were published
published := mockBus.GetPublishedEvents()
if len(published) != 5 {
t.Fatalf("expected 5 published events, got %d", len(published))
}
// Verify each has correct data
for i := 0; i < 5; i++ {
if published[i].Data["version"] != int64(i+1) {
t.Errorf("event %d: expected version %d, got %v", i, i+1, published[i].Data["version"])
}
}
}
func TestEventStored_NotPublishedWithoutEventBus(t *testing.T) {
store := NewInMemoryEventStore()
// Don't set event bus
event := &aether.Event{
ID: "evt-123",
EventType: "OrderPlaced",
ActorID: "order-456",
Version: 1,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
// Should succeed without publishing (no-op)
err := store.SaveEvent(event)
if err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
// Event should be persisted normally
retrieved, err := store.GetEvents("order-456", 0)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
if len(retrieved) != 1 {
t.Errorf("expected 1 event, got %d", len(retrieved))
}
}
func TestEventStored_ContainsRequiredFields(t *testing.T) {
store := NewInMemoryEventStore()
mockBus := NewMockEventBroadcaster()
store.WithEventBus(mockBus)
event := &aether.Event{
ID: "evt-abc",
EventType: "TestEvent",
ActorID: "actor-xyz",
Version: 42,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
published := mockBus.GetPublishedEvents()
if len(published) != 1 {
t.Fatalf("expected 1 event, got %d", len(published))
}
storedEvent := published[0]
// Verify required fields
if storedEvent.Data["eventId"] != "evt-abc" {
t.Error("missing or incorrect eventId")
}
if storedEvent.Data["actorId"] != "actor-xyz" {
t.Error("missing or incorrect actorId")
}
if storedEvent.Data["version"] != int64(42) {
t.Error("missing or incorrect version")
}
if _, hasTimestamp := storedEvent.Data["timestamp"]; !hasTimestamp {
t.Error("missing timestamp")
}
}
func TestEventStored_PublishedToCorrectNamespace(t *testing.T) {
store := NewInMemoryEventStore()
mockBus := NewMockEventBroadcaster()
store.WithEventBus(mockBus)
event := &aether.Event{
ID: "evt-1",
EventType: "TestEvent",
ActorID: "actor-1",
Version: 1,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
// Verify published to __internal__ namespace
namespaces := mockBus.namespaces
if count, ok := namespaces["__internal__"]; !ok || count != 1 {
t.Errorf("expected 1 event published to __internal__, got %v", namespaces)
}
}
func TestEventStored_WithMetricsRecording(t *testing.T) {
store := NewInMemoryEventStore()
mockBus := NewMockEventBroadcaster()
mockMetrics := aether.NewMetricsCollector()
store.WithEventBus(mockBus)
store.WithMetrics(mockMetrics)
event := &aether.Event{
ID: "evt-1",
EventType: "TestEvent",
ActorID: "actor-1",
Version: 1,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent failed: %v", err)
}
// Verify metrics were recorded
published := mockMetrics.EventsPublished("__internal__")
if published != 1 {
t.Errorf("expected 1 published metric, got %d", published)
}
}
func TestEventStored_ConcurrentPublishing(t *testing.T) {
store := NewInMemoryEventStore()
mockBus := NewMockEventBroadcaster()
store.WithEventBus(mockBus)
numGoroutines := 10
eventsPerGoroutine := 5
var wg sync.WaitGroup
for g := 0; g < numGoroutines; g++ {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
for i := 0; i < eventsPerGoroutine; i++ {
version := int64(goroutineID*eventsPerGoroutine + i + 1)
event := &aether.Event{
ID: fmt.Sprintf("evt-%d-%d", goroutineID, i),
EventType: "TestEvent",
ActorID: fmt.Sprintf("actor-%d", goroutineID),
Version: version,
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
_ = store.SaveEvent(event) // Ignore errors (some may conflict)
}
}(g)
}
wg.Wait()
// Verify EventStored events were published for successful saves
published := mockBus.GetPublishedEvents()
if len(published) != numGoroutines*eventsPerGoroutine {
t.Logf("Note: got %d published events (some saves may have conflicted)", len(published))
}
if len(published) == 0 {
t.Fatal("expected at least some published events")
}
}
func TestEventStored_OrderPreserved(t *testing.T) {
store := NewInMemoryEventStore()
mockBus := NewMockEventBroadcaster()
store.WithEventBus(mockBus)
// Save 3 events in order
for i := 1; i <= 3; i++ {
event := &aether.Event{
ID: fmt.Sprintf("evt-%d", i),
EventType: "TestEvent",
ActorID: "actor-1",
Version: int64(i),
Data: map[string]interface{}{},
Timestamp: time.Now(),
}
if err := store.SaveEvent(event); err != nil {
t.Fatalf("SaveEvent %d failed: %v", i, err)
}
}
published := mockBus.GetPublishedEvents()
// Verify order is preserved
for i := 0; i < 3; i++ {
if published[i].Data["eventId"] != fmt.Sprintf("evt-%d", i+1) {
t.Errorf("event %d: expected evt-%d, got %v", i, i+1, published[i].Data["eventId"])
}
}
}