etag-graph_test.go raw
1 //go:build !(js && wasm)
2
3 package database
4
5 import (
6 "bytes"
7 "context"
8 "testing"
9
10 "github.com/dgraph-io/badger/v4"
11 "next.orly.dev/pkg/database/indexes"
12 "next.orly.dev/pkg/database/indexes/types"
13 "next.orly.dev/pkg/nostr/encoders/event"
14 "next.orly.dev/pkg/nostr/encoders/hex"
15 "next.orly.dev/pkg/nostr/encoders/tag"
16 )
17
18 func TestETagGraphEdgeCreation(t *testing.T) {
19 ctx, cancel := context.WithCancel(context.Background())
20 defer cancel()
21
22 db, err := New(ctx, cancel, t.TempDir(), "info")
23 if err != nil {
24 t.Fatalf("Failed to create database: %v", err)
25 }
26 defer db.Close()
27
28 // Create a parent event (the post being replied to)
29 parentPubkey, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000001")
30 parentID := make([]byte, 32)
31 parentID[0] = 0x10
32 parentSig := make([]byte, 64)
33 parentSig[0] = 0x10
34
35 parentEvent := &event.E{
36 ID: parentID,
37 Pubkey: parentPubkey,
38 CreatedAt: 1234567890,
39 Kind: 1,
40 Content: []byte("This is the parent post"),
41 Sig: parentSig,
42 Tags: &tag.S{},
43 }
44 _, err = db.SaveEvent(ctx, parentEvent)
45 if err != nil {
46 t.Fatalf("Failed to save parent event: %v", err)
47 }
48
49 // Create a reply event with e-tag pointing to parent
50 replyPubkey, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000002")
51 replyID := make([]byte, 32)
52 replyID[0] = 0x20
53 replySig := make([]byte, 64)
54 replySig[0] = 0x20
55
56 replyEvent := &event.E{
57 ID: replyID,
58 Pubkey: replyPubkey,
59 CreatedAt: 1234567891,
60 Kind: 1,
61 Content: []byte("This is a reply"),
62 Sig: replySig,
63 Tags: tag.NewS(
64 tag.NewFromAny("e", hex.Enc(parentID)),
65 ),
66 }
67 _, err = db.SaveEvent(ctx, replyEvent)
68 if err != nil {
69 t.Fatalf("Failed to save reply event: %v", err)
70 }
71
72 // Get serials for both events
73 parentSerial, err := db.GetSerialById(parentID)
74 if err != nil {
75 t.Fatalf("Failed to get parent serial: %v", err)
76 }
77 replySerial, err := db.GetSerialById(replyID)
78 if err != nil {
79 t.Fatalf("Failed to get reply serial: %v", err)
80 }
81
82 t.Logf("Parent serial: %d, Reply serial: %d", parentSerial.Get(), replySerial.Get())
83
84 // Verify forward edge exists (reply -> parent)
85 forwardFound := false
86 prefix := []byte(indexes.EventEventGraphPrefix)
87
88 err = db.View(func(txn *badger.Txn) error {
89 it := txn.NewIterator(badger.DefaultIteratorOptions)
90 defer it.Close()
91
92 for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
93 item := it.Item()
94 key := item.KeyCopy(nil)
95
96 // Decode the key
97 srcSer, tgtSer, kind, direction := indexes.EventEventGraphVars()
98 keyReader := bytes.NewReader(key)
99 if err := indexes.EventEventGraphDec(srcSer, tgtSer, kind, direction).UnmarshalRead(keyReader); err != nil {
100 t.Logf("Failed to decode key: %v", err)
101 continue
102 }
103
104 // Check if this is our edge
105 if srcSer.Get() == replySerial.Get() && tgtSer.Get() == parentSerial.Get() {
106 forwardFound = true
107 if direction.Letter() != types.EdgeDirectionETagOut {
108 t.Errorf("Expected direction %d, got %d", types.EdgeDirectionETagOut, direction.Letter())
109 }
110 if kind.Get() != 1 {
111 t.Errorf("Expected kind 1, got %d", kind.Get())
112 }
113 }
114 }
115 return nil
116 })
117 if err != nil {
118 t.Fatalf("View failed: %v", err)
119 }
120 if !forwardFound {
121 t.Error("Forward edge (reply -> parent) should exist")
122 }
123
124 // Verify reverse edge exists (parent <- reply)
125 reverseFound := false
126 prefix = []byte(indexes.GraphEventEventPrefix)
127
128 err = db.View(func(txn *badger.Txn) error {
129 it := txn.NewIterator(badger.DefaultIteratorOptions)
130 defer it.Close()
131
132 for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
133 item := it.Item()
134 key := item.KeyCopy(nil)
135
136 // Decode the key
137 tgtSer, kind, direction, srcSer := indexes.GraphEventEventVars()
138 keyReader := bytes.NewReader(key)
139 if err := indexes.GraphEventEventDec(tgtSer, kind, direction, srcSer).UnmarshalRead(keyReader); err != nil {
140 t.Logf("Failed to decode key: %v", err)
141 continue
142 }
143
144 t.Logf("Found gee edge: tgt=%d kind=%d dir=%d src=%d",
145 tgtSer.Get(), kind.Get(), direction.Letter(), srcSer.Get())
146
147 // Check if this is our edge
148 if tgtSer.Get() == parentSerial.Get() && srcSer.Get() == replySerial.Get() {
149 reverseFound = true
150 if direction.Letter() != types.EdgeDirectionETagIn {
151 t.Errorf("Expected direction %d, got %d", types.EdgeDirectionETagIn, direction.Letter())
152 }
153 if kind.Get() != 1 {
154 t.Errorf("Expected kind 1, got %d", kind.Get())
155 }
156 }
157 }
158 return nil
159 })
160 if err != nil {
161 t.Fatalf("View failed: %v", err)
162 }
163 if !reverseFound {
164 t.Error("Reverse edge (parent <- reply) should exist")
165 }
166 }
167
168 func TestETagGraphMultipleReplies(t *testing.T) {
169 ctx, cancel := context.WithCancel(context.Background())
170 defer cancel()
171
172 db, err := New(ctx, cancel, t.TempDir(), "info")
173 if err != nil {
174 t.Fatalf("Failed to create database: %v", err)
175 }
176 defer db.Close()
177
178 // Create a parent event
179 parentPubkey, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000001")
180 parentID := make([]byte, 32)
181 parentID[0] = 0x10
182 parentSig := make([]byte, 64)
183 parentSig[0] = 0x10
184
185 parentEvent := &event.E{
186 ID: parentID,
187 Pubkey: parentPubkey,
188 CreatedAt: 1234567890,
189 Kind: 1,
190 Content: []byte("Parent post"),
191 Sig: parentSig,
192 Tags: &tag.S{},
193 }
194 _, err = db.SaveEvent(ctx, parentEvent)
195 if err != nil {
196 t.Fatalf("Failed to save parent: %v", err)
197 }
198
199 // Create multiple replies
200 numReplies := 5
201 for i := 0; i < numReplies; i++ {
202 replyPubkey := make([]byte, 32)
203 replyPubkey[0] = byte(i + 0x20)
204 replyID := make([]byte, 32)
205 replyID[0] = byte(i + 0x30)
206 replySig := make([]byte, 64)
207 replySig[0] = byte(i + 0x30)
208
209 replyEvent := &event.E{
210 ID: replyID,
211 Pubkey: replyPubkey,
212 CreatedAt: int64(1234567891 + i),
213 Kind: 1,
214 Content: []byte("Reply"),
215 Sig: replySig,
216 Tags: tag.NewS(
217 tag.NewFromAny("e", hex.Enc(parentID)),
218 ),
219 }
220 _, err := db.SaveEvent(ctx, replyEvent)
221 if err != nil {
222 t.Fatalf("Failed to save reply %d: %v", i, err)
223 }
224 }
225
226 // Count inbound edges to parent
227 parentSerial, err := db.GetSerialById(parentID)
228 if err != nil {
229 t.Fatalf("Failed to get parent serial: %v", err)
230 }
231
232 inboundCount := 0
233 prefix := []byte(indexes.GraphEventEventPrefix)
234
235 err = db.View(func(txn *badger.Txn) error {
236 it := txn.NewIterator(badger.DefaultIteratorOptions)
237 defer it.Close()
238
239 for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
240 item := it.Item()
241 key := item.KeyCopy(nil)
242
243 tgtSer, kind, direction, srcSer := indexes.GraphEventEventVars()
244 keyReader := bytes.NewReader(key)
245 if err := indexes.GraphEventEventDec(tgtSer, kind, direction, srcSer).UnmarshalRead(keyReader); err != nil {
246 continue
247 }
248
249 if tgtSer.Get() == parentSerial.Get() {
250 inboundCount++
251 }
252 }
253 return nil
254 })
255 if err != nil {
256 t.Fatalf("View failed: %v", err)
257 }
258
259 if inboundCount != numReplies {
260 t.Errorf("Expected %d inbound edges, got %d", numReplies, inboundCount)
261 }
262 }
263
264 func TestETagGraphDifferentKinds(t *testing.T) {
265 ctx, cancel := context.WithCancel(context.Background())
266 defer cancel()
267
268 db, err := New(ctx, cancel, t.TempDir(), "info")
269 if err != nil {
270 t.Fatalf("Failed to create database: %v", err)
271 }
272 defer db.Close()
273
274 // Create a parent event (kind 1 - note)
275 parentPubkey, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000001")
276 parentID := make([]byte, 32)
277 parentID[0] = 0x10
278 parentSig := make([]byte, 64)
279 parentSig[0] = 0x10
280
281 parentEvent := &event.E{
282 ID: parentID,
283 Pubkey: parentPubkey,
284 CreatedAt: 1234567890,
285 Kind: 1,
286 Content: []byte("A note"),
287 Sig: parentSig,
288 Tags: &tag.S{},
289 }
290 _, err = db.SaveEvent(ctx, parentEvent)
291 if err != nil {
292 t.Fatalf("Failed to save parent: %v", err)
293 }
294
295 // Create a reaction (kind 7)
296 reactionPubkey, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000002")
297 reactionID := make([]byte, 32)
298 reactionID[0] = 0x20
299 reactionSig := make([]byte, 64)
300 reactionSig[0] = 0x20
301
302 reactionEvent := &event.E{
303 ID: reactionID,
304 Pubkey: reactionPubkey,
305 CreatedAt: 1234567891,
306 Kind: 7,
307 Content: []byte("+"),
308 Sig: reactionSig,
309 Tags: tag.NewS(
310 tag.NewFromAny("e", hex.Enc(parentID)),
311 ),
312 }
313 _, err = db.SaveEvent(ctx, reactionEvent)
314 if err != nil {
315 t.Fatalf("Failed to save reaction: %v", err)
316 }
317
318 // Create a repost (kind 6)
319 repostPubkey, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000003")
320 repostID := make([]byte, 32)
321 repostID[0] = 0x30
322 repostSig := make([]byte, 64)
323 repostSig[0] = 0x30
324
325 repostEvent := &event.E{
326 ID: repostID,
327 Pubkey: repostPubkey,
328 CreatedAt: 1234567892,
329 Kind: 6,
330 Content: []byte(""),
331 Sig: repostSig,
332 Tags: tag.NewS(
333 tag.NewFromAny("e", hex.Enc(parentID)),
334 ),
335 }
336 _, err = db.SaveEvent(ctx, repostEvent)
337 if err != nil {
338 t.Fatalf("Failed to save repost: %v", err)
339 }
340
341 // Query inbound edges by kind
342 parentSerial, err := db.GetSerialById(parentID)
343 if err != nil {
344 t.Fatalf("Failed to get parent serial: %v", err)
345 }
346
347 kindCounts := make(map[uint16]int)
348 prefix := []byte(indexes.GraphEventEventPrefix)
349
350 err = db.View(func(txn *badger.Txn) error {
351 it := txn.NewIterator(badger.DefaultIteratorOptions)
352 defer it.Close()
353
354 for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
355 item := it.Item()
356 key := item.KeyCopy(nil)
357
358 tgtSer, kind, direction, srcSer := indexes.GraphEventEventVars()
359 keyReader := bytes.NewReader(key)
360 if err := indexes.GraphEventEventDec(tgtSer, kind, direction, srcSer).UnmarshalRead(keyReader); err != nil {
361 continue
362 }
363
364 if tgtSer.Get() == parentSerial.Get() {
365 kindCounts[kind.Get()]++
366 }
367 }
368 return nil
369 })
370 if err != nil {
371 t.Fatalf("View failed: %v", err)
372 }
373
374 // Verify we have edges for each kind
375 if kindCounts[7] != 1 {
376 t.Errorf("Expected 1 kind-7 (reaction) edge, got %d", kindCounts[7])
377 }
378 if kindCounts[6] != 1 {
379 t.Errorf("Expected 1 kind-6 (repost) edge, got %d", kindCounts[6])
380 }
381 }
382
383 func TestETagGraphUnknownTarget(t *testing.T) {
384 ctx, cancel := context.WithCancel(context.Background())
385 defer cancel()
386
387 db, err := New(ctx, cancel, t.TempDir(), "info")
388 if err != nil {
389 t.Fatalf("Failed to create database: %v", err)
390 }
391 defer db.Close()
392
393 // Create an event with e-tag pointing to non-existent event
394 unknownID := make([]byte, 32)
395 unknownID[0] = 0xFF
396 unknownID[31] = 0xFF
397
398 replyPubkey, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000001")
399 replyID := make([]byte, 32)
400 replyID[0] = 0x10
401 replySig := make([]byte, 64)
402 replySig[0] = 0x10
403
404 replyEvent := &event.E{
405 ID: replyID,
406 Pubkey: replyPubkey,
407 CreatedAt: 1234567890,
408 Kind: 1,
409 Content: []byte("Reply to unknown"),
410 Sig: replySig,
411 Tags: tag.NewS(
412 tag.NewFromAny("e", hex.Enc(unknownID)),
413 ),
414 }
415 _, err = db.SaveEvent(ctx, replyEvent)
416 if err != nil {
417 t.Fatalf("Failed to save reply: %v", err)
418 }
419
420 // Verify event was saved
421 replySerial, err := db.GetSerialById(replyID)
422 if err != nil {
423 t.Fatalf("Failed to get reply serial: %v", err)
424 }
425 if replySerial == nil {
426 t.Fatal("Reply serial should exist")
427 }
428
429 // Verify no forward edge was created (since target doesn't exist)
430 edgeCount := 0
431 prefix := []byte(indexes.EventEventGraphPrefix)
432
433 err = db.View(func(txn *badger.Txn) error {
434 it := txn.NewIterator(badger.DefaultIteratorOptions)
435 defer it.Close()
436
437 for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
438 item := it.Item()
439 key := item.KeyCopy(nil)
440
441 srcSer, _, _, _ := indexes.EventEventGraphVars()
442 keyReader := bytes.NewReader(key)
443 if err := indexes.EventEventGraphDec(srcSer, new(types.Uint40), new(types.Uint16), new(types.Letter)).UnmarshalRead(keyReader); err != nil {
444 continue
445 }
446
447 if srcSer.Get() == replySerial.Get() {
448 edgeCount++
449 }
450 }
451 return nil
452 })
453 if err != nil {
454 t.Fatalf("View failed: %v", err)
455 }
456
457 if edgeCount != 0 {
458 t.Errorf("Expected no edges for unknown target, got %d", edgeCount)
459 }
460 }
461