164 lines
7.1 KiB
Go
164 lines
7.1 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"slices"
|
|
"testing"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"textmachine/platform/internal/pgstore"
|
|
)
|
|
|
|
// canon is the ratified contract, read from this package's directory. The gates package reads it the
|
|
// same way and for the same reason: a second copy of a fact is one more place to forget.
|
|
const canon = "../../../docs/architecture/14-api-contract/openapi.yaml"
|
|
|
|
// ⛔ EVERY MEMBER THE CANON DECLARES REQUIRED IS ON THE WIRE, checked against the canon and not
|
|
// against a copy of its list. A Go tag proves a field can be marshalled, not that anything fills it:
|
|
// a projection that forgets one member serves a document a generated client refuses, and nothing in
|
|
// this build would have said so.
|
|
//
|
|
// Mutations this must catch: dropping a field from the projection; giving one `omitempty` so that its
|
|
// zero value vanishes; adding a member to the canon and not to the wire.
|
|
func TestEveryMemberTheCanonRequiresIsOnTheWire(t *testing.T) {
|
|
required := requiredOf(t, "OfferedTerm")
|
|
if len(required) != 12 {
|
|
t.Fatalf("the canon declares %d required members of OfferedTerm, expected 12 — this gate is "+
|
|
"reading the wrong schema or the schema changed and nobody read this test", len(required))
|
|
}
|
|
lib := &fakeLibrary{signingStop: pgstore.BankSigningStop{
|
|
Offered: []pgstore.BankOfferedTerm{{Src: "方源"}},
|
|
}}
|
|
w := call(t, readingServer(t, lib), "GET", "/v0/books/bk_1/bank/signing-stop", "")
|
|
var body struct {
|
|
Unreadable bool `json:"unreadable"`
|
|
Offered []map[string]json.RawMessage `json:"offered"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("the response is not a document: %v\n%s", err, w.Body.String())
|
|
}
|
|
if len(body.Offered) != 1 {
|
|
t.Fatalf("one term was offered and %d came back: %s", len(body.Offered), w.Body.String())
|
|
}
|
|
// The term above carries NOTHING but its surface, on purpose: a member that only appears when the
|
|
// store has a value for it is exactly the omission this gate exists to catch.
|
|
for _, member := range required {
|
|
if _, ok := body.Offered[0][member]; !ok {
|
|
t.Errorf("the canon requires `%s` on an offered term and the wire does not carry it: %s",
|
|
member, w.Body.String())
|
|
}
|
|
}
|
|
for member := range body.Offered[0] {
|
|
if !slices.Contains(required, member) {
|
|
t.Errorf("the wire carries `%s` on an offered term and the canon does not declare it", member)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ⛔ An absent measurement reaches the client as `null` and NEVER as zero. Measured on bought runs:
|
|
// `conventions` is absent from every row of one stop read-out and present on every row of another,
|
|
// and `freq: 0` is the service's own "only a translated passage saw it". A client shown 0 for either
|
|
// is shown a figure nobody took.
|
|
//
|
|
// Mutation this must catch: an `omitempty` on any of the four numbers, or a non-pointer type.
|
|
func TestAMeasurementNobodyTookReachesTheClientAsNullAndNotAsZero(t *testing.T) {
|
|
lib := &fakeLibrary{signingStop: pgstore.BankSigningStop{
|
|
Offered: []pgstore.BankOfferedTerm{{Src: "silent"}},
|
|
}}
|
|
w := call(t, readingServer(t, lib), "GET", "/v0/books/bk_1/bank/signing-stop", "")
|
|
var body struct {
|
|
Offered []map[string]any `json:"offered"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, member := range []string{"freq", "spread", "conventions", "confidence", "kind", "channel"} {
|
|
if got := body.Offered[0][member]; got != nil {
|
|
t.Errorf("`%s` was never measured and reached the client as %v", member, got)
|
|
}
|
|
}
|
|
// The three lists are collections, and an empty collection is an empty array — never null, which
|
|
// would ask a client to tell "no conflicts" from "not known" where only one of the two exists.
|
|
for _, member := range []string{"contradicts", "bank_holds", "variants"} {
|
|
got, ok := body.Offered[0][member].([]any)
|
|
if !ok || got == nil {
|
|
t.Errorf("`%s` reached the client as %v, want an empty array", member, body.Offered[0][member])
|
|
}
|
|
}
|
|
}
|
|
|
|
// ⛔ "Could not read the question" must not be byte-identical to "the stop asked nothing" — the one
|
|
// thing this surface may never say by accident, at the one moment the product stops to ask a person.
|
|
func TestAnUnreadableStopIsNotAStopThatAskedNothing(t *testing.T) {
|
|
unreadable := decode(t, call(t, readingServer(t, &fakeLibrary{
|
|
signingStop: pgstore.BankSigningStop{Unreadable: true},
|
|
}), "GET", "/v0/books/bk_1/bank/signing-stop", ""))
|
|
asked := decode(t, call(t, readingServer(t, &fakeLibrary{
|
|
signingStop: pgstore.BankSigningStop{Offered: []pgstore.BankOfferedTerm{}},
|
|
}), "GET", "/v0/books/bk_1/bank/signing-stop", ""))
|
|
if unreadable["unreadable"] != true {
|
|
t.Errorf("an unreadable stop answered %v", unreadable)
|
|
}
|
|
if asked["unreadable"] != false {
|
|
t.Errorf("a stop that asked nothing answered %v", asked)
|
|
}
|
|
if len(unreadable["offered"].([]any)) != 0 || len(asked["offered"].([]any)) != 0 {
|
|
t.Errorf("both answers should carry an empty list:\n %v\n %v", unreadable, asked)
|
|
}
|
|
}
|
|
|
|
// Completeness rides the whole-bank aggregates: the first page carries it, later pages do not, and
|
|
// `null` there means nothing measured it — never that the bank is whole.
|
|
func TestCompletenessRidesTheFirstPageAndItsAbsenceIsNotWholeness(t *testing.T) {
|
|
lib := &fakeLibrary{bank: pgstore.BankPage{First: true,
|
|
Consolidation: &pgstore.BankConsolidation{RenderBatchesDropped: 5, Unanswered: 47}}}
|
|
first := decode(t, call(t, readingServer(t, lib), "GET", "/v0/books/bk_1/bank", ""))
|
|
c, ok := first["consolidation"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("the first page carries no completeness: %v", first)
|
|
}
|
|
for _, member := range requiredOf(t, "BankConsolidation") {
|
|
if _, ok := c[member]; !ok {
|
|
t.Errorf("the canon requires `%s` on completeness and the wire does not carry it: %v", member, c)
|
|
}
|
|
}
|
|
if c["complete"] != false || c["render_batches_dropped"] != float64(5) || c["unanswered"] != float64(47) {
|
|
t.Errorf("the numbers were rewritten on the way out: %v", c)
|
|
}
|
|
nothing := decode(t, call(t, readingServer(t, &fakeLibrary{bank: pgstore.BankPage{First: true}}),
|
|
"GET", "/v0/books/bk_1/bank", ""))
|
|
if got, present := nothing["consolidation"]; present && got != nil {
|
|
t.Errorf("a bank nothing measured reported completeness as %v", got)
|
|
}
|
|
}
|
|
|
|
// requiredOf reads the `required` list of one schema from the ratified canon.
|
|
func requiredOf(t *testing.T, schema string) []string {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(canon)
|
|
if err != nil {
|
|
// Not skipped: a missing canon leaves these members with no check at all except a human
|
|
// noticing, which is the thing this gate replaces.
|
|
t.Fatalf("the ratified canon could not be read: %v", err)
|
|
}
|
|
var doc struct {
|
|
Components struct {
|
|
Schemas map[string]struct {
|
|
Required []string `yaml:"required"`
|
|
} `yaml:"schemas"`
|
|
} `yaml:"components"`
|
|
}
|
|
if err := yaml.Unmarshal(raw, &doc); err != nil {
|
|
t.Fatalf("the canon does not parse: %v", err)
|
|
}
|
|
s, ok := doc.Components.Schemas[schema]
|
|
if !ok {
|
|
t.Fatalf("the canon carries no schema %q: this gate reads the wrong document", schema)
|
|
}
|
|
if len(s.Required) == 0 {
|
|
t.Fatalf("the canon's %s declares nothing required: a gate over an empty list is a coin", schema)
|
|
}
|
|
return s.Required
|
|
}
|