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) } }) }