sync_client_test.go

  1package ygo_test
  2
  3import (
  4	"context"
  5	"testing"
  6	"time"
  7
  8	"git.kilimanjaro.io/ygo"
  9)
 10
 11func TestNewSyncClient(t *testing.T) {
 12	// Test without endpoint
 13	doc, _ := ygo.NewDoc()
 14	defer doc.Destroy()
 15
 16	_, err := ygo.NewSyncClient(doc)
 17	if err == nil {
 18		t.Error("expected error without endpoint")
 19	}
 20
 21	// Test with endpoint
 22	client, err := ygo.NewSyncClient(doc, ygo.WithSyncEndpoint("ws://test"))
 23	if err != nil {
 24		t.Fatalf("failed to create client: %v", err)
 25	}
 26
 27	// Verify not connected yet
 28	if client.Status() != ygo.SyncStatusDisconnected {
 29		t.Errorf("expected disconnected, got %v", client.Status())
 30	}
 31
 32	// Stats should show disconnected
 33	stats := client.Stats()
 34	if stats.Status != ygo.SyncStatusDisconnected {
 35		t.Errorf("expected disconnected in stats, got %v", stats.Status)
 36	}
 37}
 38
 39func TestSyncClientEventHooks(t *testing.T) {
 40	doc, _ := ygo.NewDoc()
 41	defer doc.Destroy()
 42
 43	client, _ := ygo.NewSyncClient(doc, ygo.WithSyncEndpoint("ws://test"))
 44
 45	// Test SyncHook (non-context) callbacks
 46	connectCalled := false
 47	disconnectCalled := false
 48	updateCalled := false
 49
 50	client.OnConnect(func(doc *ygo.Doc, stats ygo.SyncStats) error {
 51		connectCalled = true
 52		return nil
 53	})
 54
 55	client.OnDisconnect(func(doc *ygo.Doc, stats ygo.SyncStats, reason ygo.DisconnectReason) error {
 56		disconnectCalled = true
 57		return nil
 58	})
 59
 60	client.OnUpdate(func(doc *ygo.Doc, stats ygo.SyncStats) error {
 61		updateCalled = true
 62		return nil
 63	})
 64
 65	// Callbacks are set but not triggered yet (since not connected)
 66	if connectCalled {
 67		t.Error("connect should not be called yet")
 68	}
 69	if disconnectCalled {
 70		t.Error("disconnect should not be called yet")
 71	}
 72	if updateCalled {
 73		t.Error("update should not be called yet")
 74	}
 75}
 76
 77func TestSyncClientEventCtxHooks(t *testing.T) {
 78	doc, _ := ygo.NewDoc()
 79	defer doc.Destroy()
 80
 81	client, _ := ygo.NewSyncClient(doc, ygo.WithSyncEndpoint("ws://test"))
 82
 83	// Test SyncHookCtx (context-aware) callbacks
 84	connectCtxCalled := false
 85	disconnectCtxCalled := false
 86	updateCtxCalled := false
 87
 88	client.OnConnectCtx(func(ctx context.Context, doc *ygo.Doc, stats ygo.SyncStats) error {
 89		connectCtxCalled = true
 90		return nil
 91	})
 92
 93	client.OnDisconnectCtx(func(ctx context.Context, doc *ygo.Doc, stats ygo.SyncStats, reason ygo.DisconnectReason) error {
 94		disconnectCtxCalled = true
 95		return nil
 96	})
 97
 98	client.OnUpdateCtx(func(ctx context.Context, doc *ygo.Doc, stats ygo.SyncStats) error {
 99		updateCtxCalled = true
100		return nil
101	})
102
103	// Callbacks are set but not triggered yet
104	if connectCtxCalled {
105		t.Error("connect ctx should not be called yet")
106	}
107	if disconnectCtxCalled {
108		t.Error("disconnect ctx should not be called yet")
109	}
110	if updateCtxCalled {
111		t.Error("update ctx should not be called yet")
112	}
113}
114
115func TestSyncClientFlushNotConnected(t *testing.T) {
116	doc, _ := ygo.NewDoc()
117	defer doc.Destroy()
118
119	client, _ := ygo.NewSyncClient(doc, ygo.WithSyncEndpoint("ws://test"))
120
121	// Flush should fail when not connected
122	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
123	defer cancel()
124
125	err := client.Flush(ctx)
126	if err == nil {
127		t.Error("expected error when flushing while disconnected")
128	}
129}
130
131func TestSyncClientCloseNotConnected(t *testing.T) {
132	doc, _ := ygo.NewDoc()
133	defer doc.Destroy()
134
135	client, _ := ygo.NewSyncClient(doc, ygo.WithSyncEndpoint("ws://test"))
136
137	// Close should succeed even when not connected (no-op)
138	err := client.Close()
139	if err != nil {
140		t.Errorf("expected no error when closing disconnected client, got %v", err)
141	}
142}
143
144func TestSyncStatsStruct(t *testing.T) {
145	// Test that SyncStats fields exist and are accessible
146	stats := ygo.SyncStats{
147		PeerCount:     5,
148		LastUpdate:    time.Now(),
149		PendingUpdate: true,
150		Status:        ygo.SyncStatusConnected,
151	}
152
153	if stats.PeerCount != 5 {
154		t.Errorf("expected PeerCount 5, got %d", stats.PeerCount)
155	}
156	if stats.LastUpdate.IsZero() {
157		t.Error("expected LastUpdate to be set")
158	}
159	if !stats.PendingUpdate {
160		t.Error("expected PendingUpdate to be true")
161	}
162	if stats.Status != ygo.SyncStatusConnected {
163		t.Errorf("expected Status Connected, got %v", stats.Status)
164	}
165}
166
167func TestSyncClientDisconnectReasonClientInitiated(t *testing.T) {
168	doc, _ := ygo.NewDoc()
169	defer doc.Destroy()
170
171	client, _ := ygo.NewSyncClient(doc, ygo.WithSyncEndpoint("ws://test"))
172
173	var reasonReceived bool
174
175	client.OnDisconnect(func(d *ygo.Doc, stats ygo.SyncStats, reason ygo.DisconnectReason) error {
176		_ = reason // silence unused warning
177		reasonReceived = true
178		return nil
179	})
180
181	// Close without connecting - callback should not be called since we were never connected
182	client.Close()
183
184	// Wait a bit to ensure callback is not invoked
185	time.Sleep(100 * time.Millisecond)
186
187	if reasonReceived {
188		t.Error("disconnect callback should not be called when client was never connected")
189	}
190
191	// Note: Full ClientInitiated disconnect testing requires integration tests
192	// with a real server connection that is then closed.
193}