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.
181 lines
5.2 KiB
Go
181 lines
5.2 KiB
Go
package store_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.avsdev.uk/Go/job-search/internal/search"
|
|
"git.avsdev.uk/Go/job-search/internal/store"
|
|
"git.avsdev.uk/Go/job-search/internal/types"
|
|
)
|
|
|
|
// searchStore returns a store holding five opportunities, s1 to s5, released
|
|
// on days 1 to 5, for the search tests.
|
|
func searchStore(t *testing.T) *store.Store {
|
|
t.Helper()
|
|
s := openStore(t)
|
|
const software = "Programming services of packaged software products"
|
|
cpv := func(code, description string) types.ClassificationCode {
|
|
return types.ClassificationCode{
|
|
IsCPV: true,
|
|
Code: code,
|
|
Classification: description,
|
|
}
|
|
}
|
|
rows := []struct {
|
|
title, description, buyer, buyerID string
|
|
classifications []types.ClassificationCode
|
|
}{
|
|
{
|
|
title: "Cloud migration services",
|
|
description: "Moving servers to the cloud",
|
|
buyer: "Leeds City Council",
|
|
buyerID: "GB-LEEDS",
|
|
classifications: []types.ClassificationCode{
|
|
cpv("72100000", "Hardware consultancy services"),
|
|
cpv("72210000", software),
|
|
},
|
|
},
|
|
{
|
|
title: "Road repairs",
|
|
description: "Pothole repairs in Bradford",
|
|
buyer: "Bradford Council",
|
|
buyerID: "GB-BRAD",
|
|
classifications: []types.ClassificationCode{
|
|
cpv("45233141", "Road-maintenance works"),
|
|
},
|
|
},
|
|
{
|
|
title: "School meals",
|
|
description: "Catering and cloud-based ordering",
|
|
buyer: "Leeds Schools Trust",
|
|
buyerID: "GB-LST",
|
|
classifications: []types.ClassificationCode{
|
|
cpv("55524000", "School catering services"),
|
|
cpv("15800000", "Miscellaneous food products"),
|
|
},
|
|
},
|
|
{
|
|
title: "Software licences",
|
|
description: "Licences",
|
|
buyer: "York Council",
|
|
buyerID: "GB-YORK",
|
|
classifications: []types.ClassificationCode{
|
|
cpv("72210000", software),
|
|
},
|
|
},
|
|
{
|
|
title: "Office cleaning",
|
|
description: "Cleaning",
|
|
buyer: "Hull Council",
|
|
buyerID: "GB-HULL",
|
|
},
|
|
}
|
|
for i, row := range rows {
|
|
o := opportunity(fmt.Sprintf("s%d", i+1), row.title, i+1, "r1")
|
|
o.Description = row.description
|
|
o.BuyerName = row.buyer
|
|
o.BuyerID = row.buyerID
|
|
o.Classifications = row.classifications
|
|
mustSave(t, s, o)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestSearchOpportunities(t *testing.T) {
|
|
s := searchStore(t)
|
|
all := []string{"s1", "s2", "s3", "s4", "s5"}
|
|
cases := []struct {
|
|
query string
|
|
want []string
|
|
}{
|
|
{"", all},
|
|
{"title: !!! \"\"", all},
|
|
{"cloud", []string{"s1", "s3"}},
|
|
{"CLOUD", []string{"s1", "s3"}},
|
|
{"migr", []string{"s1"}},
|
|
{"migrating", []string{"s1"}},
|
|
{"cloud servers", []string{"s1"}},
|
|
{"leeds", []string{}},
|
|
{"bradford", []string{"s2"}},
|
|
{`"road repairs"`, []string{"s2"}},
|
|
{`"repairs road"`, []string{}},
|
|
{"title:cloud", []string{"s1"}},
|
|
{"title:pothole", []string{}},
|
|
{"title:cloud title:road", []string{"s1", "s2"}},
|
|
{"buyer:leeds", []string{"s1", "s3"}},
|
|
{"buyer:leeds buyer:bradford", []string{"s1", "s2", "s3"}},
|
|
{`buyer:"leeds city"`, []string{"s1"}},
|
|
{"cloud buyer:leeds", []string{"s1", "s3"}},
|
|
{"title:cloud buyer:leeds", []string{"s1"}},
|
|
{`cloud !buyer:"leeds city"`, []string{"s3"}},
|
|
{"!cloud", []string{"s2", "s4", "s5"}},
|
|
{"!cloud !road", []string{"s4", "s5"}},
|
|
{"buyer_id:gb-leeds", []string{"s1"}},
|
|
{"buyer_id:GB-LEEDS buyer_id:gb-york", []string{"s1", "s4"}},
|
|
{"buyer_id:GB", []string{}},
|
|
{"!buyer_id:GB-LEEDS", []string{"s2", "s3", "s4", "s5"}},
|
|
{"cpv:72", []string{"s1", "s4"}},
|
|
{"cpv:72210000-5", []string{"s1", "s4"}},
|
|
{"cpv:72 !cpv:7221", []string{"s1"}},
|
|
{"!cpv:7221", []string{"s2", "s3", "s5"}},
|
|
{"cpv:food", []string{"s3"}},
|
|
{"!cpv:food", []string{"s1", "s2", "s4", "s5"}},
|
|
{"cpv:catering cpv:road", []string{"s2", "s3"}},
|
|
{"cpv:software !cpv:hardware", []string{"s1", "s4"}},
|
|
{"cpv:72 buyer:york", []string{"s4"}},
|
|
{"foo:cloud", all},
|
|
{`cloud" OR title:road*`, []string{}},
|
|
}
|
|
for _, tc := range cases {
|
|
got, err := s.SearchOpportunities(
|
|
context.Background(), search.Parse(tc.query), time.Time{},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("%q: %v", tc.query, err)
|
|
}
|
|
slices.Sort(got)
|
|
if !slices.Equal(got, tc.want) {
|
|
t.Errorf("%q: got %v, want %v", tc.query, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// released_from bounds the search as it bounds ListOpportunities, and
|
|
// changing a searched field updates the index.
|
|
func TestSearchOpportunitiesReleasedFromAndUpdates(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := searchStore(t)
|
|
|
|
got, err := s.SearchOpportunities(ctx, search.Parse("buyer:leeds"), day(3))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !slices.Equal(got, []string{"s3"}) {
|
|
t.Errorf("released from day 3: got %v, want [s3]", got)
|
|
}
|
|
|
|
o := opportunity("s5", "Office cleaning", 6, "r1", "r2")
|
|
o.Description = "Window washing"
|
|
o.BuyerName = "Wakefield Council"
|
|
mustSave(t, s, o)
|
|
for query, want := range map[string][]string{
|
|
"washing": {"s5"},
|
|
"buyer:wakefield": {"s5"},
|
|
"cleaning": {"s5"},
|
|
"buyer:hull": {},
|
|
"cpv:agricultural": {"s5"},
|
|
} {
|
|
got, err := s.SearchOpportunities(ctx, search.Parse(query), time.Time{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !slices.Equal(got, want) {
|
|
t.Errorf("%q after update: got %v, want %v", query, got, want)
|
|
}
|
|
}
|
|
}
|