125 lines
5.7 KiB
Go
125 lines
5.7 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"textmachine/platform/internal/money"
|
|
"textmachine/platform/internal/pgstore"
|
|
)
|
|
|
|
// ⛔ THE VERSION GATE COMPARES A NUMBER, NEVER A SHAPE, so a minor that announces two new members has
|
|
// to be checked by asking what this service actually SERVES. That is this test: the canon's 0.17.0
|
|
// carries `progress_lagging` and `rebill_consent_micro_usd`, and a build whose projection dropped
|
|
// either would announce a contract it does not honour with nothing red (the class PD-399 names, whose
|
|
// lesson is that a matching version number is not a matching form).
|
|
//
|
|
// It asserts the JSON, not the Go struct: the members' names, their presence when the value is the
|
|
// zero one, and the two different meanings of «nothing» the canon gives them — a boolean that is
|
|
// always sent because absence would be a second way of writing `false`, and a sum that is NULL
|
|
// because a zero there would read as a consent of nothing.
|
|
func TestTheRunTheWireServesCarriesBothMembersTheMinorAdded(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
run pgstore.Run
|
|
wantLagging bool
|
|
wantConsent any
|
|
}{
|
|
{"a run whose projection is keeping up and carries no consent",
|
|
pgstore.Run{Status: "translating"}, false, nil},
|
|
{"a run whose figures are behind and whose resume granted a consent",
|
|
pgstore.Run{Status: "translating", ProgressLagging: true, RebillConsent: money.MicroUSD(2_241_631)},
|
|
true, float64(2_241_631)},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
body, err := json.Marshal(projectRun(tc.run))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got map[string]any
|
|
if err := json.Unmarshal(body, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lagging, ok := got["progress_lagging"]
|
|
if !ok {
|
|
t.Fatalf("the run this service serves carries no `progress_lagging` at all: %s", body)
|
|
}
|
|
if lagging != tc.wantLagging {
|
|
t.Errorf("progress_lagging is %v, want %v", lagging, tc.wantLagging)
|
|
}
|
|
consent, ok := got["rebill_consent_micro_usd"]
|
|
if !ok {
|
|
t.Fatalf("the run carries no `rebill_consent_micro_usd` member: %s", body)
|
|
}
|
|
if consent != tc.wantConsent {
|
|
t.Errorf("rebill_consent_micro_usd is %#v, want %#v", consent, tc.wantConsent)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ⛔ EVERY MEMBER THIS SERVICE ALWAYS SENDS IS EITHER `required` IN THE CANON OR A NAMED EXCEPTION.
|
|
//
|
|
// The case that produced this gate: `progress_lagging` shipped with a description saying «always
|
|
// sent, `false` when …» and was NOT in `Run.required`, so a client generated from the canon would
|
|
// have made it optional — the canon contradicting itself on one property. That is the mirror of the
|
|
// corrective minor 0.13.1, where `required` demanded a member the schema no longer had; and a
|
|
// reviewer found it by reading, which is the part worth replacing with a gate.
|
|
//
|
|
// ⚠ IT IS A CLASS AND NOT THE CASE, which is why it walks the wire's own JSON instead of naming
|
|
// `progress_lagging`: the defect is «a member the wire always emits that the contract does not
|
|
// promise», and the next such member will be added by somebody who has never read this file.
|
|
//
|
|
// ⛔ THE EXCEPTIONS ARE A ROSTER WITH REASONS, not a tolerance. Four of them are the canon's own
|
|
// standing decision — its `Run.required` block says in as many words that the nullability of those
|
|
// four is the platform zone's to decide — and the fifth is this pack's, for the opposite reason from
|
|
// `progress_lagging`'s: a consent that does not stand has no value to send, and `null` IS that
|
|
// statement. A member added to this roster is a member somebody deliberately excused, which is
|
|
// exactly the ceremony a closed contract deserves.
|
|
func TestEveryAlwaysSentMemberOfARunIsPromisedByTheCanon(t *testing.T) {
|
|
excused := map[string]string{
|
|
"ordered_chapters": "canon: nullability left to the platform zone (exactly one of it and ordered_units is set)",
|
|
"ordered_units": "canon: the other half of that pair",
|
|
"delivered_chapters": "canon: null means «read the bar as units», which is a statement and not an absence",
|
|
"term_consistency_funded": "canon: left out with the three above, same decision",
|
|
"rebill_consent_micro_usd": "this pack: nullable by design — a consent that does not stand has no " +
|
|
"figure, and null is how the wire says so",
|
|
}
|
|
// The zero value on purpose: what a run ALWAYS carries is what survives having nothing set.
|
|
body, err := json.Marshal(projectRun(pgstore.Run{}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var sent map[string]any
|
|
if err := json.Unmarshal(body, &sent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(sent) < 10 {
|
|
t.Fatalf("the wire's run carries %d members: this gate reads the wrong shape, and over a short "+
|
|
"map it would pass by having nothing to check", len(sent))
|
|
}
|
|
promised := map[string]bool{}
|
|
for _, m := range requiredOf(t, "Run") {
|
|
promised[m] = true
|
|
}
|
|
for member := range sent {
|
|
if promised[member] || excused[member] != "" {
|
|
continue
|
|
}
|
|
t.Errorf("the wire always sends `%s` and the canon neither requires it nor excuses it: a client "+
|
|
"generated from the contract will treat it as optional, which is a promise this build keeps "+
|
|
"and the contract does not make", member)
|
|
}
|
|
// And the roster is kept honest in the other direction: an excused member that the canon has since
|
|
// promised is a line nobody needs, and an excused member the wire no longer sends is a reason
|
|
// nobody can check.
|
|
for member, why := range excused {
|
|
if promised[member] {
|
|
t.Errorf("`%s` is excused here AND required in the canon: the roster's line is stale (%s)", member, why)
|
|
}
|
|
if _, ok := sent[member]; !ok {
|
|
t.Errorf("`%s` is excused here and the wire does not send it at all: the reason (%s) is about "+
|
|
"a member that no longer exists", member, why)
|
|
}
|
|
}
|
|
}
|