package research import ( "database/sql" "encoding/json" "reflect" "strings" "testing" ) func TestStoredAuthorShapes(t *testing.T) { for _, test := range []struct { raw string want []string }{ {`[]`, []string{}}, {`[["Buhrman","Harry","","CWI"],["Family","Given","Jr.","University"]]`, []string{"Harry Buhrman", "Given Family Jr."}}, {`["The BABAR Collaboration","B. Aubert"]`, []string{"The BABAR Collaboration", "B. Aubert"}}, {`[["Balázs","C.",""],["Berger","E. L.",""],["Smith","Ada","Jr."]]`, []string{"C. Balázs", "E. L. Berger", "Ada Smith Jr."}}, {`[" 张三 ",["Only family"],["","Only given",""],""]`, []string{"张三", "Only family", "Only given"}}, } { got, err := decodeAuthors(test.raw) if err != nil || !reflect.DeepEqual(got, test.want) { t.Fatalf("%s: %#v, %v", test.raw, got, err) } encoded, _ := json.Marshal(got) if string(encoded) == "null" { t.Fatal("missing authors must remain an array") } } for _, raw := range []string{`broken`, `{"unexpected":"object"}`, `[42]`, `[["a","b","c","d","e"]]`} { if _, err := decodeAuthors(raw); err == nil { t.Fatalf("accepted unsupported authors %s", raw) } } } func TestQueryValuesRemainBound(t *testing.T) { query := `graph'); DROP TABLE papers; -- OR author:*` where, args := whereClause(Filters{Query: query, Topic: "cs.LG", SearchIn: "text"}) if strings.Contains(where, query) || !reflect.DeepEqual(args, []any{query, "cs.LG"}) { t.Fatalf("unbound user input: %s %#v", where, args) } if !strings.Contains(where, "plainto_tsquery('english'::regconfig,$1)") || !strings.Contains(where, "t.value=$2") { t.Fatalf("unexpected search contract: %s", where) } where, args = whereClause(Filters{Query: "Ada Smith", SearchIn: "author"}) if !strings.Contains(where, "p.authors_json") || strings.Contains(where, "p.abstract") || !reflect.DeepEqual(args, []any{"Ada Smith"}) { t.Fatalf("author search includes unrelated text: %s", where) } where, args = whereClause(Filters{Topic: "cs.AI"}) if !strings.Contains(where, "t.value=$1") || !reflect.DeepEqual(args, []any{"cs.AI"}) { t.Fatalf("topic-only binding: %s %#v", where, args) } where, args = whereClause(Filters{}) if where != "" || len(args) != 0 { t.Fatal("empty filters changed the corpus") } } func TestSchemaCannotEscapeQualification(t *testing.T) { for _, name := range []string{"", `research";DROP SCHEMA public;--`, "a.b", "Public", strings.Repeat("a", 37)} { if _, err := NewPostgresRepository(&sql.DB{}, name); err == nil { t.Fatalf("accepted schema %q", name) } } if _, err := NewPostgresRepository(nil, "research"); err == nil { t.Fatal("accepted nil database") } r, err := NewPostgresRepository(&sql.DB{}, "research_v2") if err != nil { t.Fatal(err) } got := r.query(`SELECT p.id FROM {schema}papers p JOIN {schema}source_records s ON s.paper_id=p.id`) if got != `SELECT p.id FROM "research_v2".papers p JOIN "research_v2".source_records s ON s.paper_id=p.id` { t.Fatalf("unqualified corpus tables: %s", got) } }