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}