607 lines
15 KiB
Go
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"])
|
|
})
|
|
}
|