package store import ( "testing" "time" "git.flowmade.one/flowmade-one/aether" ) // TestEventImmutability_MemoryStore verifies that events cannot be modified after persistence // in the in-memory event store. This demonstrates the append-only nature of event sourcing. func TestEventImmutability_MemoryStore(t *testing.T) { store := NewInMemoryEventStore() actorID := "test-actor-123" // Create and save an event originalEvent := &aether.Event{ ID: "evt-immutable-1", EventType: "TestEvent", ActorID: actorID, Version: 1, Data: map[string]interface{}{ "value": "original", }, Timestamp: time.Now(), } err := store.SaveEvent(originalEvent) if err != nil { t.Fatalf("SaveEvent failed: %v", err) } // Retrieve the event from the store events, err := store.GetEvents(actorID, 0) if err != nil { t.Fatalf("GetEvents failed: %v", err) } if len(events) == 0 { t.Fatal("expected 1 event, got 0") } retrievedEvent := events[0] // Verify the stored event has the correct values if retrievedEvent.Data["value"] != "original" { t.Errorf("Data value mismatch: got %v, want %v", retrievedEvent.Data["value"], "original") } if retrievedEvent.EventType != "TestEvent" { t.Errorf("EventType mismatch: got %q, want %q", retrievedEvent.EventType, "TestEvent") } // Verify ID is correct if retrievedEvent.ID != "evt-immutable-1" { t.Errorf("Event ID mismatch: got %q, want %q", retrievedEvent.ID, "evt-immutable-1") } } // TestEventImmutability_NoUpdateMethod verifies that the EventStore interface // has only append, read methods - no Update or Delete methods. func TestEventImmutability_NoUpdateMethod(t *testing.T) { // This test documents that the EventStore interface is append-only. // The interface intentionally provides: // - SaveEvent: append only // - GetEvents: read only // - GetLatestVersion: read only // // To verify this, we demonstrate that any attempt to call non-existent // update/delete methods would be caught at compile time (not runtime). // This is enforced by the interface definition in event.go which does // not include Update, Delete, or Modify methods. store := NewInMemoryEventStore() // Compile-time check: these would not compile if we tried them: // store.Update(event) // compile error: no such method // store.Delete(eventID) // compile error: no such method // store.Modify(eventID, newData) // compile error: no such method // Only these methods exist: var eventStore aether.EventStore = store if eventStore == nil { t.Fatal("eventStore is nil") } // If we got here, the compile-time checks passed t.Log("EventStore interface enforces append-only semantics by design") } // TestEventImmutability_VersionOnlyGoesUp verifies that versions are monotonically // increasing and attempting to save with a non-increasing version fails. func TestEventImmutability_VersionOnlyGoesUp(t *testing.T) { store := NewInMemoryEventStore() actorID := "actor-version-check" // Save first event with version 1 event1 := &aether.Event{ ID: "evt-v1", EventType: "Event1", ActorID: actorID, Version: 1, Data: map[string]interface{}{}, Timestamp: time.Now(), } err := store.SaveEvent(event1) if err != nil { t.Fatalf("SaveEvent(v1) failed: %v", err) } // Try to save with same version - should fail event2Same := &aether.Event{ ID: "evt-v1-again", EventType: "Event2", ActorID: actorID, Version: 1, // Same version Data: map[string]interface{}{}, Timestamp: time.Now(), } err = store.SaveEvent(event2Same) if err == nil { t.Error("expected SaveEvent(same version) to fail, but it succeeded") } // Try to save with lower version - should fail event3Lower := &aether.Event{ ID: "evt-v0", EventType: "Event3", ActorID: actorID, Version: 0, // Lower version Data: map[string]interface{}{}, Timestamp: time.Now(), } err = store.SaveEvent(event3Lower) if err == nil { t.Error("expected SaveEvent(lower version) to fail, but it succeeded") } // Save with next version - should succeed event4Next := &aether.Event{ ID: "evt-v2", EventType: "Event4", ActorID: actorID, Version: 2, Data: map[string]interface{}{}, Timestamp: time.Now(), } err = store.SaveEvent(event4Next) if err != nil { t.Fatalf("SaveEvent(v2) failed: %v", err) } // Verify we have exactly 2 events events, err := store.GetEvents(actorID, 0) if err != nil { t.Fatalf("GetEvents failed: %v", err) } if len(events) != 2 { t.Errorf("expected 2 events, got %d", len(events)) } } // TestEventImmutability_EventCannotBeDeleted verifies that there is no way to delete // events from the store through the EventStore interface. func TestEventImmutability_EventCannotBeDeleted(t *testing.T) { store := NewInMemoryEventStore() actorID := "actor-nodelete" // Save an event event := &aether.Event{ ID: "evt-nodelete", EventType: "ImportantEvent", ActorID: actorID, Version: 1, Data: map[string]interface{}{"critical": true}, Timestamp: time.Now(), } err := store.SaveEvent(event) if err != nil { t.Fatalf("SaveEvent failed: %v", err) } // Retrieve it events1, err := store.GetEvents(actorID, 0) if err != nil { t.Fatalf("GetEvents (1) failed: %v", err) } if len(events1) != 1 { t.Fatal("expected 1 event after save") } // Try to delete through interface - this method doesn't exist // store.Delete("evt-nodelete") // compile error: no such method // store.DeleteByActorID(actorID) // compile error: no such method // Verify the event is still there (we can't delete it) events2, err := store.GetEvents(actorID, 0) if err != nil { t.Fatalf("GetEvents (2) failed: %v", err) } if len(events2) != 1 { t.Errorf("expected 1 event (should not be deletable), got %d", len(events2)) } if events2[0].ID != "evt-nodelete" { t.Errorf("event ID changed: got %q, want %q", events2[0].ID, "evt-nodelete") } }