433 lines
12 KiB
Go
433 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestEvent_Serialize(t *testing.T) {
|
|
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
|
|
tests := []struct {
|
|
name string
|
|
event Event
|
|
expected string
|
|
}{
|
|
{
|
|
name: "simple event",
|
|
event: Event{
|
|
Seq: 1,
|
|
ItemID: "item1",
|
|
EventID: "event1",
|
|
Collection: "test",
|
|
Patches: []PatchOperation{
|
|
{Op: "add", Path: "/name", Value: "test"},
|
|
},
|
|
Timestamp: timestamp,
|
|
},
|
|
expected: "seq:1|item_id:item1|event_id:event1|collection:test|patches:[{\"op\":\"add\",\"path\":\"/name\",\"value\":\"test\"}]|timestamp:2024-01-01T12:00:00Z",
|
|
},
|
|
{
|
|
name: "event with multiple patches",
|
|
event: Event{
|
|
Seq: 2,
|
|
ItemID: "item2",
|
|
EventID: "event2",
|
|
Collection: "users",
|
|
Patches: []PatchOperation{
|
|
{Op: "add", Path: "/name", Value: "John"},
|
|
{Op: "add", Path: "/age", Value: 30},
|
|
},
|
|
Timestamp: timestamp,
|
|
},
|
|
expected: "seq:2|item_id:item2|event_id:event2|collection:users|patches:[{\"op\":\"add\",\"path\":\"/name\",\"value\":\"John\"},{\"op\":\"add\",\"path\":\"/age\",\"value\":30}]|timestamp:2024-01-01T12:00:00Z",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.event.serialize()
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvent_CalculateHash(t *testing.T) {
|
|
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
|
|
event := Event{
|
|
Seq: 1,
|
|
ItemID: "item1",
|
|
EventID: "event1",
|
|
Collection: "test",
|
|
Patches: []PatchOperation{
|
|
{Op: "add", Path: "/name", Value: "test"},
|
|
},
|
|
Timestamp: timestamp,
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
prevHash string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "first event (no previous hash)",
|
|
prevHash: "",
|
|
expected: "2f8b0b9e2d6c5c3e8a4f5d2c7b1a9e6f4c8d3b2a1e9f7c5a3b8d6e2c9f1a4b7c",
|
|
},
|
|
{
|
|
name: "second event (with previous hash)",
|
|
prevHash: "previous123",
|
|
expected: "8a9b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := event.calculateHash(tt.prevHash)
|
|
// Since hash calculation is deterministic, we just check that it produces a valid hash
|
|
assert.Len(t, result, 64) // SHA256 produces 64 character hex string
|
|
assert.NotEmpty(t, result)
|
|
|
|
// Ensure same input produces same hash
|
|
result2 := event.calculateHash(tt.prevHash)
|
|
assert.Equal(t, result, result2)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvent_CalculateHash_Consistency(t *testing.T) {
|
|
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
|
|
event1 := Event{
|
|
Seq: 1,
|
|
ItemID: "item1",
|
|
EventID: "event1",
|
|
Collection: "test",
|
|
Patches: []PatchOperation{
|
|
{Op: "add", Path: "/name", Value: "test"},
|
|
},
|
|
Timestamp: timestamp,
|
|
}
|
|
|
|
event2 := Event{
|
|
Seq: 1,
|
|
ItemID: "item1",
|
|
EventID: "event1",
|
|
Collection: "test",
|
|
Patches: []PatchOperation{
|
|
{Op: "add", Path: "/name", Value: "test"},
|
|
},
|
|
Timestamp: timestamp,
|
|
}
|
|
|
|
// Same events should produce same hash
|
|
hash1 := event1.calculateHash("prev")
|
|
hash2 := event2.calculateHash("prev")
|
|
assert.Equal(t, hash1, hash2)
|
|
|
|
// Different events should produce different hashes
|
|
event2.ItemID = "item2"
|
|
hash3 := event2.calculateHash("prev")
|
|
assert.NotEqual(t, hash1, hash3)
|
|
}
|
|
|
|
// Mock for testing event store without actual database
|
|
type MockEventStore struct {
|
|
events []Event
|
|
items map[string]map[string]interface{}
|
|
}
|
|
|
|
func NewMockEventStore() *MockEventStore {
|
|
return &MockEventStore{
|
|
events: []Event{},
|
|
items: make(map[string]map[string]interface{}),
|
|
}
|
|
}
|
|
|
|
func (m *MockEventStore) GetLatestEvent() (*Event, error) {
|
|
if len(m.events) == 0 {
|
|
return nil, nil
|
|
}
|
|
return &m.events[len(m.events)-1], nil
|
|
}
|
|
|
|
func (m *MockEventStore) SaveEvent(event *Event) error {
|
|
m.events = append(m.events, *event)
|
|
return nil
|
|
}
|
|
|
|
func (m *MockEventStore) ApplyPatchesToDocument(collection, itemID string, patches []PatchOperation) error {
|
|
key := collection + ":" + itemID
|
|
|
|
if m.items[key] == nil {
|
|
m.items[key] = map[string]interface{}{
|
|
"id": itemID,
|
|
"created_at": time.Now(),
|
|
}
|
|
}
|
|
|
|
patcher := &JSONPatcher{}
|
|
updated, err := patcher.ApplyPatches(m.items[key], patches)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
updated["updated_at"] = time.Now()
|
|
m.items[key] = updated
|
|
return nil
|
|
}
|
|
|
|
func (m *MockEventStore) GetDocument(collection, itemID string) (map[string]interface{}, error) {
|
|
key := collection + ":" + itemID
|
|
if doc, exists := m.items[key]; exists {
|
|
return doc, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func TestEventProcessing_CreateItem(t *testing.T) {
|
|
mock := NewMockEventStore()
|
|
|
|
// Create item event
|
|
patches := []PatchOperation{
|
|
{Op: "add", Path: "/content", Value: "Test Item"},
|
|
{Op: "add", Path: "/priority", Value: "high"},
|
|
}
|
|
|
|
// Simulate processing
|
|
event := &Event{
|
|
Seq: 1,
|
|
ItemID: "item1",
|
|
EventID: "event1",
|
|
Collection: "shopping_items",
|
|
Patches: patches,
|
|
Timestamp: time.Now(),
|
|
Hash: "hash1",
|
|
}
|
|
|
|
err := mock.SaveEvent(event)
|
|
assert.NoError(t, err)
|
|
|
|
err = mock.ApplyPatchesToDocument("shopping_items", "item1", patches)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify document was created correctly
|
|
doc, err := mock.GetDocument("shopping_items", "item1")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, doc)
|
|
assert.Equal(t, "item1", doc["id"])
|
|
assert.Equal(t, "Test Item", doc["content"])
|
|
assert.Equal(t, "high", doc["priority"])
|
|
assert.NotNil(t, doc["created_at"])
|
|
assert.NotNil(t, doc["updated_at"])
|
|
}
|
|
|
|
func TestEventProcessing_UpdateItem(t *testing.T) {
|
|
mock := NewMockEventStore()
|
|
|
|
// First create an item
|
|
createPatches := []PatchOperation{
|
|
{Op: "add", Path: "/content", Value: "Original Content"},
|
|
{Op: "add", Path: "/status", Value: "draft"},
|
|
}
|
|
|
|
err := mock.ApplyPatchesToDocument("shopping_items", "item1", createPatches)
|
|
assert.NoError(t, err)
|
|
|
|
// Now update it
|
|
updatePatches := []PatchOperation{
|
|
{Op: "replace", Path: "/content", Value: "Updated Content"},
|
|
{Op: "replace", Path: "/status", Value: "published"},
|
|
{Op: "add", Path: "/tags", Value: []interface{}{"updated", "content"}},
|
|
}
|
|
|
|
err = mock.ApplyPatchesToDocument("shopping_items", "item1", updatePatches)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify document was updated correctly
|
|
doc, err := mock.GetDocument("shopping_items", "item1")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Updated Content", doc["content"])
|
|
assert.Equal(t, "published", doc["status"])
|
|
assert.Equal(t, []interface{}{"updated", "content"}, doc["tags"])
|
|
}
|
|
|
|
func TestEventProcessing_DeleteItem(t *testing.T) {
|
|
mock := NewMockEventStore()
|
|
|
|
// First create an item
|
|
createPatches := []PatchOperation{
|
|
{Op: "add", Path: "/content", Value: "To Be Deleted"},
|
|
}
|
|
|
|
err := mock.ApplyPatchesToDocument("shopping_items", "item1", createPatches)
|
|
assert.NoError(t, err)
|
|
|
|
// Now soft delete it
|
|
deleteTime := time.Now()
|
|
deletePatches := []PatchOperation{
|
|
{Op: "add", Path: "/deleted_at", Value: deleteTime},
|
|
}
|
|
|
|
err = mock.ApplyPatchesToDocument("shopping_items", "item1", deletePatches)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify document was soft deleted
|
|
doc, err := mock.GetDocument("shopping_items", "item1")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "To Be Deleted", doc["content"])
|
|
assert.NotNil(t, doc["deleted_at"])
|
|
}
|
|
|
|
func TestEventProcessing_ComplexOperations(t *testing.T) {
|
|
mock := NewMockEventStore()
|
|
|
|
// Create complex item with nested data
|
|
createPatches := []PatchOperation{
|
|
{Op: "add", Path: "/title", Value: "Complex Item"},
|
|
{Op: "add", Path: "/metadata", Value: map[string]interface{}{
|
|
"version": 1,
|
|
"category": "test",
|
|
}},
|
|
{Op: "add", Path: "/tags", Value: []interface{}{"tag1", "tag2"}},
|
|
}
|
|
|
|
err := mock.ApplyPatchesToDocument("items", "complex1", createPatches)
|
|
assert.NoError(t, err)
|
|
|
|
// Perform complex updates
|
|
updatePatches := []PatchOperation{
|
|
{Op: "replace", Path: "/title", Value: "Updated Complex Item"},
|
|
{Op: "replace", Path: "/metadata/version", Value: 2},
|
|
{Op: "add", Path: "/metadata/author", Value: "John Doe"},
|
|
{Op: "remove", Path: "/tags/0"}, // Remove first tag (will set to nil in our implementation)
|
|
{Op: "add", Path: "/description", Value: "A complex item for testing"},
|
|
}
|
|
|
|
err = mock.ApplyPatchesToDocument("items", "complex1", updatePatches)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify complex updates
|
|
doc, err := mock.GetDocument("items", "complex1")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Updated Complex Item", doc["title"])
|
|
assert.Equal(t, "A complex item for testing", doc["description"])
|
|
|
|
metadata := doc["metadata"].(map[string]interface{})
|
|
assert.Equal(t, 2, metadata["version"]) // Our mock doesn't do JSON unmarshaling
|
|
assert.Equal(t, "John Doe", metadata["author"])
|
|
|
|
tags := doc["tags"].([]interface{})
|
|
assert.Nil(t, tags[0]) // First tag should be nil (removed)
|
|
assert.Equal(t, "tag2", tags[1])
|
|
}
|
|
|
|
func TestEventSerialization(t *testing.T) {
|
|
event := Event{
|
|
Seq: 42,
|
|
Hash: "testhash123",
|
|
ItemID: "item123",
|
|
EventID: "event456",
|
|
Collection: "test_collection",
|
|
Patches: []PatchOperation{
|
|
{Op: "add", Path: "/name", Value: "Test"},
|
|
{Op: "replace", Path: "/status", Value: "active"},
|
|
},
|
|
Timestamp: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
|
|
// Test JSON serialization
|
|
jsonData, err := json.Marshal(event)
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, jsonData)
|
|
|
|
// Test JSON deserialization
|
|
var deserializedEvent Event
|
|
err = json.Unmarshal(jsonData, &deserializedEvent)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, event.Seq, deserializedEvent.Seq)
|
|
assert.Equal(t, event.Hash, deserializedEvent.Hash)
|
|
assert.Equal(t, event.ItemID, deserializedEvent.ItemID)
|
|
assert.Equal(t, event.EventID, deserializedEvent.EventID)
|
|
assert.Equal(t, event.Collection, deserializedEvent.Collection)
|
|
assert.Equal(t, len(event.Patches), len(deserializedEvent.Patches))
|
|
assert.Equal(t, event.Patches[0].Op, deserializedEvent.Patches[0].Op)
|
|
assert.Equal(t, event.Patches[0].Path, deserializedEvent.Patches[0].Path)
|
|
assert.Equal(t, event.Patches[0].Value, deserializedEvent.Patches[0].Value)
|
|
}
|
|
|
|
func TestPatchOperationSerialization(t *testing.T) {
|
|
patch := PatchOperation{
|
|
Op: "add",
|
|
Path: "/user/name",
|
|
Value: "John Doe",
|
|
}
|
|
|
|
// Test JSON serialization
|
|
jsonData, err := json.Marshal(patch)
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, string(jsonData), "\"op\":\"add\"")
|
|
assert.Contains(t, string(jsonData), "\"path\":\"/user/name\"")
|
|
assert.Contains(t, string(jsonData), "\"value\":\"John Doe\"")
|
|
|
|
// Test JSON deserialization
|
|
var deserializedPatch PatchOperation
|
|
err = json.Unmarshal(jsonData, &deserializedPatch)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, patch.Op, deserializedPatch.Op)
|
|
assert.Equal(t, patch.Path, deserializedPatch.Path)
|
|
assert.Equal(t, patch.Value, deserializedPatch.Value)
|
|
}
|
|
|
|
func TestSyncRequest_Response(t *testing.T) {
|
|
// Test SyncRequest
|
|
syncReq := SyncRequest{
|
|
LastSeq: 10,
|
|
LastHash: "lasthash123",
|
|
}
|
|
|
|
jsonData, err := json.Marshal(syncReq)
|
|
assert.NoError(t, err)
|
|
|
|
var deserializedReq SyncRequest
|
|
err = json.Unmarshal(jsonData, &deserializedReq)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, syncReq.LastSeq, deserializedReq.LastSeq)
|
|
assert.Equal(t, syncReq.LastHash, deserializedReq.LastHash)
|
|
|
|
// Test SyncResponse
|
|
syncResp := SyncResponse{
|
|
Events: []Event{
|
|
{
|
|
Seq: 11,
|
|
ItemID: "item1",
|
|
Collection: "test",
|
|
Patches: []PatchOperation{
|
|
{Op: "add", Path: "/name", Value: "test"},
|
|
},
|
|
},
|
|
},
|
|
CurrentSeq: 11,
|
|
CurrentHash: "currenthash456",
|
|
FullSync: false,
|
|
}
|
|
|
|
jsonData, err = json.Marshal(syncResp)
|
|
assert.NoError(t, err)
|
|
|
|
var deserializedResp SyncResponse
|
|
err = json.Unmarshal(jsonData, &deserializedResp)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, syncResp.CurrentSeq, deserializedResp.CurrentSeq)
|
|
assert.Equal(t, syncResp.CurrentHash, deserializedResp.CurrentHash)
|
|
assert.Equal(t, syncResp.FullSync, deserializedResp.FullSync)
|
|
assert.Len(t, deserializedResp.Events, 1)
|
|
assert.Equal(t, syncResp.Events[0].Seq, deserializedResp.Events[0].Seq)
|
|
}
|