query_test.go raw
1 package graph
2
3 import (
4 "testing"
5
6 "next.orly.dev/pkg/nostr/encoders/filter"
7 )
8
9 func TestQueryValidate(t *testing.T) {
10 validPubkey := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
11
12 tests := []struct {
13 name string
14 query Query
15 wantErr error
16 }{
17 {
18 name: "valid pp/out query",
19 query: Query{Pubkey: validPubkey, Depth: 2, Edge: "pp", Direction: "out"},
20 wantErr: nil,
21 },
22 {
23 name: "valid pp/in query",
24 query: Query{Pubkey: validPubkey, Depth: 1, Edge: "pp", Direction: "in"},
25 wantErr: nil,
26 },
27 {
28 name: "valid pe/both query",
29 query: Query{Pubkey: validPubkey, Depth: 3, Edge: "pe", Direction: "both"},
30 wantErr: nil,
31 },
32 {
33 name: "valid ee/out query",
34 query: Query{Pubkey: validPubkey, Depth: 1, Edge: "ee", Direction: "out"},
35 wantErr: nil,
36 },
37 {
38 name: "depth 0 defaults to 1",
39 query: Query{Pubkey: validPubkey, Depth: 0, Edge: "pp", Direction: "out"},
40 wantErr: nil,
41 },
42 {
43 name: "missing pubkey",
44 query: Query{Edge: "pp", Direction: "out"},
45 wantErr: ErrMissingPubkey,
46 },
47 {
48 name: "pubkey too short",
49 query: Query{Pubkey: "abc123", Edge: "pp", Direction: "out"},
50 wantErr: ErrInvalidPubkey,
51 },
52 {
53 name: "pubkey with invalid hex",
54 query: Query{Pubkey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdeg", Edge: "pp", Direction: "out"},
55 wantErr: ErrInvalidPubkey,
56 },
57 {
58 name: "depth too high",
59 query: Query{Pubkey: validPubkey, Depth: 17, Edge: "pp", Direction: "out"},
60 wantErr: ErrInvalidDepth,
61 },
62 {
63 name: "missing edge",
64 query: Query{Pubkey: validPubkey, Direction: "out"},
65 wantErr: ErrMissingEdge,
66 },
67 {
68 name: "invalid edge",
69 query: Query{Pubkey: validPubkey, Edge: "xx", Direction: "out"},
70 wantErr: ErrInvalidEdge,
71 },
72 {
73 name: "missing direction",
74 query: Query{Pubkey: validPubkey, Edge: "pp"},
75 wantErr: ErrMissingDirection,
76 },
77 {
78 name: "invalid direction",
79 query: Query{Pubkey: validPubkey, Edge: "pp", Direction: "left"},
80 wantErr: ErrInvalidDirection,
81 },
82 }
83
84 for _, tt := range tests {
85 t.Run(tt.name, func(t *testing.T) {
86 err := tt.query.Validate()
87 if tt.wantErr == nil {
88 if err != nil {
89 t.Errorf("unexpected error: %v", err)
90 }
91 } else {
92 if err != tt.wantErr {
93 t.Errorf("error = %v, want %v", err, tt.wantErr)
94 }
95 }
96 })
97 }
98 }
99
100 func TestQueryDefaults(t *testing.T) {
101 validPubkey := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
102
103 q := Query{Pubkey: validPubkey, Depth: 0, Edge: "pp", Direction: "out"}
104 if err := q.Validate(); err != nil {
105 t.Fatalf("unexpected error: %v", err)
106 }
107 if q.Depth != 1 {
108 t.Errorf("Depth = %d, want 1 (default)", q.Depth)
109 }
110 }
111
112 func TestExtractFromFilter_Array(t *testing.T) {
113 validPubkey := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
114
115 tests := []struct {
116 name string
117 filterJSON string
118 wantQuery bool
119 wantErr bool
120 wantEdge string
121 wantDir string
122 }{
123 {
124 name: "4-element array format",
125 filterJSON: `{"kinds":[1],"_graph":["` + validPubkey + `",2,"pp","out"]}`,
126 wantQuery: true,
127 wantEdge: "pp",
128 wantDir: "out",
129 },
130 {
131 name: "legacy object format (backward compat)",
132 filterJSON: `{"kinds":[1],"_graph":{"pubkey":"` + validPubkey + `","depth":2,"edge":"pp","direction":"out"}}`,
133 wantQuery: true,
134 wantEdge: "pp",
135 wantDir: "out",
136 },
137 {
138 name: "no graph query",
139 filterJSON: `{"kinds":[1,7]}`,
140 wantQuery: false,
141 },
142 {
143 name: "invalid array length",
144 filterJSON: `{"kinds":[1],"_graph":["` + validPubkey + `",2,"pp"]}`,
145 wantErr: true,
146 },
147 {
148 name: "invalid edge in array",
149 filterJSON: `{"kinds":[1],"_graph":["` + validPubkey + `",2,"xx","out"]}`,
150 wantErr: true,
151 },
152 }
153
154 for _, tt := range tests {
155 t.Run(tt.name, func(t *testing.T) {
156 f := &filter.F{}
157 if _, err := f.Unmarshal([]byte(tt.filterJSON)); err != nil {
158 t.Fatalf("failed to unmarshal filter: %v", err)
159 }
160
161 q, err := ExtractFromFilter(f)
162
163 if tt.wantErr {
164 if err == nil {
165 t.Error("expected error, got nil")
166 }
167 return
168 }
169 if err != nil {
170 t.Errorf("unexpected error: %v", err)
171 return
172 }
173
174 if tt.wantQuery {
175 if q == nil {
176 t.Fatal("expected query, got nil")
177 }
178 if q.Edge != tt.wantEdge {
179 t.Errorf("Edge = %q, want %q", q.Edge, tt.wantEdge)
180 }
181 if q.Direction != tt.wantDir {
182 t.Errorf("Direction = %q, want %q", q.Direction, tt.wantDir)
183 }
184 } else if q != nil {
185 t.Errorf("expected nil query, got %+v", q)
186 }
187 })
188 }
189 }
190
191 func TestIsGraphQuery(t *testing.T) {
192 tests := []struct {
193 name string
194 filterJSON string
195 want bool
196 }{
197 {
198 name: "filter with graph query",
199 filterJSON: `{"kinds":[1],"_graph":["abc",1,"pp","out"]}`,
200 want: true,
201 },
202 {
203 name: "filter without graph query",
204 filterJSON: `{"kinds":[1,7]}`,
205 want: false,
206 },
207 {
208 name: "filter with other extension",
209 filterJSON: `{"kinds":[1],"_custom":"value"}`,
210 want: false,
211 },
212 }
213
214 for _, tt := range tests {
215 t.Run(tt.name, func(t *testing.T) {
216 f := &filter.F{}
217 if _, err := f.Unmarshal([]byte(tt.filterJSON)); err != nil {
218 t.Fatalf("failed to unmarshal filter: %v", err)
219 }
220 if got := IsGraphQuery(f); got != tt.want {
221 t.Errorf("IsGraphQuery() = %v, want %v", got, tt.want)
222 }
223 })
224 }
225 }
226
227 func TestQueryEdgeHelpers(t *testing.T) {
228 tests := []struct {
229 edge string
230 direction string
231 pp, pe, ee bool
232 out, in_, both bool
233 }{
234 {"pp", "out", true, false, false, true, false, false},
235 {"pe", "in", false, true, false, false, true, false},
236 {"ee", "both", false, false, true, true, true, true},
237 {"pp", "both", true, false, false, true, true, true},
238 }
239
240 for _, tt := range tests {
241 q := Query{Edge: tt.edge, Direction: tt.direction}
242 if q.IsPubkeyPubkey() != tt.pp {
243 t.Errorf("edge=%s: IsPubkeyPubkey() = %v, want %v", tt.edge, q.IsPubkeyPubkey(), tt.pp)
244 }
245 if q.IsPubkeyEvent() != tt.pe {
246 t.Errorf("edge=%s: IsPubkeyEvent() = %v, want %v", tt.edge, q.IsPubkeyEvent(), tt.pe)
247 }
248 if q.IsEventEvent() != tt.ee {
249 t.Errorf("edge=%s: IsEventEvent() = %v, want %v", tt.edge, q.IsEventEvent(), tt.ee)
250 }
251 if q.IsOutbound() != tt.out {
252 t.Errorf("dir=%s: IsOutbound() = %v, want %v", tt.direction, q.IsOutbound(), tt.out)
253 }
254 if q.IsInbound() != tt.in_ {
255 t.Errorf("dir=%s: IsInbound() = %v, want %v", tt.direction, q.IsInbound(), tt.in_)
256 }
257 if q.IsBidirectional() != tt.both {
258 t.Errorf("dir=%s: IsBidirectional() = %v, want %v", tt.direction, q.IsBidirectional(), tt.both)
259 }
260 }
261 }
262