Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/matching/candidate_search_test.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/matching/candidate_search_test.go |
| Original SHA-256 | bfba8ef73688cc81df4c0e847e3099a44cd36352eed5309ccd8998fb27ddb74a |
| Git blob SHA | 8712d87f7378953fb83e57c122564df384f85eb8 |
| Reading-copy SHA-256 | bfba8ef73688cc81df4c0e847e3099a44cd36352eed5309ccd8998fb27ddb74a |
Recovered source transcript
Download complete source text ↓211 lines · 7.5 KB
package matching
import (
"encoding/json"
"strings"
"testing"
)
// Normalize is what stands between stored user input and a query, so it is exercised with the values a
// person or an attacker actually supplies.
func TestNormalizeBoundsEverything(t *testing.T) {
huge := strings.Repeat("x", 5000)
terms := make([]string, 200)
for i := range terms {
terms[i] = "skill" + string(rune('a'+i%26)) + string(rune('a'+i/26))
}
q := CandidateQuery{
Text: huge, Location: huge, Skills: terms, ClearanceLevels: terms,
Limit: 100000, Offset: -5,
}
q.Normalize()
if len(q.Text) > 400 {
t.Errorf("text not bounded: %d", len(q.Text))
}
if len(q.Location) > 120 {
t.Errorf("location not bounded: %d", len(q.Location))
}
if len(q.Skills) > maxSearchTerms {
t.Errorf("skills not bounded: %d, cap is %d", len(q.Skills), maxSearchTerms)
}
if len(q.ClearanceLevels) > maxSearchTerms {
t.Errorf("clearance levels not bounded: %d", len(q.ClearanceLevels))
}
if q.Limit != maxSearchLimit {
t.Errorf("limit not clamped: %d", q.Limit)
}
if q.Offset != 0 {
t.Errorf("negative offset not corrected: %d", q.Offset)
}
// An empty query gets a workable default rather than zero, which would return nothing and read as an
// empty bench.
var empty CandidateQuery
empty.Normalize()
if empty.Limit != defaultSearchLimit {
t.Errorf("default limit is %d, expected %d", empty.Limit, defaultSearchLimit)
}
}
func TestNormalizeDedupesAndTrims(t *testing.T) {
q := CandidateQuery{Skills: []string{" Go ", "go", "GO", "", " ", "Rust"}}
q.Normalize()
if len(q.Skills) != 2 {
t.Fatalf("expected Go and Rust after dedupe, got %v", q.Skills)
}
if q.Skills[0] != "Go" {
t.Errorf("expected the trimmed first spelling to survive, got %q", q.Skills[0])
}
// Blanks must never survive: an empty skill term would match nothing and silently empty the result.
for _, s := range q.Skills {
if strings.TrimSpace(s) == "" {
t.Error("a blank term survived normalization")
}
}
}
func TestNegativeNumbersCorrected(t *testing.T) {
neg, negDays := -5, -30
q := CandidateQuery{MinYears: &neg, ClearanceExpiringDays: &negDays}
q.Normalize()
if q.MinYears == nil || *q.MinYears != 0 {
t.Errorf("negative min years should clamp to zero, got %v", q.MinYears)
}
if q.ClearanceExpiringDays != nil {
t.Error("a negative expiry window is meaningless and should be dropped, not queried")
}
}
// A saved search round trips through JSON and must come back the same, RE NORMALIZED. Stored criteria are
// user input that has been sitting in a table.
func TestCriteriaRoundTrip(t *testing.T) {
yes := true
min := 5
in := CandidateQuery{
Text: "network engineer", ClearanceLevels: []string{"TS/SCI"}, Skills: []string{"Cisco"},
Location: "Columbia, MD", Available: &yes, MinYears: &min, Limit: 77, Offset: 33,
}
b, err := MarshalCriteria(in)
if err != nil {
t.Fatalf("marshal: %v", err)
}
// PAGING IS NOT PART OF WHAT WAS SAVED. A search saved on page four should not reopen on page four.
if strings.Contains(string(b), `"limit"`) || strings.Contains(string(b), `"offset"`) {
t.Errorf("paging leaked into the saved criteria: %s", b)
}
out, err := UnmarshalCriteria(b)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if out.Text != in.Text || out.Location != in.Location {
t.Errorf("round trip lost fields: %+v", out)
}
if out.Available == nil || !*out.Available {
t.Error("round trip lost the available filter")
}
if out.MinYears == nil || *out.MinYears != 5 {
t.Error("round trip lost min years")
}
// It comes back normalized, so a limit is present and workable.
if out.Limit != defaultSearchLimit {
t.Errorf("criteria came back with limit %d, it must be re normalized", out.Limit)
}
}
// A corrupt blob must be an error the caller can handle, never a panic and never a silent empty query that
// would return the whole bench.
func TestCorruptCriteriaRefused(t *testing.T) {
for _, bad := range []string{`{not json`, `[1,2,3]`, `"a string"`} {
if _, err := UnmarshalCriteria([]byte(bad)); err == nil {
t.Errorf("corrupt criteria %q should be refused", bad)
}
}
// Empty is not corrupt, it is a search with no filters, and it must still come back normalized.
q, err := UnmarshalCriteria(nil)
if err != nil {
t.Errorf("empty criteria should be accepted: %v", err)
}
if q.Limit != 0 {
t.Errorf("a nil blob short circuits before normalize, got limit %d", q.Limit)
}
}
// The struct must serialise without the empty fields, so a saved search stays small and readable.
func TestCriteriaOmitsEmptyFields(t *testing.T) {
b, _ := MarshalCriteria(CandidateQuery{Text: "cleared"})
var m map[string]any
if err := json.Unmarshal(b, &m); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(m) != 1 {
t.Errorf("expected only the text field, got %v", m)
}
}
func TestParseUUID(t *testing.T) {
if _, ok := parseUUID(""); ok {
t.Error("an empty string is not an identifier")
}
if _, ok := parseUUID(" "); ok {
t.Error("whitespace is not an identifier")
}
if _, ok := parseUUID("'; DROP TABLE candidates--"); ok {
t.Error("an injection attempt must not parse as an identifier")
}
if _, ok := parseUUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8"); !ok {
t.Error("a valid identifier should parse")
}
}
// THE DEFECT THE SEARCH FOUND, locked so it cannot come back.
//
// The persisted value is the CODE, ts_sci; the interface shows the LABEL, TS/SCI. Storing accepted only the
// code and silently degraded anything else to "unknown", and searching compared raw input against the column.
// So a candidate saved with the label lost their clearance, and a recruiter searching with the label got an
// empty bench. Neither raised an error.
func TestClearanceLabelsAndCodesBothResolve(t *testing.T) {
cases := map[string]string{
"ts_sci": "ts_sci", "TS/SCI": "ts_sci", "ts/sci": "ts_sci", "TS SCI": "ts_sci", "tssci": "ts_sci",
"top secret/sci": "ts_sci",
"top_secret": "top_secret", "Top Secret": "top_secret", "TS": "top_secret",
"secret": "secret", "Secret": "secret", "S": "secret",
"confidential": "confidential", "Confidential": "confidential",
"none": "none", "None": "none",
}
for in, want := range cases {
got, ok := clearanceLevelCode(in)
if !ok {
t.Errorf("clearanceLevelCode(%q) was not recognised, it must resolve to %q", in, want)
continue
}
if got != want {
t.Errorf("clearanceLevelCode(%q) = %q, want %q", in, got, want)
}
}
// STORING AND SEARCHING SHARE THE TRANSLATION, so what can be saved and what can be found are the same
// vocabulary by construction rather than by two lists somebody keeps in step.
for in, want := range cases {
if got := validLevelCode(in); got != want {
t.Errorf("validLevelCode(%q) = %q, want %q, storing and searching have drifted", in, got, want)
}
}
}
// Anything unrecognised is DROPPED from a filter rather than passed through, because an unmatchable term
// silently empties the result set, which looks exactly like a real answer.
func TestUnrecognisedClearanceDropped(t *testing.T) {
got := canonicalClearanceLevels([]string{"TS/SCI", "nonsense", "", " ", "Secret", "ts_sci"})
if len(got) != 2 {
t.Fatalf("expected ts_sci and secret after canonicalisation and dedupe, got %v", got)
}
if got[0] != "ts_sci" || got[1] != "secret" {
t.Errorf("unexpected canonical set: %v", got)
}
if len(canonicalClearanceLevels([]string{"nonsense", "invalid"})) != 0 {
t.Error("a filter of only unrecognised terms must produce no predicate at all")
}
// Status has the same contract.
if s := canonicalClearanceStatus([]string{"active", "ACTIVE", "bogus"}); len(s) != 1 || s[0] != "active" {
t.Errorf("status canonicalisation wrong: %v", s)
}
}