package store import ( "testing" "time" "git.flowmade.one/flowmade-one/aether" ) // TestEventImmutability verifies that events cannot be modified after being // persisted to the store. This test demonstrates that Aether maintains an // append-only event log that serves as an immutable audit trail. func TestEventImmutability_InMemory(t *testing.T) { store := NewInMemoryEventStore() // Create and save an event originalEvent := &aether.Event{ ID: "evt-immutability-123", EventType: "OrderPlaced", ActorID: "order-789", Version: 1, Data: map[string]interface{}{ "total": 99.99, "currency": "USD", }, 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("order-789", 0) if err != nil { t.Fatalf("GetEvents failed: %v", err) } if len(events) != 1 { t.Fatalf("expected 1 event, got %d", len(events)) } retrievedEvent := events[0] // Verify the event has the original data if retrievedEvent.Data["total"].(float64) != 99.99 { t.Errorf("expected total 99.99, got %v", retrievedEvent.Data["total"]) } // Attempt to modify the retrieved event retrievedEvent.Data["total"] = 199.99 retrievedEvent.EventType = "OrderCancelled" retrievedEvent.Data["currency"] = "EUR" // Retrieve the event again from the store events, err = store.GetEvents("order-789", 0) if err != nil { t.Fatalf("GetEvents failed on second call: %v", err) } storedEvent := events[0] // Verify that the stored event still has the original data // This confirms that modifying the retrieved event didn't affect the stored event if storedEvent.Data["total"].(float64) != 99.99 { t.Errorf("stored event total was modified: expected 99.99, got %v", storedEvent.Data["total"]) } if storedEvent.EventType != "OrderPlaced" { t.Errorf("stored event type was modified: expected OrderPlaced, got %s", storedEvent.EventType) } if storedEvent.Data["currency"].(string) != "USD" { t.Errorf("stored event currency was modified: expected USD, got %s", storedEvent.Data["currency"]) } // Additional verification: EventStore has no Update or Delete methods // This is enforced at the type system level by the interface definition. // The EventStore interface only provides: // - SaveEvent (append-only) // - GetEvents (read-only) // - GetLatestVersion (read-only) // There are intentionally no Update, Delete, or Modify methods. } // TestEventImmutability_Sequential verifies that events remain immutable // even when multiple events are saved for the same actor. func TestEventImmutability_Sequential(t *testing.T) { store := NewInMemoryEventStore() actorID := "order-sequential-123" // Save multiple events event1 := &aether.Event{ ID: "evt-seq-1", EventType: "OrderCreated", ActorID: actorID, Version: 1, Data: map[string]interface{}{"status": "new"}, Timestamp: time.Now(), } event2 := &aether.Event{ ID: "evt-seq-2", EventType: "OrderProcessed", ActorID: actorID, Version: 2, Data: map[string]interface{}{"status": "processing"}, Timestamp: time.Now().Add(time.Second), } err := store.SaveEvent(event1) if err != nil { t.Fatalf("SaveEvent(event1) failed: %v", err) } err = store.SaveEvent(event2) if err != nil { t.Fatalf("SaveEvent(event2) failed: %v", err) } // Retrieve all events events, err := store.GetEvents(actorID, 0) if err != nil { t.Fatalf("GetEvents failed: %v", err) } if len(events) != 2 { t.Fatalf("expected 2 events, got %d", len(events)) } // Attempt to modify the first event's data events[0].Data["status"] = "cancelled" // Retrieve events again and verify the first event is unchanged events, err = store.GetEvents(actorID, 0) if err != nil { t.Fatalf("GetEvents failed on second call: %v", err) } if events[0].Data["status"].(string) != "new" { t.Errorf("first event was modified: expected status=new, got %v", events[0].Data["status"]) } if events[1].Data["status"].(string) != "processing" { t.Errorf("second event was modified: expected status=processing, got %v", events[1].Data["status"]) } } // TestNoDeleteMethod verifies that the EventStore interface has no Delete method. // This is a compile-time check: if Delete were added to the interface, // all implementations would fail to compile until they implemented it. // This test serves as a runtime confirmation that the interface intentionally // omits delete/update operations. func TestNoDeleteMethod(t *testing.T) { store := NewInMemoryEventStore() // The following would not compile if EventStore had a Delete method: // store.Delete(...) // store.Update(...) // store.Modify(...) // This test passes if compilation succeeds, confirming that // the EventStore interface is append-only by design. // Verify the interface has exactly 3 methods var iface interface{} = store if _, ok := iface.(aether.EventStore); !ok { t.Fatal("InMemoryEventStore does not implement EventStore") } // The EventStore interface should only have SaveEvent, GetEvents, GetLatestVersion // Verify by attempting to call each method event := &aether.Event{ ID: "evt-test", EventType: "Test", ActorID: "test-actor", Version: 1, Data: map[string]interface{}{}, Timestamp: time.Now(), } // These should work store.SaveEvent(event) store.GetEvents("test-actor", 0) store.GetLatestVersion("test-actor") // No other methods should exist (compile-time check) }