query-addressable_test.go raw
1 //go:build !(js && wasm)
2
3 package database
4
5 import (
6 "context"
7 "os"
8 "testing"
9 "time"
10
11 "next.orly.dev/pkg/nostr/encoders/event"
12 "next.orly.dev/pkg/nostr/encoders/filter"
13 "next.orly.dev/pkg/nostr/encoders/kind"
14 "next.orly.dev/pkg/nostr/encoders/tag"
15 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
16 "next.orly.dev/pkg/lol/chk"
17 )
18
19 func TestIsAddressableEventQuery(t *testing.T) {
20 // Generate a test keypair
21 signer := p8k.MustNew()
22 if err := signer.Generate(); chk.E(err) {
23 t.Fatal(err)
24 }
25 pub := signer.Pub()
26
27 tests := []struct {
28 name string
29 filter *filter.F
30 expected bool
31 }{
32 {
33 name: "valid NIP-33 query - kind 30000",
34 filter: &filter.F{
35 Kinds: kind.NewS(kind.New(30000)),
36 Authors: tag.NewFromBytesSlice(pub),
37 Tags: tag.NewS(tag.NewFromAny("#d", []byte("test-d-tag"))),
38 },
39 expected: true,
40 },
41 {
42 name: "valid NIP-33 query - kind 30382",
43 filter: &filter.F{
44 Kinds: kind.NewS(kind.New(30382)),
45 Authors: tag.NewFromBytesSlice(pub),
46 Tags: tag.NewS(tag.NewFromAny("#d", []byte("some-identifier"))),
47 },
48 expected: true,
49 },
50 {
51 name: "invalid - kind 1 (not parameterized replaceable)",
52 filter: &filter.F{
53 Kinds: kind.NewS(kind.New(1)),
54 Authors: tag.NewFromBytesSlice(pub),
55 Tags: tag.NewS(tag.NewFromAny("#d", []byte("test"))),
56 },
57 expected: false,
58 },
59 {
60 name: "invalid - kind 10000 (replaceable, not parameterized)",
61 filter: &filter.F{
62 Kinds: kind.NewS(kind.New(10000)),
63 Authors: tag.NewFromBytesSlice(pub),
64 Tags: tag.NewS(tag.NewFromAny("#d", []byte("test"))),
65 },
66 expected: false,
67 },
68 {
69 name: "invalid - missing d-tag",
70 filter: &filter.F{
71 Kinds: kind.NewS(kind.New(30000)),
72 Authors: tag.NewFromBytesSlice(pub),
73 },
74 expected: false,
75 },
76 {
77 name: "invalid - multiple kinds",
78 filter: &filter.F{
79 Kinds: kind.NewS(kind.New(30000), kind.New(30001)),
80 Authors: tag.NewFromBytesSlice(pub),
81 Tags: tag.NewS(tag.NewFromAny("#d", []byte("test"))),
82 },
83 expected: false,
84 },
85 {
86 name: "invalid - no authors",
87 filter: &filter.F{
88 Kinds: kind.NewS(kind.New(30000)),
89 Tags: tag.NewS(tag.NewFromAny("#d", []byte("test"))),
90 },
91 expected: false,
92 },
93 }
94
95 for _, tt := range tests {
96 t.Run(tt.name, func(t *testing.T) {
97 result := IsAddressableEventQuery(tt.filter)
98 if result != tt.expected {
99 t.Errorf("IsAddressableEventQuery() = %v, want %v", result, tt.expected)
100 }
101 })
102 }
103 }
104
105 func TestQueryForAddressableEvent(t *testing.T) {
106 // Create temporary database
107 tempDir, err := os.MkdirTemp("", "test-addressable-*")
108 if err != nil {
109 t.Fatalf("Failed to create temp dir: %v", err)
110 }
111 defer os.RemoveAll(tempDir)
112
113 ctx, cancel := context.WithCancel(context.Background())
114 defer cancel()
115
116 db, err := New(ctx, cancel, tempDir, "info")
117 if err != nil {
118 t.Fatalf("failed to create database: %v", err)
119 }
120 defer db.Close()
121
122 // Generate a test keypair
123 signer := p8k.MustNew()
124 if err := signer.Generate(); chk.E(err) {
125 t.Fatal(err)
126 }
127 pub := signer.Pub()
128
129 // Create and save a parameterized replaceable event (kind 30382)
130 dTagValue := []byte("test-identifier-12345")
131 ev := &event.E{
132 Kind: 30382,
133 Pubkey: pub,
134 CreatedAt: time.Now().Unix(),
135 Content: []byte("Test content for addressable event"),
136 Tags: tag.NewS(tag.NewFromAny("d", dTagValue)),
137 }
138
139 // Sign the event
140 if err := ev.Sign(signer); err != nil {
141 t.Fatalf("failed to sign event: %v", err)
142 }
143
144 // Save the event
145 _, err = db.SaveEvent(ctx, ev)
146 if err != nil {
147 t.Fatalf("failed to save event: %v", err)
148 }
149
150 // Query using the fast path
151 queryFilter := &filter.F{
152 Kinds: kind.NewS(kind.New(30382)),
153 Authors: tag.NewFromBytesSlice(pub),
154 Tags: tag.NewS(tag.NewFromAny("#d", dTagValue)),
155 }
156
157 // Test IsAddressableEventQuery
158 if !IsAddressableEventQuery(queryFilter) {
159 t.Errorf("Expected IsAddressableEventQuery to return true for valid NIP-33 filter")
160 }
161
162 // Test QueryForAddressableEvent
163 serial, err := db.QueryForAddressableEvent(queryFilter)
164 if err != nil {
165 t.Fatalf("QueryForAddressableEvent failed: %v", err)
166 }
167 if serial == nil {
168 t.Fatalf("QueryForAddressableEvent returned nil serial, expected to find event")
169 }
170
171 // Fetch the event and verify it matches
172 fetchedEv, err := db.FetchEventBySerial(serial)
173 if err != nil {
174 t.Fatalf("FetchEventBySerial failed: %v", err)
175 }
176 if fetchedEv == nil {
177 t.Fatalf("FetchEventBySerial returned nil event")
178 }
179
180 // Verify it's the same event
181 if string(fetchedEv.ID[:]) != string(ev.ID[:]) {
182 t.Errorf("Fetched event ID doesn't match: got %x, want %x", fetchedEv.ID, ev.ID)
183 }
184
185 // Test that QueryEvents also uses the fast path
186 evs, err := db.QueryEvents(ctx, queryFilter)
187 if err != nil {
188 t.Fatalf("QueryEvents failed: %v", err)
189 }
190 if len(evs) != 1 {
191 t.Fatalf("QueryEvents returned %d events, expected 1", len(evs))
192 }
193 if string(evs[0].ID[:]) != string(ev.ID[:]) {
194 t.Errorf("QueryEvents returned wrong event: got %x, want %x", evs[0].ID, ev.ID)
195 }
196
197 t.Logf("Successfully queried addressable event via fast path: kind=%d, d=%s", ev.Kind, string(dTagValue))
198 }
199
200 func TestQueryForAddressableEventNotFound(t *testing.T) {
201 // Create temporary database
202 tempDir, err := os.MkdirTemp("", "test-addressable-notfound-*")
203 if err != nil {
204 t.Fatalf("Failed to create temp dir: %v", err)
205 }
206 defer os.RemoveAll(tempDir)
207
208 ctx, cancel := context.WithCancel(context.Background())
209 defer cancel()
210
211 db, err := New(ctx, cancel, tempDir, "info")
212 if err != nil {
213 t.Fatalf("failed to create database: %v", err)
214 }
215 defer db.Close()
216
217 // Generate a test keypair
218 signer := p8k.MustNew()
219 if err := signer.Generate(); chk.E(err) {
220 t.Fatal(err)
221 }
222 pub := signer.Pub()
223
224 // Query for non-existent event
225 queryFilter := &filter.F{
226 Kinds: kind.NewS(kind.New(30000)),
227 Authors: tag.NewFromBytesSlice(pub),
228 Tags: tag.NewS(tag.NewFromAny("#d", []byte("non-existent-d-tag"))),
229 }
230
231 serial, err := db.QueryForAddressableEvent(queryFilter)
232 if err != nil {
233 t.Fatalf("QueryForAddressableEvent failed: %v", err)
234 }
235 if serial != nil {
236 t.Errorf("Expected nil serial for non-existent event, got %d", serial.Get())
237 }
238 }
239
240 func TestAddressableEventReplacement(t *testing.T) {
241 // Create temporary database
242 tempDir, err := os.MkdirTemp("", "test-addressable-replace-*")
243 if err != nil {
244 t.Fatalf("Failed to create temp dir: %v", err)
245 }
246 defer os.RemoveAll(tempDir)
247
248 ctx, cancel := context.WithCancel(context.Background())
249 defer cancel()
250
251 db, err := New(ctx, cancel, tempDir, "info")
252 if err != nil {
253 t.Fatalf("failed to create database: %v", err)
254 }
255 defer db.Close()
256
257 // Generate a test keypair
258 signer := p8k.MustNew()
259 if err := signer.Generate(); chk.E(err) {
260 t.Fatal(err)
261 }
262 pub := signer.Pub()
263
264 dTagValue := []byte("replaceable-event")
265 baseTime := time.Now().Unix()
266
267 // Create and save the first event
268 ev1 := &event.E{
269 Kind: 30000,
270 Pubkey: pub,
271 CreatedAt: baseTime,
272 Content: []byte("First version"),
273 Tags: tag.NewS(tag.NewFromAny("d", dTagValue)),
274 }
275 if err := ev1.Sign(signer); err != nil {
276 t.Fatalf("failed to sign event 1: %v", err)
277 }
278 if _, err := db.SaveEvent(ctx, ev1); err != nil {
279 t.Fatalf("failed to save event 1: %v", err)
280 }
281
282 // Create and save a newer replacement event
283 ev2 := &event.E{
284 Kind: 30000,
285 Pubkey: pub,
286 CreatedAt: baseTime + 1000, // Newer
287 Content: []byte("Second version - replacement"),
288 Tags: tag.NewS(tag.NewFromAny("d", dTagValue)),
289 }
290 if err := ev2.Sign(signer); err != nil {
291 t.Fatalf("failed to sign event 2: %v", err)
292 }
293 if _, err := db.SaveEvent(ctx, ev2); err != nil {
294 t.Fatalf("failed to save event 2: %v", err)
295 }
296
297 // Query and verify we get the newer event via fast path
298 queryFilter := &filter.F{
299 Kinds: kind.NewS(kind.New(30000)),
300 Authors: tag.NewFromBytesSlice(pub),
301 Tags: tag.NewS(tag.NewFromAny("#d", dTagValue)),
302 }
303
304 serial, err := db.QueryForAddressableEvent(queryFilter)
305 if err != nil {
306 t.Fatalf("QueryForAddressableEvent failed: %v", err)
307 }
308 if serial == nil {
309 t.Fatalf("QueryForAddressableEvent returned nil serial")
310 }
311
312 fetchedEv, err := db.FetchEventBySerial(serial)
313 if err != nil {
314 t.Fatalf("FetchEventBySerial failed: %v", err)
315 }
316
317 // Should be the second (newer) event
318 if string(fetchedEv.ID[:]) != string(ev2.ID[:]) {
319 t.Errorf("Expected to get newer event (ev2), got different event")
320 }
321 if string(fetchedEv.Content) != "Second version - replacement" {
322 t.Errorf("Expected content 'Second version - replacement', got '%s'", string(fetchedEv.Content))
323 }
324
325 t.Logf("Replacement event correctly indexed: %x", ev2.ID)
326 }
327