Replaced the Title filter with a general search, run by the backend
GET /api/opportunities/search?q= returns the UUIDs of the matching opportunities, and the frontend filters its list by them. Words without a prompt search the title and description; title:, buyer:, buyer_id: and cpv: search only their own field, and a leading ! leaves out what a term matches. Repeats of one prompt are OR, everything else AND. CPV terms are checked per code: cpv:72 !cpv:7221 needs a code starting 72 that doesn't start 7221. Unknown prompts are reported, and every response carries the help that the search box's "?" panel shows. Migration 00011 adds the description and buyer name to the full-text index, and indexes CPV descriptions.
This commit is contained in:
1 parent
0a777792d8
commit
b0230e6847
29 files changed
+1913
-43
No files matched your search
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -483,3 +484,54 @@ func TestChangeMarkPublishes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GET /opportunities/search returns the matching UUIDs, bounded by
|
||||
// released_from, with the help; a search with nothing to search for matches
|
||||
// all, and unknown prompts are listed.
|
||||
func TestSearchOpportunities(t *testing.T) {
|
||||
url := opportunitiesServer(t, 5) + "/opportunities/search"
|
||||
from3 := "&released_from=2026-10-03T00:00:00Z"
|
||||
cases := []struct {
|
||||
query string
|
||||
uuids []string
|
||||
matchesAll bool
|
||||
unknown []string
|
||||
}{
|
||||
{"", []string{}, true, []string{}},
|
||||
{"?q=", []string{}, true, []string{}},
|
||||
{"?q=cpv:72", []string{"ocds-1", "ocds-3", "ocds-5"}, false, []string{}},
|
||||
{"?q=cpv:72" + from3, []string{"ocds-3", "ocds-5"}, false, []string{}},
|
||||
{"?q=opportunity%20!cpv:72", []string{"ocds-2", "ocds-4"}, false, []string{}},
|
||||
{"?q=nothing", []string{}, false, []string{}},
|
||||
{"?q=foo:x%20Bar:y", []string{}, true, []string{"foo", "Bar"}},
|
||||
{"?q=foo:x%20title:3", []string{"ocds-3"}, false, []string{"foo"}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
var got OpportunitySearch
|
||||
status := request(t, http.MethodGet, url+tc.query, &got)
|
||||
slices.Sort(got.UUIDs)
|
||||
if status != http.StatusOK || !slices.Equal(got.UUIDs, tc.uuids) ||
|
||||
got.MatchesAll != tc.matchesAll ||
|
||||
!slices.Equal(got.UnknownPrompts, tc.unknown) {
|
||||
t.Errorf("%s: status %d, %+v; want %v, matches_all %v, unknown %v",
|
||||
tc.query, status, got, tc.uuids, tc.matchesAll, tc.unknown)
|
||||
}
|
||||
if len(got.Prompts) == 0 || len(got.Syntax) == 0 {
|
||||
t.Errorf("%s: no help: %+v", tc.query, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Empty lists are [], never null.
|
||||
var raw map[string]json.RawMessage
|
||||
request(t, http.MethodGet, url+"?q=nothing", &raw)
|
||||
for _, field := range []string{"uuids", "unknown_prompts"} {
|
||||
if string(raw[field]) != "[]" {
|
||||
t.Errorf("%s = %s; want []", field, raw[field])
|
||||
}
|
||||
}
|
||||
|
||||
status := request(t, http.MethodGet, url+"?q=x&released_from=x", nil)
|
||||
if status != http.StatusBadRequest {
|
||||
t.Errorf("bad released_from: status %d; want 400", status)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user