filter_test.go raw
1 package nostr
2
3 import (
4 "strings"
5 "testing"
6 )
7
8 func TestParseFilter(t *testing.T) {
9 raw := []byte(`{"ids":["eaeb41f2e1d44abc3949052406db478aeed872a64e46022daace5955cd65b316"],"authors":["f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"],"kinds":[0,1,30023],"#e":["abc"],"#p":["def","ghi"],"since":100,"until":200,"limit":50,"search":"hello world"}`)
10 f, err := ParseFilter(raw)
11 if err != nil {
12 t.Fatal(err)
13 }
14 if len(f.IDs) != 1 || len(f.Authors) != 1 || len(f.Kinds) != 3 {
15 t.Fatalf("unexpected filter: %+v", f)
16 }
17 if f.Since != 100 || f.Until != 200 || f.Limit != 50 || f.Search != "hello world" {
18 t.Fatalf("unexpected scalars: %+v", f)
19 }
20 if len(f.Tags["e"]) != 1 || len(f.Tags["p"]) != 2 {
21 t.Fatalf("unexpected tags: %+v", f.Tags)
22 }
23 }
24
25 func TestFilterMatches(t *testing.T) {
26 e := &Event{Kind: 1, CreatedAt: 150, Content: "hello world"}
27 e.PubKey[0] = 0xaa
28 e.ID[0] = 0xbb
29 e.Tags = Tags{{"p", "deadbeef"}, {"t", "nostr"}}
30 id := Hex(e.ID[:])
31 pub := Hex(e.PubKey[:])
32
33 base := func() *Filter { return &Filter{} }
34 cases := []struct {
35 name string
36 f *Filter
37 want bool
38 }{
39 {"empty", base(), true},
40 {"kind match", &Filter{Kinds: []int{1}}, true},
41 {"kind miss", &Filter{Kinds: []int{2}}, false},
42 {"id match", &Filter{IDs: []string{id}}, true},
43 {"id miss", &Filter{IDs: []string{Hex(make([]byte, 32))}}, false},
44 {"author match", &Filter{Authors: []string{pub}}, true},
45 {"tag match", &Filter{Tags: map[string][]string{"p": {"deadbeef"}}}, true},
46 {"tag miss", &Filter{Tags: map[string][]string{"p": {"cafe"}}}, false},
47 {"two tags and", &Filter{Tags: map[string][]string{"p": {"deadbeef"}, "t": {"nostr"}}}, true},
48 {"since pass", &Filter{Since: 150}, true},
49 {"since fail", &Filter{Since: 151}, false},
50 {"until pass", &Filter{Until: 150}, true},
51 {"until fail", &Filter{Until: 149}, false},
52 {"multi-letter tag", &Filter{Tags: map[string][]string{"ee": {"x"}}}, false},
53 }
54 for _, c := range cases {
55 if got := c.f.Matches(e); got != c.want {
56 t.Errorf("%s: got %v, want %v", c.name, got, c.want)
57 }
58 }
59 }
60
61 func TestFilterRejectsBadHex(t *testing.T) {
62 if _, err := ParseFilter([]byte(`{"ids":["nothex"]}`)); err == nil {
63 t.Fatal("expected error for non-hex id")
64 }
65 if _, err := ParseFilter([]byte(`{"authors":["ABCDEF"]}`)); err == nil {
66 t.Fatal("expected error for uppercase author")
67 }
68 if _, err := ParseFilter([]byte(`{"kinds":[70000]}`)); err == nil {
69 t.Fatal("expected error for out-of-range kind")
70 }
71 }
72
73 func TestFilterUnknownKeysIgnored(t *testing.T) {
74 f, err := ParseFilter([]byte(`{"kinds":[1],"future_extension":{"x":1},"limit":10}`))
75 if err != nil {
76 t.Fatal(err)
77 }
78 if len(f.Kinds) != 1 || f.Limit != 10 {
79 t.Fatalf("filter: %+v", f)
80 }
81 }
82
83 func TestFilterLimitClamp(t *testing.T) {
84 f, err := ParseFilter([]byte(`{"limit":1000000000000}`))
85 if err != nil {
86 t.Fatal(err)
87 }
88 if f.Limit != 1<<31-1 {
89 t.Fatalf("limit = %d, want MaxInt32", f.Limit)
90 }
91 if f, err = ParseFilter([]byte(`{"limit":-5}`)); err != nil || f.Limit != 0 {
92 t.Fatalf("negative limit: %+v %v", f, err)
93 }
94 }
95
96 func TestFilterTypeErrors(t *testing.T) {
97 cases := []string{
98 `{"kinds":["one"]}`,
99 `{"ids":5}`,
100 `{"#e":"not-an-array"}`,
101 `{"since":"yesterday"}`,
102 `{"#e":["ok",5]}`,
103 `[]`,
104 }
105 for _, raw := range cases {
106 if _, err := ParseFilter([]byte(raw)); err == nil {
107 t.Errorf("accepted %s", raw)
108 }
109 }
110 }
111
112 func TestFilterAppendJSONRoundTrip(t *testing.T) {
113 f := &Filter{
114 IDs: []string{"eaeb41f2e1d44abc3949052406db478aeed872a64e46022daace5955cd65b316"},
115 Authors: []string{"f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"},
116 Kinds: []int{1, 2},
117 Tags: map[string][]string{"e": {"abc"}},
118 Since: 10,
119 Until: 20,
120 Limit: 5,
121 Search: "hello",
122 }
123 back, err := ParseFilter(f.AppendJSON(nil))
124 if err != nil {
125 t.Fatalf("reparse %s: %v", f, err)
126 }
127 if len(back.IDs) != 1 || len(back.Kinds) != 2 || back.Since != 10 || back.Until != 20 ||
128 back.Limit != 5 || back.Search != "hello" || len(back.Tags["e"]) != 1 {
129 t.Fatalf("round trip: %+v", back)
130 }
131 if TagsKey("#e") != "e" || TagsKey("e") != "e" {
132 t.Fatal("TagsKey")
133 }
134 }
135
136 func TestParseGraphFilter(t *testing.T) {
137 seed := strings.Repeat("ab", 32)
138 f, err := ParseFilter([]byte(`{"_graph":["` + seed + `",2,"PP","BOTH"]}`))
139 if err != nil {
140 t.Fatal(err)
141 }
142 if f.Graph == nil || f.Graph.Seed != seed || f.Graph.Depth != 2 ||
143 f.Graph.Edge != GraphEdgePubkeyPubkey || f.Graph.Direction != GraphDirBoth {
144 t.Fatalf("array form: %+v", f.Graph)
145 }
146 back, err := ParseFilter(f.AppendJSON(nil))
147 if err != nil {
148 t.Fatal(err)
149 }
150 if back.Graph == nil || *back.Graph != *f.Graph {
151 t.Fatalf("round trip: %+v", back.Graph)
152 }
153
154 object := `{"_graph":{"seed":"` + strings.ToUpper(seed) + `","edge":"pe","direction":"in"}}`
155 f, err = ParseFilter([]byte(object))
156 if err != nil {
157 t.Fatal(err)
158 }
159 if f.Graph.Seed != seed || f.Graph.Depth != 1 || f.Graph.Edge != GraphEdgePubkeyEvent ||
160 f.Graph.Direction != GraphDirIn {
161 t.Fatalf("object form: %+v", f.Graph)
162 }
163 }
164
165 func TestParseGraphFilterRejectsBadValues(t *testing.T) {
166 seed := strings.Repeat("ab", 32)
167 cases := []string{
168 `{"_graph":[]}`,
169 `{"_graph":["` + seed + `",1,"pp"]}`,
170 `{"_graph":["` + seed + `",1,"pp","out",1]}`,
171 `{"_graph":["short",1,"pp","out"]}`,
172 `{"_graph":["` + seed + `",17,"pp","out"]}`,
173 `{"_graph":["` + seed + `",1,"xx","out"]}`,
174 `{"_graph":["` + seed + `",1,"pp","sideways"]}`,
175 `{"_graph":"pp"}`,
176 `{"_graph":{"":"x"}}`,
177 `{"_graph":{"pubkey":"` + seed + `"}}`,
178 }
179 for _, raw := range cases {
180 if _, err := ParseFilter([]byte(raw)); err == nil {
181 t.Errorf("accepted %s", raw)
182 }
183 }
184 }
185