package http import ( "net/url" "strings" "testing" ) // Review PERF-2: page search treats % and _ literally and is backed by the // trigram indexes introduced alongside the escaping change. func TestPageSearchEscapesWildcards(t *testing.T) { srv, d := testServerDB(t) alice, _ := accountClient(t, srv.URL, "pagesearch@computer.test") other, _ := accountClient(t, srv.URL, "pagesearch-other@computer.test") alice.request("POST", "/api/pages", map[string]any{"title": "100% done", "content": "literal percent"}, 201) alice.request("POST", "/api/pages", map[string]any{"title": "100x done", "content": "not a percent"}, 201) alice.request("POST", "/api/pages", map[string]any{"title": "snake_case", "content": "underscore"}, 201) alice.request("POST", "/api/pages", map[string]any{"title": "snakeXcase", "content": "not an underscore"}, 201) other.request("POST", "/api/pages", map[string]any{"title": "100% private", "content": "someone else"}, 201) search := func(term string) []map[string]any { return alice.getJSON("/api/pages?search="+url.QueryEscape(term), 200) } if got := search("100%"); len(got) != 1 || got[0]["title"] != "100% done" { t.Fatalf("literal %% search returned %v", got) } if got := search("snake_"); len(got) != 1 || got[0]["title"] != "snake_case" { t.Fatalf("literal _ search returned %v", got) } if got := search("100x"); len(got) != 1 || got[0]["title"] != "100x done" { t.Fatalf("plain substring search returned %v", got) } for _, idx := range []string{"Page_title_trgm_idx", "Page_content_trgm_idx"} { var n int if e := d.QueryRow(`SELECT count(*) FROM pg_indexes WHERE schemaname=current_schema() AND indexname=$1`, idx).Scan(&n); e != nil || n != 1 { t.Fatalf("missing page search index %s: n=%d err=%v", idx, n, e) } } } // Review "cap input sizes": per-field limits reject absurd values with a 400 // instead of persisting them (the 1 MiB body cap alone is too coarse). func TestTextFieldSizeLimits(t *testing.T) { srv := testServer(t) alice, _ := accountClient(t, srv.URL, "limits@computer.test") const ( urlLimit = 2048 titleLimit = 500 tagNameLimit = 80 contentLimit = 512 << 10 ) alice.request("POST", "/api/bookmarks", map[string]any{ "url": "https://example.org/" + strings.Repeat("u", urlLimit), "title": "too long", }, 400) alice.request("POST", "/api/bookmarks", map[string]any{ "url": "https://example.org/ok", "title": strings.Repeat("t", titleLimit+1), }, 400) alice.request("POST", "/api/tags", map[string]any{"name": strings.Repeat("n", tagNameLimit+1)}, 400) alice.request("POST", "/api/pages", map[string]any{ "title": "huge", "content": strings.Repeat("x", contentLimit+1), }, 400) // The boundary itself remains accepted. if got := alice.request("POST", "/api/tags", map[string]any{"name": strings.Repeat("n", tagNameLimit)}, 201); got["name"] != strings.Repeat("n", tagNameLimit) { t.Fatal("boundary-length tag rejected or altered") } }