Files
event-driven-shoppinglist/integration_test.go
2025-09-29 08:22:38 +02:00

518 lines
15 KiB
Go

package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestFullWorkflow demonstrates a complete workflow of the RFC6902 Event Store
func TestFullWorkflow(t *testing.T) {
eventStore := NewMockSimpleEventStore()
router := setupTestRouter(eventStore)
t.Run("1. Create initial item using JSON Patch", func(t *testing.T) {
patches := []PatchOperation{
{Op: "add", Path: "/content", Value: "Milk"},
{Op: "add", Path: "/quantity", Value: 2},
{Op: "add", Path: "/category", Value: "dairy"},
}
body, _ := json.Marshal(patches)
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/milk-001", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response Event
err := json.NewDecoder(w.Body).Decode(&response)
assert.NoError(t, err)
assert.Equal(t, "milk-001", response.ItemID)
assert.Equal(t, 1, response.Seq)
assert.Len(t, response.Patches, 3)
})
t.Run("2. Update item with multiple operations", func(t *testing.T) {
patches := []PatchOperation{
{Op: "replace", Path: "/content", Value: "Organic Milk"},
{Op: "replace", Path: "/quantity", Value: 1},
{Op: "add", Path: "/store", Value: "Whole Foods"},
{Op: "add", Path: "/urgent", Value: true},
}
body, _ := json.Marshal(patches)
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/milk-001", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response Event
err := json.NewDecoder(w.Body).Decode(&response)
assert.NoError(t, err)
assert.Equal(t, 2, response.Seq)
})
t.Run("3. Create second item", func(t *testing.T) {
patches := []PatchOperation{
{Op: "add", Path: "/content", Value: "Bread"},
{Op: "add", Path: "/quantity", Value: 1},
{Op: "add", Path: "/category", Value: "bakery"},
}
body, _ := json.Marshal(patches)
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/bread-001", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
})
t.Run("4. Get all items", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/items/shopping_items", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.NewDecoder(w.Body).Decode(&response)
assert.NoError(t, err)
items, ok := response["items"].([]interface{})
assert.True(t, ok)
assert.Len(t, items, 2)
// Verify milk item was updated
var milkItem map[string]interface{}
for _, item := range items {
itemMap := item.(map[string]interface{})
if itemMap["id"] == "milk-001" {
milkItem = itemMap
break
}
}
assert.NotNil(t, milkItem)
assert.Equal(t, "Organic Milk", milkItem["content"])
assert.Equal(t, float64(1), milkItem["quantity"])
assert.Equal(t, "Whole Foods", milkItem["store"])
assert.Equal(t, true, milkItem["urgent"])
})
t.Run("5. Test client synchronization", func(t *testing.T) {
// Simulate client requesting initial sync
syncReq := SyncRequest{
LastSeq: 0,
LastHash: "",
}
body, _ := json.Marshal(syncReq)
req := httptest.NewRequest("POST", "/api/sync", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var syncResponse SyncResponse
err := json.NewDecoder(w.Body).Decode(&syncResponse)
assert.NoError(t, err)
assert.True(t, syncResponse.FullSync)
assert.Len(t, syncResponse.Events, 3) // 2 creates + 1 update
assert.Equal(t, 3, syncResponse.CurrentSeq)
assert.NotEmpty(t, syncResponse.CurrentHash)
// Now simulate incremental sync
syncReq = SyncRequest{
LastSeq: syncResponse.CurrentSeq,
LastHash: syncResponse.CurrentHash,
}
body, _ = json.Marshal(syncReq)
req = httptest.NewRequest("POST", "/api/sync", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
err = json.NewDecoder(w.Body).Decode(&syncResponse)
assert.NoError(t, err)
assert.False(t, syncResponse.FullSync)
assert.Len(t, syncResponse.Events, 0) // No new events
})
t.Run("6. Soft delete an item", func(t *testing.T) {
patches := []PatchOperation{
{Op: "add", Path: "/deleted_at", Value: time.Now().Format(time.RFC3339)},
}
body, _ := json.Marshal(patches)
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/bread-001", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Verify item is now filtered out
req = httptest.NewRequest("GET", "/api/items/shopping_items", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
var response map[string]interface{}
err := json.NewDecoder(w.Body).Decode(&response)
assert.NoError(t, err)
items, ok := response["items"].([]interface{})
assert.True(t, ok)
assert.Len(t, items, 1) // Only milk item left
})
t.Run("7. Complex nested operations", func(t *testing.T) {
patches := []PatchOperation{
{Op: "add", Path: "/metadata", Value: map[string]interface{}{
"tags": []interface{}{"organic", "priority"},
"supplier": "Local Farm",
"expiry": "2024-12-31",
}},
{Op: "add", Path: "/notes", Value: []interface{}{"Check expiry date", "Preferred brand"}},
}
body, _ := json.Marshal(patches)
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/milk-001", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Verify complex data was added
req = httptest.NewRequest("GET", "/api/items/shopping_items", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
var response map[string]interface{}
err := json.NewDecoder(w.Body).Decode(&response)
assert.NoError(t, err)
items := response["items"].([]interface{})
milkItem := items[0].(map[string]interface{})
metadata := milkItem["metadata"].(map[string]interface{})
assert.Equal(t, "Local Farm", metadata["supplier"])
assert.Equal(t, "2024-12-31", metadata["expiry"])
tags := metadata["tags"].([]interface{})
assert.Contains(t, tags, "organic")
assert.Contains(t, tags, "priority")
notes := milkItem["notes"].([]interface{})
assert.Contains(t, notes, "Check expiry date")
assert.Contains(t, notes, "Preferred brand")
})
t.Run("8. Test operation on nested data", func(t *testing.T) {
patches := []PatchOperation{
{Op: "replace", Path: "/metadata/supplier", Value: "Premium Organic Farm"},
{Op: "add", Path: "/metadata/certified", Value: true},
{Op: "replace", Path: "/notes/0", Value: "Always check expiry date carefully"},
}
body, _ := json.Marshal(patches)
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/milk-001", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Verify nested updates
req = httptest.NewRequest("GET", "/api/items/shopping_items", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
var response map[string]interface{}
err := json.NewDecoder(w.Body).Decode(&response)
assert.NoError(t, err)
items := response["items"].([]interface{})
milkItem := items[0].(map[string]interface{})
metadata := milkItem["metadata"].(map[string]interface{})
assert.Equal(t, "Premium Organic Farm", metadata["supplier"])
assert.Equal(t, true, metadata["certified"])
notes := milkItem["notes"].([]interface{})
assert.Equal(t, "Always check expiry date carefully", notes[0])
})
t.Run("9. Test move operation", func(t *testing.T) {
patches := []PatchOperation{
{Op: "move", From: "/urgent", Path: "/metadata/urgent"},
}
body, _ := json.Marshal(patches)
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/milk-001", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Verify move operation
req = httptest.NewRequest("GET", "/api/items/shopping_items", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
var response map[string]interface{}
err := json.NewDecoder(w.Body).Decode(&response)
assert.NoError(t, err)
items := response["items"].([]interface{})
milkItem := items[0].(map[string]interface{})
// Original field should be gone
_, exists := milkItem["urgent"]
assert.False(t, exists)
// New location should have the value
metadata := milkItem["metadata"].(map[string]interface{})
assert.Equal(t, true, metadata["urgent"])
})
t.Run("10. Test copy operation", func(t *testing.T) {
patches := []PatchOperation{
{Op: "copy", From: "/content", Path: "/display_name"},
}
body, _ := json.Marshal(patches)
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/milk-001", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Verify copy operation
req = httptest.NewRequest("GET", "/api/items/shopping_items", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
var response map[string]interface{}
err := json.NewDecoder(w.Body).Decode(&response)
assert.NoError(t, err)
items := response["items"].([]interface{})
milkItem := items[0].(map[string]interface{})
// Both fields should exist
assert.Equal(t, "Organic Milk", milkItem["content"])
assert.Equal(t, "Organic Milk", milkItem["display_name"])
})
t.Run("11. Final state verification", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/state", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var stateResponse map[string]interface{}
err := json.NewDecoder(w.Body).Decode(&stateResponse)
assert.NoError(t, err)
// We should have many events by now
assert.Greater(t, int(stateResponse["seq"].(float64)), 5)
assert.NotEmpty(t, stateResponse["hash"])
// Final sync to get all events
syncReq := SyncRequest{
LastSeq: 0,
LastHash: "",
}
body, _ := json.Marshal(syncReq)
req = httptest.NewRequest("POST", "/api/sync", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
var syncResponse SyncResponse
err = json.NewDecoder(w.Body).Decode(&syncResponse)
assert.NoError(t, err)
// Verify we have all the events in proper sequence
assert.True(t, len(syncResponse.Events) > 5)
for i := 1; i < len(syncResponse.Events); i++ {
assert.Greater(t, syncResponse.Events[i].Seq, syncResponse.Events[i-1].Seq)
}
})
}
// TestErrorScenarios tests various error conditions
func TestErrorScenarios(t *testing.T) {
eventStore := NewMockSimpleEventStore()
router := setupTestRouter(eventStore)
t.Run("Invalid JSON Patch operation", func(t *testing.T) {
patches := []PatchOperation{
{Op: "invalid_op", Path: "/content", Value: "test"},
}
body, _ := json.Marshal(patches)
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/item1", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
})
t.Run("Test operation failure", func(t *testing.T) {
// Create item first
createPatches := []PatchOperation{
{Op: "add", Path: "/status", Value: "active"},
}
body, _ := json.Marshal(createPatches)
req := httptest.NewRequest("PATCH", "/api/collections/test/items/item1", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Now try test operation that should fail
testPatches := []PatchOperation{
{Op: "test", Path: "/status", Value: "inactive"}, // This should fail
{Op: "replace", Path: "/status", Value: "updated"}, // This shouldn't execute
}
body, _ = json.Marshal(testPatches)
req = httptest.NewRequest("PATCH", "/api/collections/test/items/item1", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
})
t.Run("Remove non-existent field", func(t *testing.T) {
patches := []PatchOperation{
{Op: "remove", Path: "/non_existent_field"},
}
body, _ := json.Marshal(patches)
req := httptest.NewRequest("PATCH", "/api/collections/test/items/empty_item", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
})
t.Run("Replace non-existent field", func(t *testing.T) {
patches := []PatchOperation{
{Op: "replace", Path: "/non_existent_field", Value: "value"},
}
body, _ := json.Marshal(patches)
req := httptest.NewRequest("PATCH", "/api/collections/test/items/empty_item2", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
})
}
// BenchmarkJSONPatchOperations benchmarks different JSON Patch operations
func BenchmarkJSONPatchOperations(b *testing.B) {
patcher := &JSONPatcher{}
baseDoc := map[string]interface{}{
"name": "Test Item",
"value": 100,
"active": true,
"tags": []interface{}{"tag1", "tag2", "tag3"},
"metadata": map[string]interface{}{
"version": 1,
"author": "test",
},
}
b.Run("Add Operation", func(b *testing.B) {
patches := []PatchOperation{
{Op: "add", Path: "/description", Value: "Test description"},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Create copy for each iteration
doc := make(map[string]interface{})
for k, v := range baseDoc {
doc[k] = v
}
patcher.ApplyPatches(doc, patches)
}
})
b.Run("Replace Operation", func(b *testing.B) {
patches := []PatchOperation{
{Op: "replace", Path: "/value", Value: 200},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
doc := make(map[string]interface{})
for k, v := range baseDoc {
doc[k] = v
}
patcher.ApplyPatches(doc, patches)
}
})
b.Run("Multiple Operations", func(b *testing.B) {
patches := []PatchOperation{
{Op: "replace", Path: "/name", Value: "Updated Item"},
{Op: "add", Path: "/description", Value: "New description"},
{Op: "replace", Path: "/metadata/version", Value: 2},
{Op: "add", Path: "/metadata/updated", Value: true},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
doc := make(map[string]interface{})
for k, v := range baseDoc {
doc[k] = v
}
patcher.ApplyPatches(doc, patches)
}
})
}