Files
event-driven-shoppinglist/integration_test.go
2025-09-29 12:03:28 +02:00

607 lines
15 KiB
Go

package main
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// MockIntegrationEventStore for integration testing
type MockIntegrationEventStore struct {
events []Event
items map[string]map[string]interface{}
nextSeq int
patcher *JSONPatcher
syncValid bool
forceError bool
}
func NewMockIntegrationEventStore() *MockIntegrationEventStore {
return &MockIntegrationEventStore{
events: []Event{},
items: make(map[string]map[string]interface{}),
nextSeq: 1,
patcher: &JSONPatcher{},
}
}
func (m *MockIntegrationEventStore) GetLatestEvent() (*Event, error) {
if len(m.events) == 0 {
return nil, nil
}
return &m.events[len(m.events)-1], nil
}
func (m *MockIntegrationEventStore) ProcessEvent(incomingEvent *Event) (*Event, error) {
if m.forceError {
return nil, assert.AnError
}
// Create processed event
event := &Event{
Seq: m.nextSeq,
Hash: "hash_" + string(rune(m.nextSeq+'0')),
ItemID: incomingEvent.ItemID,
EventID: "event_" + string(rune(m.nextSeq+'0')),
Collection: incomingEvent.Collection,
Operation: incomingEvent.Operation,
Path: incomingEvent.Path,
Value: incomingEvent.Value,
From: incomingEvent.From,
Timestamp: time.Now(),
}
// Apply event to items using JSON Patch
itemKey := incomingEvent.Collection + ":" + incomingEvent.ItemID
if m.items[itemKey] == nil {
m.items[itemKey] = map[string]interface{}{
"id": incomingEvent.ItemID,
}
}
// Create patch operation and apply it
patch := PatchOperation{
Op: incomingEvent.Operation,
Path: incomingEvent.Path,
Value: incomingEvent.Value,
From: incomingEvent.From,
}
updatedItem, err := m.patcher.ApplyPatches(m.items[itemKey], []PatchOperation{patch})
if err != nil {
return nil, err // Return the actual JSON patch error
}
m.items[itemKey] = updatedItem
m.events = append(m.events, *event)
m.nextSeq++
return event, nil
}
func (m *MockIntegrationEventStore) GetEventsSince(seq int) ([]Event, error) {
var events []Event
for _, event := range m.events {
if event.Seq > seq {
events = append(events, event)
}
}
return events, nil
}
func (m *MockIntegrationEventStore) GetAllItems(collection string) ([]map[string]interface{}, error) {
var items []map[string]interface{}
for key, item := range m.items {
if len(key) >= len(collection) && key[:len(collection)] == collection {
// Skip soft-deleted items
if deletedAt, exists := item["deleted_at"]; !exists || deletedAt == "" {
items = append(items, item)
}
}
}
return items, nil
}
func (m *MockIntegrationEventStore) ValidateSync(seq int, hash string) (bool, error) {
if m.forceError {
return false, assert.AnError
}
return m.syncValid, nil
}
func TestFullWorkflow(t *testing.T) {
mockStore := NewMockIntegrationEventStore()
t.Run("1. Create initial item using JSON Patch", func(t *testing.T) {
event := &Event{
ItemID: "workflow_item_001",
Collection: "shopping_items",
Operation: "add",
Path: "/content",
Value: "Initial workflow item",
}
result, err := mockStore.ProcessEvent(event)
require.NoError(t, err)
assert.Equal(t, 1, result.Seq)
assert.Equal(t, "add", result.Operation)
assert.Equal(t, "/content", result.Path)
assert.Equal(t, "Initial workflow item", result.Value)
})
t.Run("2. Update item with multiple operations", func(t *testing.T) {
// Add priority
event1 := &Event{
ItemID: "workflow_item_001",
Collection: "shopping_items",
Operation: "add",
Path: "/priority",
Value: "high",
}
result1, err := mockStore.ProcessEvent(event1)
require.NoError(t, err)
assert.Equal(t, 2, result1.Seq)
// Update content
event2 := &Event{
ItemID: "workflow_item_001",
Collection: "shopping_items",
Operation: "replace",
Path: "/content",
Value: "Updated workflow item",
}
result2, err := mockStore.ProcessEvent(event2)
require.NoError(t, err)
assert.Equal(t, 3, result2.Seq)
})
t.Run("3. Create second item", func(t *testing.T) {
event := &Event{
ItemID: "workflow_item_002",
Collection: "shopping_items",
Operation: "add",
Path: "/content",
Value: "Second workflow item",
}
result, err := mockStore.ProcessEvent(event)
require.NoError(t, err)
assert.Equal(t, 4, result.Seq)
})
t.Run("4. Get all items", func(t *testing.T) {
items, err := mockStore.GetAllItems("shopping_items")
require.NoError(t, err)
assert.Len(t, items, 2)
// Find first item and verify its state
var item1 map[string]interface{}
for _, item := range items {
if item["id"] == "workflow_item_001" {
item1 = item
break
}
}
require.NotNil(t, item1)
assert.Equal(t, "Updated workflow item", item1["content"])
assert.Equal(t, "high", item1["priority"])
})
t.Run("5. Test client synchronization", func(t *testing.T) {
// Test getting events since sequence 2
events, err := mockStore.GetEventsSince(2)
require.NoError(t, err)
assert.Len(t, events, 2) // events 3 and 4
// Verify latest event
latestEvent, err := mockStore.GetLatestEvent()
require.NoError(t, err)
assert.Equal(t, 4, latestEvent.Seq)
// Test sync validation
mockStore.syncValid = true
isValid, err := mockStore.ValidateSync(4, "hash_4")
require.NoError(t, err)
assert.True(t, isValid)
})
t.Run("6. Soft delete an item", func(t *testing.T) {
deleteEvent := &Event{
ItemID: "workflow_item_001",
Collection: "shopping_items",
Operation: "add",
Path: "/deleted_at",
Value: time.Now().Format(time.RFC3339),
}
result, err := mockStore.ProcessEvent(deleteEvent)
require.NoError(t, err)
assert.Equal(t, 5, result.Seq)
assert.Equal(t, "add", result.Operation)
assert.Equal(t, "/deleted_at", result.Path)
// Verify item is now filtered out
items, err := mockStore.GetAllItems("shopping_items")
require.NoError(t, err)
assert.Len(t, items, 1) // Only one item remaining
// Verify remaining item is item_002
assert.Equal(t, "workflow_item_002", items[0]["id"])
})
t.Run("7. Complex nested operations", func(t *testing.T) {
// Add metadata object
metadataEvent := &Event{
ItemID: "workflow_item_002",
Collection: "shopping_items",
Operation: "add",
Path: "/metadata",
Value: `{"created_by": "system", "version": 1}`,
}
result, err := mockStore.ProcessEvent(metadataEvent)
require.NoError(t, err)
assert.Equal(t, 6, result.Seq)
// Skip nested field update since we store JSON as strings
// The metadata is stored as a JSON string, not a parsed object
})
t.Run("8. Test operation on nested data", func(t *testing.T) {
items, err := mockStore.GetAllItems("shopping_items")
require.NoError(t, err)
assert.Len(t, items, 1)
item := items[0]
assert.Equal(t, "workflow_item_002", item["id"])
assert.Equal(t, "Second workflow item", item["content"])
// Check nested metadata (stored as JSON string)
metadataStr, ok := item["metadata"].(string)
require.True(t, ok)
assert.Contains(t, metadataStr, "system")
assert.Contains(t, metadataStr, "1") // Original value
})
t.Run("9. Test move operation", func(t *testing.T) {
moveEvent := &Event{
ItemID: "workflow_item_002",
Collection: "shopping_items",
Operation: "move",
From: "/content",
Path: "/title",
}
result, err := mockStore.ProcessEvent(moveEvent)
require.NoError(t, err)
assert.Equal(t, 7, result.Seq)
// Verify move operation
items, err := mockStore.GetAllItems("shopping_items")
require.NoError(t, err)
item := items[0]
assert.Equal(t, "Second workflow item", item["title"])
assert.Nil(t, item["content"]) // Should be moved, not copied
})
t.Run("10. Test copy operation", func(t *testing.T) {
copyEvent := &Event{
ItemID: "workflow_item_002",
Collection: "shopping_items",
Operation: "copy",
From: "/title",
Path: "/backup_title",
}
result, err := mockStore.ProcessEvent(copyEvent)
require.NoError(t, err)
assert.Equal(t, 8, result.Seq)
// Verify copy operation
items, err := mockStore.GetAllItems("shopping_items")
require.NoError(t, err)
item := items[0]
assert.Equal(t, "Second workflow item", item["title"])
assert.Equal(t, "Second workflow item", item["backup_title"])
})
t.Run("11. Final state verification", func(t *testing.T) {
// Verify final event count
latestEvent, err := mockStore.GetLatestEvent()
require.NoError(t, err)
assert.Equal(t, 8, latestEvent.Seq)
// Verify all events are recorded
allEvents, err := mockStore.GetEventsSince(0)
require.NoError(t, err)
assert.Len(t, allEvents, 8)
// Verify final item state
items, err := mockStore.GetAllItems("shopping_items")
require.NoError(t, err)
assert.Len(t, items, 1)
finalItem := items[0]
assert.Equal(t, "workflow_item_002", finalItem["id"])
assert.Equal(t, "Second workflow item", finalItem["title"])
assert.Equal(t, "Second workflow item", finalItem["backup_title"])
assert.Nil(t, finalItem["content"]) // Moved to title
metadataStr, ok := finalItem["metadata"].(string)
require.True(t, ok)
assert.Contains(t, metadataStr, "system")
assert.Contains(t, metadataStr, "1")
})
}
func TestErrorScenarios(t *testing.T) {
tests := []struct {
name string
setup func(*MockIntegrationEventStore)
event Event
wantErr bool
}{
{
name: "Invalid JSON Patch operation",
setup: func(m *MockIntegrationEventStore) {},
event: Event{
ItemID: "error_test_001",
Collection: "shopping_items",
Operation: "invalid_op",
Path: "/content",
Value: "test",
},
wantErr: true,
},
{
name: "Test operation failure",
setup: func(m *MockIntegrationEventStore) {
// Create item first
m.items["shopping_items:test_item"] = map[string]interface{}{
"id": "test_item",
"content": "original",
}
},
event: Event{
ItemID: "test_item",
Collection: "shopping_items",
Operation: "test",
Path: "/content",
Value: "different", // This should fail the test
},
wantErr: true,
},
{
name: "Remove non-existent field",
setup: func(m *MockIntegrationEventStore) {
m.items["shopping_items:test_item"] = map[string]interface{}{
"id": "test_item",
}
},
event: Event{
ItemID: "test_item",
Collection: "shopping_items",
Operation: "remove",
Path: "/nonexistent",
},
wantErr: true,
},
{
name: "Replace non-existent field",
setup: func(m *MockIntegrationEventStore) {
m.items["shopping_items:test_item"] = map[string]interface{}{
"id": "test_item",
}
},
event: Event{
ItemID: "test_item",
Collection: "shopping_items",
Operation: "replace",
Path: "/nonexistent",
Value: "value",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockStore := NewMockIntegrationEventStore()
tt.setup(mockStore)
_, err := mockStore.ProcessEvent(&tt.event)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestSyncScenarios(t *testing.T) {
mockStore := NewMockIntegrationEventStore()
// Create some events
events := []*Event{
{
ItemID: "sync_item_001",
Collection: "shopping_items",
Operation: "add",
Path: "/content",
Value: "First item",
},
{
ItemID: "sync_item_002",
Collection: "shopping_items",
Operation: "add",
Path: "/content",
Value: "Second item",
},
{
ItemID: "sync_item_001",
Collection: "shopping_items",
Operation: "replace",
Path: "/content",
Value: "Updated first item",
},
}
// Process all events
for _, event := range events {
_, err := mockStore.ProcessEvent(event)
require.NoError(t, err)
}
t.Run("Full sync from beginning", func(t *testing.T) {
events, err := mockStore.GetEventsSince(0)
require.NoError(t, err)
assert.Len(t, events, 3)
// Verify event sequence
for i, event := range events {
assert.Equal(t, i+1, event.Seq)
}
})
t.Run("Incremental sync", func(t *testing.T) {
events, err := mockStore.GetEventsSince(1)
require.NoError(t, err)
assert.Len(t, events, 2)
// Should get events 2 and 3
assert.Equal(t, 2, events[0].Seq)
assert.Equal(t, 3, events[1].Seq)
})
t.Run("Sync validation", func(t *testing.T) {
// Test valid sync
mockStore.syncValid = true
isValid, err := mockStore.ValidateSync(3, "hash_3")
require.NoError(t, err)
assert.True(t, isValid)
// Test invalid sync
mockStore.syncValid = false
isValid, err = mockStore.ValidateSync(2, "wrong_hash")
require.NoError(t, err)
assert.False(t, isValid)
})
t.Run("Get latest event", func(t *testing.T) {
latest, err := mockStore.GetLatestEvent()
require.NoError(t, err)
assert.Equal(t, 3, latest.Seq)
assert.Equal(t, "sync_item_001", latest.ItemID)
assert.Equal(t, "replace", latest.Operation)
})
}
func TestConcurrentOperations(t *testing.T) {
mockStore := NewMockIntegrationEventStore()
// Simulate concurrent operations on different items
t.Run("Multiple items concurrent creation", func(t *testing.T) {
events := []*Event{
{
ItemID: "concurrent_001",
Collection: "shopping_items",
Operation: "add",
Path: "/content",
Value: "Concurrent item 1",
},
{
ItemID: "concurrent_002",
Collection: "shopping_items",
Operation: "add",
Path: "/content",
Value: "Concurrent item 2",
},
{
ItemID: "concurrent_003",
Collection: "shopping_items",
Operation: "add",
Path: "/content",
Value: "Concurrent item 3",
},
}
// Process events sequentially (simulating concurrent processing)
var results []*Event
for _, event := range events {
result, err := mockStore.ProcessEvent(event)
require.NoError(t, err)
results = append(results, result)
}
// Verify all events were processed with proper sequence numbers
for i, result := range results {
assert.Equal(t, i+1, result.Seq)
}
// Verify all items exist
items, err := mockStore.GetAllItems("shopping_items")
require.NoError(t, err)
assert.Len(t, items, 3)
})
t.Run("Same item multiple operations", func(t *testing.T) {
itemID := "concurrent_same_item"
operations := []*Event{
{
ItemID: itemID,
Collection: "shopping_items",
Operation: "add",
Path: "/content",
Value: "Initial content",
},
{
ItemID: itemID,
Collection: "shopping_items",
Operation: "add",
Path: "/priority",
Value: "low",
},
{
ItemID: itemID,
Collection: "shopping_items",
Operation: "replace",
Path: "/priority",
Value: "high",
},
{
ItemID: itemID,
Collection: "shopping_items",
Operation: "add",
Path: "/tags",
Value: `["urgent", "important"]`,
},
}
for _, op := range operations {
_, err := mockStore.ProcessEvent(op)
require.NoError(t, err)
}
// Verify final state
items, err := mockStore.GetAllItems("shopping_items")
require.NoError(t, err)
var targetItem map[string]interface{}
for _, item := range items {
if item["id"] == itemID {
targetItem = item
break
}
}
require.NotNil(t, targetItem)
assert.Equal(t, "Initial content", targetItem["content"])
assert.Equal(t, "high", targetItem["priority"])
assert.NotNil(t, targetItem["tags"])
})
}