package nostr import ( "strings" "testing" ) func TestParseFilter(t *testing.T) { raw := []byte(`{"ids":["eaeb41f2e1d44abc3949052406db478aeed872a64e46022daace5955cd65b316"],"authors":["f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"],"kinds":[0,1,30023],"#e":["abc"],"#p":["def","ghi"],"since":100,"until":200,"limit":50,"search":"hello world"}`) f, err := ParseFilter(raw) if err != nil { t.Fatal(err) } if len(f.IDs) != 1 || len(f.Authors) != 1 || len(f.Kinds) != 3 { t.Fatalf("unexpected filter: %+v", f) } if f.Since != 100 || f.Until != 200 || f.Limit != 50 || f.Search != "hello world" { t.Fatalf("unexpected scalars: %+v", f) } if len(f.Tags["e"]) != 1 || len(f.Tags["p"]) != 2 { t.Fatalf("unexpected tags: %+v", f.Tags) } } func TestFilterMatches(t *testing.T) { e := &Event{Kind: 1, CreatedAt: 150, Content: "hello world"} e.PubKey[0] = 0xaa e.ID[0] = 0xbb e.Tags = Tags{{"p", "deadbeef"}, {"t", "nostr"}} id := Hex(e.ID[:]) pub := Hex(e.PubKey[:]) base := func() *Filter { return &Filter{} } cases := []struct { name string f *Filter want bool }{ {"empty", base(), true}, {"kind match", &Filter{Kinds: []int{1}}, true}, {"kind miss", &Filter{Kinds: []int{2}}, false}, {"id match", &Filter{IDs: []string{id}}, true}, {"id miss", &Filter{IDs: []string{Hex(make([]byte, 32))}}, false}, {"author match", &Filter{Authors: []string{pub}}, true}, {"tag match", &Filter{Tags: map[string][]string{"p": {"deadbeef"}}}, true}, {"tag miss", &Filter{Tags: map[string][]string{"p": {"cafe"}}}, false}, {"two tags and", &Filter{Tags: map[string][]string{"p": {"deadbeef"}, "t": {"nostr"}}}, true}, {"since pass", &Filter{Since: 150}, true}, {"since fail", &Filter{Since: 151}, false}, {"until pass", &Filter{Until: 150}, true}, {"until fail", &Filter{Until: 149}, false}, {"multi-letter tag", &Filter{Tags: map[string][]string{"ee": {"x"}}}, false}, } for _, c := range cases { if got := c.f.Matches(e); got != c.want { t.Errorf("%s: got %v, want %v", c.name, got, c.want) } } } func TestFilterRejectsBadHex(t *testing.T) { if _, err := ParseFilter([]byte(`{"ids":["nothex"]}`)); err == nil { t.Fatal("expected error for non-hex id") } if _, err := ParseFilter([]byte(`{"authors":["ABCDEF"]}`)); err == nil { t.Fatal("expected error for uppercase author") } if _, err := ParseFilter([]byte(`{"kinds":[70000]}`)); err == nil { t.Fatal("expected error for out-of-range kind") } } func TestFilterUnknownKeysIgnored(t *testing.T) { f, err := ParseFilter([]byte(`{"kinds":[1],"future_extension":{"x":1},"limit":10}`)) if err != nil { t.Fatal(err) } if len(f.Kinds) != 1 || f.Limit != 10 { t.Fatalf("filter: %+v", f) } } func TestFilterLimitClamp(t *testing.T) { f, err := ParseFilter([]byte(`{"limit":1000000000000}`)) if err != nil { t.Fatal(err) } if f.Limit != 1<<31-1 { t.Fatalf("limit = %d, want MaxInt32", f.Limit) } if f, err = ParseFilter([]byte(`{"limit":-5}`)); err != nil || f.Limit != 0 { t.Fatalf("negative limit: %+v %v", f, err) } } func TestFilterTypeErrors(t *testing.T) { cases := []string{ `{"kinds":["one"]}`, `{"ids":5}`, `{"#e":"not-an-array"}`, `{"since":"yesterday"}`, `{"#e":["ok",5]}`, `[]`, } for _, raw := range cases { if _, err := ParseFilter([]byte(raw)); err == nil { t.Errorf("accepted %s", raw) } } } func TestFilterAppendJSONRoundTrip(t *testing.T) { f := &Filter{ IDs: []string{"eaeb41f2e1d44abc3949052406db478aeed872a64e46022daace5955cd65b316"}, Authors: []string{"f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"}, Kinds: []int{1, 2}, Tags: map[string][]string{"e": {"abc"}}, Since: 10, Until: 20, Limit: 5, Search: "hello", } back, err := ParseFilter(f.AppendJSON(nil)) if err != nil { t.Fatalf("reparse %s: %v", f, err) } if len(back.IDs) != 1 || len(back.Kinds) != 2 || back.Since != 10 || back.Until != 20 || back.Limit != 5 || back.Search != "hello" || len(back.Tags["e"]) != 1 { t.Fatalf("round trip: %+v", back) } if TagsKey("#e") != "e" || TagsKey("e") != "e" { t.Fatal("TagsKey") } } func TestParseGraphFilter(t *testing.T) { seed := strings.Repeat("ab", 32) f, err := ParseFilter([]byte(`{"_graph":["` + seed + `",2,"PP","BOTH"]}`)) if err != nil { t.Fatal(err) } if f.Graph == nil || f.Graph.Seed != seed || f.Graph.Depth != 2 || f.Graph.Edge != GraphEdgePubkeyPubkey || f.Graph.Direction != GraphDirBoth { t.Fatalf("array form: %+v", f.Graph) } back, err := ParseFilter(f.AppendJSON(nil)) if err != nil { t.Fatal(err) } if back.Graph == nil || *back.Graph != *f.Graph { t.Fatalf("round trip: %+v", back.Graph) } object := `{"_graph":{"seed":"` + strings.ToUpper(seed) + `","edge":"pe","direction":"in"}}` f, err = ParseFilter([]byte(object)) if err != nil { t.Fatal(err) } if f.Graph.Seed != seed || f.Graph.Depth != 1 || f.Graph.Edge != GraphEdgePubkeyEvent || f.Graph.Direction != GraphDirIn { t.Fatalf("object form: %+v", f.Graph) } } func TestParseGraphFilterRejectsBadValues(t *testing.T) { seed := strings.Repeat("ab", 32) cases := []string{ `{"_graph":[]}`, `{"_graph":["` + seed + `",1,"pp"]}`, `{"_graph":["` + seed + `",1,"pp","out",1]}`, `{"_graph":["short",1,"pp","out"]}`, `{"_graph":["` + seed + `",17,"pp","out"]}`, `{"_graph":["` + seed + `",1,"xx","out"]}`, `{"_graph":["` + seed + `",1,"pp","sideways"]}`, `{"_graph":"pp"}`, `{"_graph":{"":"x"}}`, `{"_graph":{"pubkey":"` + seed + `"}}`, } for _, raw := range cases { if _, err := ParseFilter([]byte(raw)); err == nil { t.Errorf("accepted %s", raw) } } }