graph-follows_test.go raw
1 //go:build !(js && wasm)
2
3 package database
4
5 import (
6 "context"
7 "testing"
8
9 "next.orly.dev/pkg/nostr/encoders/event"
10 "next.orly.dev/pkg/nostr/encoders/hex"
11 "next.orly.dev/pkg/nostr/encoders/tag"
12 )
13
14 func TestTraverseFollows(t *testing.T) {
15 ctx, cancel := context.WithCancel(context.Background())
16 defer cancel()
17
18 db, err := New(ctx, cancel, t.TempDir(), "info")
19 if err != nil {
20 t.Fatalf("Failed to create database: %v", err)
21 }
22 defer db.Close()
23
24 // Create a simple follow graph:
25 // Alice -> Bob, Carol
26 // Bob -> David, Eve
27 // Carol -> Eve, Frank
28 //
29 // Expected depth 1 from Alice: Bob, Carol
30 // Expected depth 2 from Alice: David, Eve, Frank (Eve deduplicated)
31
32 alice, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000001")
33 bob, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000002")
34 carol, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000003")
35 david, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000004")
36 eve, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000005")
37 frank, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000006")
38
39 // Create Alice's follow list (kind 3)
40 aliceContactID := make([]byte, 32)
41 aliceContactID[0] = 0x10
42 aliceContactSig := make([]byte, 64)
43 aliceContactSig[0] = 0x10
44 aliceContact := &event.E{
45 ID: aliceContactID,
46 Pubkey: alice,
47 CreatedAt: 1234567890,
48 Kind: 3,
49 Content: []byte(""),
50 Sig: aliceContactSig,
51 Tags: tag.NewS(
52 tag.NewFromAny("p", hex.Enc(bob)),
53 tag.NewFromAny("p", hex.Enc(carol)),
54 ),
55 }
56 _, err = db.SaveEvent(ctx, aliceContact)
57 if err != nil {
58 t.Fatalf("Failed to save Alice's contact list: %v", err)
59 }
60
61 // Create Bob's follow list
62 bobContactID := make([]byte, 32)
63 bobContactID[0] = 0x20
64 bobContactSig := make([]byte, 64)
65 bobContactSig[0] = 0x20
66 bobContact := &event.E{
67 ID: bobContactID,
68 Pubkey: bob,
69 CreatedAt: 1234567891,
70 Kind: 3,
71 Content: []byte(""),
72 Sig: bobContactSig,
73 Tags: tag.NewS(
74 tag.NewFromAny("p", hex.Enc(david)),
75 tag.NewFromAny("p", hex.Enc(eve)),
76 ),
77 }
78 _, err = db.SaveEvent(ctx, bobContact)
79 if err != nil {
80 t.Fatalf("Failed to save Bob's contact list: %v", err)
81 }
82
83 // Create Carol's follow list
84 carolContactID := make([]byte, 32)
85 carolContactID[0] = 0x30
86 carolContactSig := make([]byte, 64)
87 carolContactSig[0] = 0x30
88 carolContact := &event.E{
89 ID: carolContactID,
90 Pubkey: carol,
91 CreatedAt: 1234567892,
92 Kind: 3,
93 Content: []byte(""),
94 Sig: carolContactSig,
95 Tags: tag.NewS(
96 tag.NewFromAny("p", hex.Enc(eve)),
97 tag.NewFromAny("p", hex.Enc(frank)),
98 ),
99 }
100 _, err = db.SaveEvent(ctx, carolContact)
101 if err != nil {
102 t.Fatalf("Failed to save Carol's contact list: %v", err)
103 }
104
105 // Traverse follows from Alice with depth 2
106 result, err := db.TraverseFollows(alice, 2)
107 if err != nil {
108 t.Fatalf("TraverseFollows failed: %v", err)
109 }
110
111 // Check depth 1: should have Bob and Carol
112 depth1 := result.GetPubkeysAtDepth(1)
113 if len(depth1) != 2 {
114 t.Errorf("Expected 2 pubkeys at depth 1, got %d", len(depth1))
115 }
116
117 depth1Set := make(map[string]bool)
118 for _, pk := range depth1 {
119 depth1Set[pk] = true
120 }
121 if !depth1Set[hex.Enc(bob)] {
122 t.Error("Bob should be at depth 1")
123 }
124 if !depth1Set[hex.Enc(carol)] {
125 t.Error("Carol should be at depth 1")
126 }
127
128 // Check depth 2: should have David, Eve, Frank (Eve deduplicated)
129 depth2 := result.GetPubkeysAtDepth(2)
130 if len(depth2) != 3 {
131 t.Errorf("Expected 3 pubkeys at depth 2, got %d: %v", len(depth2), depth2)
132 }
133
134 depth2Set := make(map[string]bool)
135 for _, pk := range depth2 {
136 depth2Set[pk] = true
137 }
138 if !depth2Set[hex.Enc(david)] {
139 t.Error("David should be at depth 2")
140 }
141 if !depth2Set[hex.Enc(eve)] {
142 t.Error("Eve should be at depth 2")
143 }
144 if !depth2Set[hex.Enc(frank)] {
145 t.Error("Frank should be at depth 2")
146 }
147
148 // Verify total count
149 if result.TotalPubkeys != 5 {
150 t.Errorf("Expected 5 total pubkeys, got %d", result.TotalPubkeys)
151 }
152
153 // Verify ToDepthArrays output
154 arrays := result.ToDepthArrays()
155 if len(arrays) != 2 {
156 t.Errorf("Expected 2 depth arrays, got %d", len(arrays))
157 }
158 if len(arrays[0]) != 2 {
159 t.Errorf("Expected 2 pubkeys in depth 1 array, got %d", len(arrays[0]))
160 }
161 if len(arrays[1]) != 3 {
162 t.Errorf("Expected 3 pubkeys in depth 2 array, got %d", len(arrays[1]))
163 }
164 }
165
166 func TestTraverseFollowsDepth1(t *testing.T) {
167 ctx, cancel := context.WithCancel(context.Background())
168 defer cancel()
169
170 db, err := New(ctx, cancel, t.TempDir(), "info")
171 if err != nil {
172 t.Fatalf("Failed to create database: %v", err)
173 }
174 defer db.Close()
175
176 alice, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000001")
177 bob, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000002")
178 carol, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000003")
179
180 // Create Alice's follow list
181 aliceContactID := make([]byte, 32)
182 aliceContactID[0] = 0x10
183 aliceContactSig := make([]byte, 64)
184 aliceContactSig[0] = 0x10
185 aliceContact := &event.E{
186 ID: aliceContactID,
187 Pubkey: alice,
188 CreatedAt: 1234567890,
189 Kind: 3,
190 Content: []byte(""),
191 Sig: aliceContactSig,
192 Tags: tag.NewS(
193 tag.NewFromAny("p", hex.Enc(bob)),
194 tag.NewFromAny("p", hex.Enc(carol)),
195 ),
196 }
197 _, err = db.SaveEvent(ctx, aliceContact)
198 if err != nil {
199 t.Fatalf("Failed to save contact list: %v", err)
200 }
201
202 // Traverse with depth 1 only
203 result, err := db.TraverseFollows(alice, 1)
204 if err != nil {
205 t.Fatalf("TraverseFollows failed: %v", err)
206 }
207
208 if result.TotalPubkeys != 2 {
209 t.Errorf("Expected 2 pubkeys, got %d", result.TotalPubkeys)
210 }
211
212 arrays := result.ToDepthArrays()
213 if len(arrays) != 1 {
214 t.Errorf("Expected 1 depth array for depth 1 query, got %d", len(arrays))
215 }
216 }
217
218 func TestTraverseFollowersBasic(t *testing.T) {
219 ctx, cancel := context.WithCancel(context.Background())
220 defer cancel()
221
222 db, err := New(ctx, cancel, t.TempDir(), "info")
223 if err != nil {
224 t.Fatalf("Failed to create database: %v", err)
225 }
226 defer db.Close()
227
228 // Create scenario: Bob and Carol follow Alice
229 alice, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000001")
230 bob, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000002")
231 carol, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000003")
232
233 // Bob's contact list includes Alice
234 bobContactID := make([]byte, 32)
235 bobContactID[0] = 0x10
236 bobContactSig := make([]byte, 64)
237 bobContactSig[0] = 0x10
238 bobContact := &event.E{
239 ID: bobContactID,
240 Pubkey: bob,
241 CreatedAt: 1234567890,
242 Kind: 3,
243 Content: []byte(""),
244 Sig: bobContactSig,
245 Tags: tag.NewS(
246 tag.NewFromAny("p", hex.Enc(alice)),
247 ),
248 }
249 _, err = db.SaveEvent(ctx, bobContact)
250 if err != nil {
251 t.Fatalf("Failed to save Bob's contact list: %v", err)
252 }
253
254 // Carol's contact list includes Alice
255 carolContactID := make([]byte, 32)
256 carolContactID[0] = 0x20
257 carolContactSig := make([]byte, 64)
258 carolContactSig[0] = 0x20
259 carolContact := &event.E{
260 ID: carolContactID,
261 Pubkey: carol,
262 CreatedAt: 1234567891,
263 Kind: 3,
264 Content: []byte(""),
265 Sig: carolContactSig,
266 Tags: tag.NewS(
267 tag.NewFromAny("p", hex.Enc(alice)),
268 ),
269 }
270 _, err = db.SaveEvent(ctx, carolContact)
271 if err != nil {
272 t.Fatalf("Failed to save Carol's contact list: %v", err)
273 }
274
275 // Find Alice's followers
276 result, err := db.TraverseFollowers(alice, 1)
277 if err != nil {
278 t.Fatalf("TraverseFollowers failed: %v", err)
279 }
280
281 if result.TotalPubkeys != 2 {
282 t.Errorf("Expected 2 followers, got %d", result.TotalPubkeys)
283 }
284
285 followers := result.GetPubkeysAtDepth(1)
286 followerSet := make(map[string]bool)
287 for _, pk := range followers {
288 followerSet[pk] = true
289 }
290 if !followerSet[hex.Enc(bob)] {
291 t.Error("Bob should be a follower")
292 }
293 if !followerSet[hex.Enc(carol)] {
294 t.Error("Carol should be a follower")
295 }
296 }
297
298 func TestTraverseFollowsNonExistent(t *testing.T) {
299 ctx, cancel := context.WithCancel(context.Background())
300 defer cancel()
301
302 db, err := New(ctx, cancel, t.TempDir(), "info")
303 if err != nil {
304 t.Fatalf("Failed to create database: %v", err)
305 }
306 defer db.Close()
307
308 // Try to traverse from a pubkey that doesn't exist
309 nonExistent, _ := hex.Dec("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
310 result, err := db.TraverseFollows(nonExistent, 2)
311 if err != nil {
312 t.Fatalf("TraverseFollows should not error for non-existent pubkey: %v", err)
313 }
314
315 if result.TotalPubkeys != 0 {
316 t.Errorf("Expected 0 pubkeys for non-existent seed, got %d", result.TotalPubkeys)
317 }
318 }
319