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.
107 lines
3.0 KiB
Go
107 lines
3.0 KiB
Go
package search
|
|
|
|
import (
|
|
"reflect"
|
|
"slices"
|
|
"testing"
|
|
)
|
|
|
|
func TestParse(t *testing.T) {
|
|
term := func(field Field, value string) Term {
|
|
return Term{
|
|
Field: field,
|
|
Value: value,
|
|
}
|
|
}
|
|
text := func(value string) Term {
|
|
return term(FieldText, value)
|
|
}
|
|
not := func(term Term) Term {
|
|
term.Negated = true
|
|
return term
|
|
}
|
|
none := []string{}
|
|
cases := []struct {
|
|
input string
|
|
terms []Term
|
|
unknown []string
|
|
}{
|
|
{"", nil, none},
|
|
{" ", nil, none},
|
|
{"cloud hosting", []Term{text("cloud"), text("hosting")}, none},
|
|
{`"road repairs" leeds`, []Term{text("road repairs"), text("leeds")}, none},
|
|
{`title:"road repairs"`, []Term{term(FieldTitle, "road repairs")}, none},
|
|
{"TITLE:road Buyer:leeds", []Term{term(FieldTitle, "road"), term(FieldBuyer, "leeds")}, none},
|
|
{"buyer_id:GB-1 cpv:72", []Term{term(FieldBuyerID, "GB-1"), term(FieldCPV, "72")}, none},
|
|
{"!cloud !cpv:food", []Term{not(text("cloud")), not(term(FieldCPV, "food"))}, none},
|
|
{`!title:"a b"`, []Term{not(term(FieldTitle, "a b"))}, none},
|
|
{"12:00", []Term{text("12:00")}, none},
|
|
{`"unclosed phrase`, []Term{text("unclosed phrase")}, none},
|
|
{`title: !!! "" ! cloud`, []Term{text("cloud")}, none},
|
|
{"foo:bar cloud Foo:baz foo:x baz:", []Term{text("cloud")}, []string{"foo", "Foo", "baz"}},
|
|
{"buyer_id:", nil, none},
|
|
}
|
|
for _, tc := range cases {
|
|
got := Parse(tc.input)
|
|
if !reflect.DeepEqual(got.Terms, tc.terms) {
|
|
t.Errorf("%q: terms %+v, want %+v", tc.input, got.Terms, tc.terms)
|
|
}
|
|
if !slices.Equal(got.UnknownPrompts, tc.unknown) {
|
|
t.Errorf(
|
|
"%q: unknown %q, want %q",
|
|
tc.input, got.UnknownPrompts, tc.unknown,
|
|
)
|
|
}
|
|
if got.UnknownPrompts == nil {
|
|
t.Errorf("%q: UnknownPrompts is nil", tc.input)
|
|
}
|
|
if got.MatchesAll() != (len(tc.terms) == 0) {
|
|
t.Errorf("%q: MatchesAll %v", tc.input, got.MatchesAll())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTermWordsAndCPVCode(t *testing.T) {
|
|
cases := []struct {
|
|
value string
|
|
words []string
|
|
code string
|
|
isCPV bool
|
|
}{
|
|
{"road-repairs", []string{"road", "repairs"}, "", false},
|
|
{"Café's", []string{"Café", "s"}, "", false},
|
|
{"72", []string{"72"}, "72", true},
|
|
{"72210000-5", []string{"72210000", "5"}, "72210000", true},
|
|
{"72-55", []string{"72", "55"}, "", false},
|
|
{"72a", []string{"72a"}, "", false},
|
|
}
|
|
for _, tc := range cases {
|
|
term := Term{
|
|
Field: FieldCPV,
|
|
Value: tc.value,
|
|
}
|
|
if got := term.Words(); !slices.Equal(got, tc.words) {
|
|
t.Errorf("%q: words %q, want %q", tc.value, got, tc.words)
|
|
}
|
|
code, ok := term.CPVCode()
|
|
if code != tc.code || ok != tc.isCPV {
|
|
t.Errorf(
|
|
"%q: CPVCode %q %v, want %q %v",
|
|
tc.value, code, ok, tc.code, tc.isCPV,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Every known prompt has help, and every prompt with help is known.
|
|
func TestPromptsHaveHelp(t *testing.T) {
|
|
if len(Prompts) != len(prompts) {
|
|
t.Errorf("%d prompts with help, %d known", len(Prompts), len(prompts))
|
|
}
|
|
for _, prompt := range Prompts {
|
|
if _, ok := prompts[prompt.Name]; !ok || prompt.Help == "" {
|
|
t.Errorf("prompt %q: known %v, help %q", prompt.Name, ok, prompt.Help)
|
|
}
|
|
}
|
|
}
|