package api
import (
"errors"
"strings"
"testing"
"craftdesign.group/quarry/internal/ops"
)
// A field value the repository's vocabulary does not list is refused.
//
// ticket.set stored whatever it was handed. Measured on the forge:
// status="Bogus_Not_A_Status" answered 200 and the ticket then read
// Bogus_Not_A_Status — a state no filter offers and no reader expects
// (7d46f782).
//
// The refusal names the words that would have worked, because a caller
// who guessed one value wrong will otherwise guess again. Same
// reasoning as the unknown-JSON-field refusal: "that is wrong" without
// "here is the set" costs a second round trip and often a second wrong
// guess.
func TestAValueOutsideTheVocabularyIsRefused(t *testing.T) {
vocab := map[string][]string{
"status": {"Open", "Closed"},
"severity": {"Severe", "Minor"},
"subsystem": {}, // free text, by default
}
err := refuseUnknownVocab(vocab, map[string]string{"status": "Bogus_Not_A_Status"})
if !errors.Is(err, ops.ErrInvalid) {
t.Fatalf("err = %v, want ErrInvalid", err)
}
for _, want := range []string{"Bogus_Not_A_Status", "Open", "Closed"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("the refusal does not mention %q: %v", want, err)
}
}
}
// The values the vocabulary DOES list still go through, and a field the
// vocabulary leaves empty is free text rather than a closed set.
//
// This is the half that must not narrow: subsystem has no default
// words, so checking it would refuse every subsystem anyone has ever
// used. A field the vocabulary does not mention at all is likewise none
// of this check's business.
func TestTheVocabularyDoesNotRefuseWhatItAllows(t *testing.T) {
vocab := map[string][]string{
"status": {"Open", "Closed"},
"subsystem": {},
}
for _, fields := range []map[string]string{
{"status": "Open"},
{"status": ""}, // not being set
{"subsystem": "anything at all"}, // free text
{"assignee": "someone"}, // not a vocabulary field
{"status": "Closed", "subsystem": "ci"}, // both kinds at once
} {
if err := refuseUnknownVocab(vocab, fields); err != nil {
t.Errorf("%v was refused: %v", fields, err)
}
}
}
// A vocabulary that lists nothing for a field cannot refuse anything
// for it — including when the whole vocabulary is missing, which is
// what a repository with no store answers.
func TestAnEmptyVocabularyRefusesNothing(t *testing.T) {
if err := refuseUnknownVocab(nil, map[string]string{"status": "whatever"}); err != nil {
t.Errorf("an absent vocabulary refused a value: %v", err)
}
if err := refuseUnknownVocab(map[string][]string{}, map[string]string{"status": "whatever"}); err != nil {
t.Errorf("an empty vocabulary refused a value: %v", err)
}
}
// The defaults themselves must pass their own check, or every write
// against a repository with no .quarry/tickets.json is refused.
func TestTheDefaultVocabularyAcceptsItsOwnWords(t *testing.T) {
for field, words := range DefaultTicketVocabulary {
for _, w := range words {
if err := refuseUnknownVocab(DefaultTicketVocabulary, map[string]string{field: w}); err != nil {
t.Errorf("the default vocabulary refuses its own %s %q: %v", field, w, err)
}
}
}
}