package search import ( "path/filepath" "strings" "testing" ) // TestMatchExpressionIsWordsOnly is the security-relevant half of search: FTS5 // syntax in visitor input is never honoured, so a query cannot shape the index // beyond words. It also means "zigbee)" is not an error page. func TestMatchExpressionIsWordsOnly(t *testing.T) { cases := []struct{ in, want string }{ {"zigbee", `"zigbee"*`}, {"Zigbee Routers", `"zigbee"* AND "routers"*`}, {`zigbee" OR body:`, `"zigbee"* AND "or"* AND "body"*`}, {`NEAR(a b)`, `"near"* AND "a"* AND "b"*`}, {` `, ``}, {`!!!`, ``}, {"snapcast-audio", `"snapcast-audio"*`}, } for _, tc := range cases { if got := MatchExpression(tc.in); got != tc.want { t.Errorf("MatchExpression(%q) = %q, want %q", tc.in, got, tc.want) } } } // TestBuildAndQuery is the ยง2.5 DoD check: the FTS5 index returns sensible // results for the three terms the phase document names. func TestBuildAndQuery(t *testing.T) { indexFile := filepath.Join(t.TempDir(), "search.db") docs := []Doc{ {ID: "edge", URL: "/edge/", Title: "Edge & Devices", Area: "edge", Level: 1, Confidence: "running", Body: "Twenty nodes on constrained hardware. Zigbee routers and a LoRa link."}, {ID: "platform", URL: "/platform/", Title: "Platform & Infrastructure", Area: "platform", Level: 1, Confidence: "running", Body: "Declarative hosts, networking and storage."}, {ID: "products", URL: "/products/", Title: "Products", Area: "products", Level: 1, Confidence: "delivered", Body: "Commercial signage on commodity hardware."}, {ID: "ai", URL: "/ai/", Title: "AI & Agentic Systems", Area: "ai", Level: 1, Confidence: "running", Body: "Model routing and cost control through a single gateway."}, } if err := Build(indexFile, docs); err != nil { t.Fatalf("Build: %v", err) } idx, err := Open(indexFile) if err != nil { t.Fatalf("Open: %v", err) } defer idx.Close() if n, err := idx.Count(); err != nil || n != len(docs) { t.Fatalf("Count = %d, %v; want %d", n, err, len(docs)) } cases := []struct{ q, wantURL string }{ {"zigbee", "/edge/"}, {"signage", "/products/"}, {"routing", "/ai/"}, {"ZIGBEE", "/edge/"}, } for _, tc := range cases { results, err := idx.Query(tc.q, 10) if err != nil { t.Errorf("Query(%q): %v", tc.q, err) continue } if len(results) == 0 { t.Errorf("Query(%q) found nothing", tc.q) continue } found := false for _, r := range results { if r.URL == tc.wantURL { found = true } } if !found { t.Errorf("Query(%q) did not return %s; got %+v", tc.q, tc.wantURL, results) } if !strings.Contains(results[0].Snippet, "") { t.Errorf("Query(%q) snippet has no highlight: %q", tc.q, results[0].Snippet) } } if results, err := idx.Query("!!!", 10); err != nil || len(results) != 0 { t.Errorf("Query(no words) = %v, %v; want no results and no error", results, err) } }