Update
This commit is contained in:
482
api_test.go
482
api_test.go
@@ -9,107 +9,102 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// MockSimpleEventStore for testing API endpoints
|
||||
type MockSimpleEventStore struct {
|
||||
// MockSimpleEventStore for API testing
|
||||
type MockAPIEventStore struct {
|
||||
events []Event
|
||||
items map[string]map[string]interface{}
|
||||
nextSeq int
|
||||
}
|
||||
|
||||
func NewMockSimpleEventStore() *MockSimpleEventStore {
|
||||
return &MockSimpleEventStore{
|
||||
func NewMockAPIEventStore() *MockAPIEventStore {
|
||||
return &MockAPIEventStore{
|
||||
events: []Event{},
|
||||
items: make(map[string]map[string]interface{}),
|
||||
nextSeq: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockSimpleEventStore) GetLatestEvent() (*Event, error) {
|
||||
func (m *MockAPIEventStore) GetLatestEvent() (*Event, error) {
|
||||
if len(m.events) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &m.events[len(m.events)-1], nil
|
||||
}
|
||||
|
||||
func (m *MockSimpleEventStore) ProcessEvent(incomingEvent *Event) (*Event, error) {
|
||||
// Simulate event processing
|
||||
func (m *MockAPIEventStore) ProcessEvent(incomingEvent *Event) (*Event, error) {
|
||||
event := &Event{
|
||||
Seq: m.nextSeq,
|
||||
Hash: "mock_hash_" + string(rune(m.nextSeq)),
|
||||
Hash: "mock_hash_" + string(rune(m.nextSeq+'0')),
|
||||
ItemID: incomingEvent.ItemID,
|
||||
EventID: "mock_event_" + string(rune(m.nextSeq)),
|
||||
EventID: "mock_event_" + string(rune(m.nextSeq+'0')),
|
||||
Collection: incomingEvent.Collection,
|
||||
Patches: incomingEvent.Patches,
|
||||
Operation: incomingEvent.Operation,
|
||||
Path: incomingEvent.Path,
|
||||
Value: incomingEvent.Value,
|
||||
From: incomingEvent.From,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
m.events = append(m.events, *event)
|
||||
m.nextSeq++
|
||||
|
||||
// Apply patches to items
|
||||
key := event.Collection + ":" + event.ItemID
|
||||
if m.items[key] == nil {
|
||||
m.items[key] = map[string]interface{}{
|
||||
"id": event.ItemID,
|
||||
"created_at": event.Timestamp,
|
||||
}
|
||||
// Apply operation to items for testing
|
||||
itemKey := incomingEvent.Collection + ":" + incomingEvent.ItemID
|
||||
if m.items[itemKey] == nil {
|
||||
m.items[itemKey] = map[string]interface{}{"id": incomingEvent.ItemID}
|
||||
}
|
||||
|
||||
patcher := &JSONPatcher{}
|
||||
updated, err := patcher.ApplyPatches(m.items[key], event.Patches)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Simple mock application of JSON Patch operations
|
||||
switch incomingEvent.Operation {
|
||||
case "add", "replace":
|
||||
field := incomingEvent.Path[1:] // remove leading "/"
|
||||
m.items[itemKey][field] = incomingEvent.Value
|
||||
case "remove":
|
||||
field := incomingEvent.Path[1:]
|
||||
delete(m.items[itemKey], field)
|
||||
}
|
||||
|
||||
updated["updated_at"] = event.Timestamp
|
||||
m.items[key] = updated
|
||||
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func (m *MockSimpleEventStore) GetEventsSince(seq int) ([]Event, error) {
|
||||
var result []Event
|
||||
func (m *MockAPIEventStore) GetEventsSince(seq int) ([]Event, error) {
|
||||
var events []Event
|
||||
for _, event := range m.events {
|
||||
if event.Seq > seq {
|
||||
result = append(result, event)
|
||||
events = append(events, event)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (m *MockSimpleEventStore) GetAllItems(collection string) ([]map[string]interface{}, error) {
|
||||
var result []map[string]interface{}
|
||||
func (m *MockAPIEventStore) 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 {
|
||||
// Check if item is not deleted
|
||||
if deletedAt, exists := item["deleted_at"]; !exists || deletedAt == nil {
|
||||
result = append(result, item)
|
||||
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 result, nil
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (m *MockSimpleEventStore) ValidateSync(clientSeq int, clientHash string) (bool, error) {
|
||||
if clientSeq == 0 {
|
||||
return false, nil
|
||||
func (m *MockAPIEventStore) ValidateSync(seq int, hash string) (bool, error) {
|
||||
if len(m.events) == 0 {
|
||||
return seq == 0 && hash == "", nil
|
||||
}
|
||||
|
||||
for _, event := range m.events {
|
||||
if event.Seq == clientSeq {
|
||||
return event.Hash == clientHash, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil // Event not found
|
||||
latest := m.events[len(m.events)-1]
|
||||
return latest.Seq == seq && latest.Hash == hash, nil
|
||||
}
|
||||
|
||||
func setupTestRouter(eventStore *MockSimpleEventStore) http.Handler {
|
||||
func createTestRouter(eventStore *MockAPIEventStore) *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// JSON Patch endpoint
|
||||
// PATCH endpoint for single operations
|
||||
mux.HandleFunc("PATCH /api/collections/{collection}/items/{itemId}", func(w http.ResponseWriter, r *http.Request) {
|
||||
collection := r.PathValue("collection")
|
||||
itemID := r.PathValue("itemId")
|
||||
@@ -119,16 +114,24 @@ func setupTestRouter(eventStore *MockSimpleEventStore) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
var patches []PatchOperation
|
||||
if err := json.NewDecoder(r.Body).Decode(&patches); err != nil {
|
||||
http.Error(w, "Failed to parse JSON Patch data", http.StatusBadRequest)
|
||||
var operation struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
Value string `json:"value"`
|
||||
From string `json:"from"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&operation); err != nil {
|
||||
http.Error(w, "Failed to parse operation data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
incomingEvent := &Event{
|
||||
ItemID: itemID,
|
||||
Collection: collection,
|
||||
Patches: patches,
|
||||
Operation: operation.Op,
|
||||
Path: operation.Path,
|
||||
Value: operation.Value,
|
||||
From: operation.From,
|
||||
}
|
||||
|
||||
processedEvent, err := eventStore.ProcessEvent(incomingEvent)
|
||||
@@ -141,7 +144,7 @@ func setupTestRouter(eventStore *MockSimpleEventStore) http.Handler {
|
||||
json.NewEncoder(w).Encode(processedEvent)
|
||||
})
|
||||
|
||||
// Legacy POST endpoint
|
||||
// POST endpoint for legacy events
|
||||
mux.HandleFunc("POST /api/events", func(w http.ResponseWriter, r *http.Request) {
|
||||
var incomingEvent Event
|
||||
if err := json.NewDecoder(r.Body).Decode(&incomingEvent); err != nil {
|
||||
@@ -149,8 +152,8 @@ func setupTestRouter(eventStore *MockSimpleEventStore) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
if incomingEvent.ItemID == "" || incomingEvent.Collection == "" || len(incomingEvent.Patches) == 0 {
|
||||
http.Error(w, "Missing required fields: item_id, collection, patches", http.StatusBadRequest)
|
||||
if incomingEvent.ItemID == "" || incomingEvent.Collection == "" || incomingEvent.Operation == "" {
|
||||
http.Error(w, "Missing required fields: item_id, collection, operation", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -179,30 +182,17 @@ func setupTestRouter(eventStore *MockSimpleEventStore) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
var response SyncResponse
|
||||
|
||||
if !isValid {
|
||||
events, err := eventStore.GetEventsSince(0)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get events", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
response.Events = events
|
||||
response.FullSync = true
|
||||
} else {
|
||||
events, err := eventStore.GetEventsSince(syncReq.LastSeq)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get events", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
response.Events = events
|
||||
response.FullSync = false
|
||||
events, err := eventStore.GetEventsSince(syncReq.LastSeq)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get events", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
latestEvent, err := eventStore.GetLatestEvent()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get latest event", http.StatusInternalServerError)
|
||||
return
|
||||
latestEvent, _ := eventStore.GetLatestEvent()
|
||||
|
||||
response := SyncResponse{
|
||||
Events: events,
|
||||
FullSync: !isValid,
|
||||
}
|
||||
|
||||
if latestEvent != nil {
|
||||
@@ -217,10 +207,6 @@ func setupTestRouter(eventStore *MockSimpleEventStore) http.Handler {
|
||||
// Get items endpoint
|
||||
mux.HandleFunc("GET /api/items/{collection}", func(w http.ResponseWriter, r *http.Request) {
|
||||
collection := r.PathValue("collection")
|
||||
if collection == "" {
|
||||
http.Error(w, "Collection name required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
items, err := eventStore.GetAllItems(collection)
|
||||
if err != nil {
|
||||
@@ -228,19 +214,25 @@ func setupTestRouter(eventStore *MockSimpleEventStore) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{"items": items}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"items": items,
|
||||
})
|
||||
json.NewEncoder(w).Encode(response)
|
||||
})
|
||||
|
||||
// Get state endpoint
|
||||
mux.HandleFunc("GET /api/state", func(w http.ResponseWriter, r *http.Request) {
|
||||
latestEvent, err := eventStore.GetLatestEvent()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get latest event", http.StatusInternalServerError)
|
||||
return
|
||||
// Health endpoint
|
||||
mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]interface{}{
|
||||
"message": "API is healthy.",
|
||||
"code": 200,
|
||||
"data": map[string]interface{}{},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
})
|
||||
|
||||
// State endpoint
|
||||
mux.HandleFunc("GET /api/state", func(w http.ResponseWriter, r *http.Request) {
|
||||
latestEvent, _ := eventStore.GetLatestEvent()
|
||||
|
||||
response := map[string]interface{}{
|
||||
"seq": 0,
|
||||
@@ -260,117 +252,119 @@ func setupTestRouter(eventStore *MockSimpleEventStore) http.Handler {
|
||||
}
|
||||
|
||||
func TestAPI_PatchEndpoint_CreateItem(t *testing.T) {
|
||||
eventStore := NewMockSimpleEventStore()
|
||||
router := setupTestRouter(eventStore)
|
||||
mockStore := NewMockAPIEventStore()
|
||||
router := createTestRouter(mockStore)
|
||||
|
||||
patches := []PatchOperation{
|
||||
{Op: "add", Path: "/content", Value: "Test Item"},
|
||||
{Op: "add", Path: "/priority", Value: "high"},
|
||||
operation := map[string]string{
|
||||
"op": "add",
|
||||
"path": "/content",
|
||||
"value": "test item content",
|
||||
}
|
||||
body, _ := json.Marshal(operation)
|
||||
|
||||
body, _ := json.Marshal(patches)
|
||||
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/item1", bytes.NewReader(body))
|
||||
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/test123", 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, "item1", response.ItemID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test123", response.ItemID)
|
||||
assert.Equal(t, "shopping_items", response.Collection)
|
||||
assert.Equal(t, "add", response.Operation)
|
||||
assert.Equal(t, "/content", response.Path)
|
||||
assert.Equal(t, "test item content", response.Value)
|
||||
assert.Equal(t, 1, response.Seq)
|
||||
assert.Len(t, response.Patches, 2)
|
||||
}
|
||||
|
||||
func TestAPI_PatchEndpoint_UpdateItem(t *testing.T) {
|
||||
eventStore := NewMockSimpleEventStore()
|
||||
router := setupTestRouter(eventStore)
|
||||
mockStore := NewMockAPIEventStore()
|
||||
router := createTestRouter(mockStore)
|
||||
|
||||
// First create an item
|
||||
createPatches := []PatchOperation{
|
||||
{Op: "add", Path: "/content", Value: "Original Content"},
|
||||
// Create item first
|
||||
createOp := map[string]string{
|
||||
"op": "add",
|
||||
"path": "/content",
|
||||
"value": "original content",
|
||||
}
|
||||
body, _ := json.Marshal(createPatches)
|
||||
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/item1", bytes.NewReader(body))
|
||||
body, _ := json.Marshal(createOp)
|
||||
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/test123", 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 update the item
|
||||
updatePatches := []PatchOperation{
|
||||
{Op: "replace", Path: "/content", Value: "Updated Content"},
|
||||
{Op: "add", Path: "/status", Value: "published"},
|
||||
// Update item
|
||||
updateOp := map[string]string{
|
||||
"op": "replace",
|
||||
"path": "/content",
|
||||
"value": "updated content",
|
||||
}
|
||||
body, _ = json.Marshal(updatePatches)
|
||||
req = httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/item1", bytes.NewReader(body))
|
||||
body, _ = json.Marshal(updateOp)
|
||||
req = httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/test123", 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, "item1", response.ItemID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test123", response.ItemID)
|
||||
assert.Equal(t, "replace", response.Operation)
|
||||
assert.Equal(t, "/content", response.Path)
|
||||
assert.Equal(t, "updated content", response.Value)
|
||||
assert.Equal(t, 2, response.Seq)
|
||||
assert.Len(t, response.Patches, 2)
|
||||
}
|
||||
|
||||
func TestAPI_PatchEndpoint_InvalidData(t *testing.T) {
|
||||
eventStore := NewMockSimpleEventStore()
|
||||
router := setupTestRouter(eventStore)
|
||||
mockStore := NewMockAPIEventStore()
|
||||
router := createTestRouter(mockStore)
|
||||
|
||||
// Test with invalid JSON
|
||||
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/item1", bytes.NewReader([]byte("invalid json")))
|
||||
req := httptest.NewRequest("PATCH", "/api/collections/shopping_items/items/test123", bytes.NewReader([]byte("invalid json")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestAPI_PostEndpoint_LegacyEvent(t *testing.T) {
|
||||
eventStore := NewMockSimpleEventStore()
|
||||
router := setupTestRouter(eventStore)
|
||||
mockStore := NewMockAPIEventStore()
|
||||
router := createTestRouter(mockStore)
|
||||
|
||||
event := Event{
|
||||
ItemID: "item1",
|
||||
ItemID: "test123",
|
||||
Collection: "shopping_items",
|
||||
Patches: []PatchOperation{
|
||||
{Op: "add", Path: "/content", Value: "Test Item"},
|
||||
},
|
||||
Operation: "add",
|
||||
Path: "/content",
|
||||
Value: "test content",
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(event)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/events", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
var response Event
|
||||
err := json.NewDecoder(w.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "item1", response.ItemID)
|
||||
assert.Equal(t, "shopping_items", response.Collection)
|
||||
assert.Equal(t, 1, response.Seq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test123", response.ItemID)
|
||||
assert.Equal(t, "add", response.Operation)
|
||||
}
|
||||
|
||||
func TestAPI_PostEndpoint_MissingFields(t *testing.T) {
|
||||
eventStore := NewMockSimpleEventStore()
|
||||
router := setupTestRouter(eventStore)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
event Event
|
||||
@@ -379,33 +373,41 @@ func TestAPI_PostEndpoint_MissingFields(t *testing.T) {
|
||||
name: "missing item_id",
|
||||
event: Event{
|
||||
Collection: "shopping_items",
|
||||
Patches: []PatchOperation{{Op: "add", Path: "/content", Value: "test"}},
|
||||
Operation: "add",
|
||||
Path: "/content",
|
||||
Value: "test",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing collection",
|
||||
event: Event{
|
||||
ItemID: "item1",
|
||||
Patches: []PatchOperation{{Op: "add", Path: "/content", Value: "test"}},
|
||||
ItemID: "test123",
|
||||
Operation: "add",
|
||||
Path: "/content",
|
||||
Value: "test",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing patches",
|
||||
name: "missing operation",
|
||||
event: Event{
|
||||
ItemID: "item1",
|
||||
ItemID: "test123",
|
||||
Collection: "shopping_items",
|
||||
Patches: []PatchOperation{},
|
||||
Path: "/content",
|
||||
Value: "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockStore := NewMockAPIEventStore()
|
||||
router := createTestRouter(mockStore)
|
||||
|
||||
body, _ := json.Marshal(tt.event)
|
||||
req := httptest.NewRequest("POST", "/api/events", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
@@ -414,138 +416,139 @@ func TestAPI_PostEndpoint_MissingFields(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAPI_SyncEndpoint_FullSync(t *testing.T) {
|
||||
eventStore := NewMockSimpleEventStore()
|
||||
router := setupTestRouter(eventStore)
|
||||
mockStore := NewMockAPIEventStore()
|
||||
router := createTestRouter(mockStore)
|
||||
|
||||
// Create some events first
|
||||
event1 := Event{
|
||||
// Add some events first
|
||||
event1 := &Event{
|
||||
ItemID: "item1",
|
||||
Collection: "shopping_items",
|
||||
Patches: []PatchOperation{{Op: "add", Path: "/content", Value: "Item 1"}},
|
||||
Operation: "add",
|
||||
Path: "/content",
|
||||
Value: "Item 1",
|
||||
}
|
||||
eventStore.ProcessEvent(&event1)
|
||||
mockStore.ProcessEvent(event1)
|
||||
|
||||
event2 := Event{
|
||||
event2 := &Event{
|
||||
ItemID: "item2",
|
||||
Collection: "shopping_items",
|
||||
Patches: []PatchOperation{{Op: "add", Path: "/content", Value: "Item 2"}},
|
||||
Operation: "add",
|
||||
Path: "/content",
|
||||
Value: "Item 2",
|
||||
}
|
||||
eventStore.ProcessEvent(&event2)
|
||||
mockStore.ProcessEvent(event2)
|
||||
|
||||
// Request full sync (client starts with seq 0)
|
||||
// Request sync from beginning
|
||||
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 response SyncResponse
|
||||
err := json.NewDecoder(w.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, response.FullSync)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, response.Events, 2)
|
||||
assert.Equal(t, 2, response.CurrentSeq)
|
||||
assert.NotEmpty(t, response.CurrentHash)
|
||||
}
|
||||
|
||||
func TestAPI_SyncEndpoint_IncrementalSync(t *testing.T) {
|
||||
eventStore := NewMockSimpleEventStore()
|
||||
router := setupTestRouter(eventStore)
|
||||
mockStore := NewMockAPIEventStore()
|
||||
router := createTestRouter(mockStore)
|
||||
|
||||
// Create some events
|
||||
event1 := Event{
|
||||
// Add some events first
|
||||
event1 := &Event{
|
||||
ItemID: "item1",
|
||||
Collection: "shopping_items",
|
||||
Patches: []PatchOperation{{Op: "add", Path: "/content", Value: "Item 1"}},
|
||||
Operation: "add",
|
||||
Path: "/content",
|
||||
Value: "Item 1",
|
||||
}
|
||||
processedEvent1, _ := eventStore.ProcessEvent(&event1)
|
||||
mockStore.ProcessEvent(event1)
|
||||
|
||||
event2 := Event{
|
||||
event2 := &Event{
|
||||
ItemID: "item2",
|
||||
Collection: "shopping_items",
|
||||
Patches: []PatchOperation{{Op: "add", Path: "/content", Value: "Item 2"}},
|
||||
Operation: "add",
|
||||
Path: "/content",
|
||||
Value: "Item 2",
|
||||
}
|
||||
eventStore.ProcessEvent(&event2)
|
||||
mockStore.ProcessEvent(event2)
|
||||
|
||||
// Request incremental sync (client has first event)
|
||||
// Request sync from seq 1
|
||||
syncReq := SyncRequest{
|
||||
LastSeq: processedEvent1.Seq,
|
||||
LastHash: processedEvent1.Hash,
|
||||
LastSeq: 1,
|
||||
LastHash: "mock_hash_1",
|
||||
}
|
||||
|
||||
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 response SyncResponse
|
||||
err := json.NewDecoder(w.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, response.FullSync)
|
||||
assert.Len(t, response.Events, 1) // Only the second event
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, response.Events, 1) // Only event 2
|
||||
assert.Equal(t, 2, response.CurrentSeq)
|
||||
}
|
||||
|
||||
func TestAPI_GetItemsEndpoint(t *testing.T) {
|
||||
eventStore := NewMockSimpleEventStore()
|
||||
router := setupTestRouter(eventStore)
|
||||
mockStore := NewMockAPIEventStore()
|
||||
router := createTestRouter(mockStore)
|
||||
|
||||
// Create some items
|
||||
event1 := Event{
|
||||
// Add some items
|
||||
event1 := &Event{
|
||||
ItemID: "item1",
|
||||
Collection: "shopping_items",
|
||||
Patches: []PatchOperation{{Op: "add", Path: "/content", Value: "Item 1"}},
|
||||
Operation: "add",
|
||||
Path: "/content",
|
||||
Value: "Item 1",
|
||||
}
|
||||
eventStore.ProcessEvent(&event1)
|
||||
mockStore.ProcessEvent(event1)
|
||||
|
||||
event2 := Event{
|
||||
event2 := &Event{
|
||||
ItemID: "item2",
|
||||
Collection: "shopping_items",
|
||||
Patches: []PatchOperation{{Op: "add", Path: "/content", Value: "Item 2"}},
|
||||
Operation: "add",
|
||||
Path: "/content",
|
||||
Value: "Item 2",
|
||||
}
|
||||
eventStore.ProcessEvent(&event2)
|
||||
|
||||
// Delete one item
|
||||
event3 := Event{
|
||||
ItemID: "item2",
|
||||
Collection: "shopping_items",
|
||||
Patches: []PatchOperation{{Op: "add", Path: "/deleted_at", Value: time.Now()}},
|
||||
}
|
||||
eventStore.ProcessEvent(&event3)
|
||||
mockStore.ProcessEvent(event2)
|
||||
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
|
||||
items, ok := response["items"].([]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Len(t, items, 1) // Only non-deleted item
|
||||
require.True(t, ok)
|
||||
assert.Len(t, items, 2)
|
||||
}
|
||||
|
||||
func TestAPI_GetStateEndpoint(t *testing.T) {
|
||||
eventStore := NewMockSimpleEventStore()
|
||||
router := setupTestRouter(eventStore)
|
||||
mockStore := NewMockAPIEventStore()
|
||||
router := createTestRouter(mockStore)
|
||||
|
||||
// Test with no events
|
||||
// Test empty state
|
||||
req := httptest.NewRequest("GET", "/api/state", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
@@ -554,19 +557,21 @@ func TestAPI_GetStateEndpoint(t *testing.T) {
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.NewDecoder(w.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, float64(0), response["seq"])
|
||||
assert.Equal(t, "", response["hash"])
|
||||
|
||||
// Create an event
|
||||
event := Event{
|
||||
// Add an event
|
||||
event := &Event{
|
||||
ItemID: "item1",
|
||||
Collection: "shopping_items",
|
||||
Patches: []PatchOperation{{Op: "add", Path: "/content", Value: "Item 1"}},
|
||||
Operation: "add",
|
||||
Path: "/content",
|
||||
Value: "Item 1",
|
||||
}
|
||||
processedEvent, _ := eventStore.ProcessEvent(&event)
|
||||
mockStore.ProcessEvent(event)
|
||||
|
||||
// Test with events
|
||||
// Test state with event
|
||||
req = httptest.NewRequest("GET", "/api/state", nil)
|
||||
w = httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
@@ -574,34 +579,49 @@ func TestAPI_GetStateEndpoint(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
err = json.NewDecoder(w.Body).Decode(&response)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, float64(processedEvent.Seq), response["seq"])
|
||||
assert.Equal(t, processedEvent.Hash, response["hash"])
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, float64(1), response["seq"])
|
||||
assert.NotEmpty(t, response["hash"])
|
||||
}
|
||||
|
||||
func TestAPI_PathValues(t *testing.T) {
|
||||
eventStore := NewMockSimpleEventStore()
|
||||
router := setupTestRouter(eventStore)
|
||||
mockStore := NewMockAPIEventStore()
|
||||
router := createTestRouter(mockStore)
|
||||
|
||||
// Test with empty collection name (router will handle as empty path value)
|
||||
patches := []PatchOperation{{Op: "add", Path: "/content", Value: "test"}}
|
||||
body, _ := json.Marshal(patches)
|
||||
req := httptest.NewRequest("PATCH", "/api/collections/empty/items/item1", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
method string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "valid patch path",
|
||||
url: "/api/collections/shopping_items/items/test123",
|
||||
method: "PATCH",
|
||||
expectedStatus: http.StatusOK, // Valid operation should succeed
|
||||
},
|
||||
{
|
||||
name: "valid get items path",
|
||||
url: "/api/items/shopping_items",
|
||||
method: "GET",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var req *http.Request
|
||||
if tt.method == "PATCH" {
|
||||
req = httptest.NewRequest(tt.method, tt.url, bytes.NewReader([]byte("{}")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req = httptest.NewRequest(tt.method, tt.url, nil)
|
||||
}
|
||||
|
||||
// This should work fine - we'll test empty values in the handler
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Test empty item ID by using an empty string
|
||||
req = httptest.NewRequest("PATCH", "/api/collections/test/items/", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// This will likely result in 404 due to route not matching
|
||||
assert.True(t, w.Code == http.StatusNotFound || w.Code == http.StatusBadRequest)
|
||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user