Files
mesh-delivery/contracts/go/clients/meshdeliveryclient/gate_test.go
T

187 lines
9.8 KiB
Go

package meshdeliveryclient
import (
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
"time"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// sameJSON says whether two documents hold the same values (the generated types write their fields in alphabetical
// order, the old package's in declaration order: the bytes differ, the documents do not).
func sameJSON(t *testing.T, got []byte, want string) {
t.Helper()
var g, w any
if err := json.Unmarshal(got, &g); err != nil {
t.Fatalf("%s: %v", got, err)
}
if err := json.Unmarshal([]byte(want), &w); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(g, w) {
t.Errorf("got %s\nwant %s", got, want)
}
}
// The goldens are the output of go/delivery's Encode* before it was deleted; each value must encode to the same
// document and decode back to what it was.
func TestGateValuesEncodeAsGoDeliveryDid(t *testing.T) {
ap := time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)
wait := ap.Add(2 * time.Minute)
for _, c := range []struct {
name string
encode func() ([]byte, error)
decode func([]byte) (any, error)
value any
golden string
}{
{"source", func() ([]byte, error) {
return EncodeSource(seat.SourceByMainValue{Group: "fix/1.2", Heads: map[string]string{"novox/a": "aaaa", "novox/b": "bbbb"}})
}, func(b []byte) (any, error) { return DecodeSource(b) },
seat.SourceByMainValue{V: 1, Group: "fix/1.2", Heads: map[string]string{"novox/a": "aaaa", "novox/b": "bbbb"}},
`{"v":1,"group":"fix/1.2","heads":{"novox/a":"aaaa","novox/b":"bbbb"}}`},
{"merge", func() ([]byte, error) {
return EncodeMerge(seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab", Approved: map[string]string{"novox/a@7": "h1"},
Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 7, Head: "h1", State: "merged", Commit: "c1"}}})
}, func(b []byte) (any, error) { return DecodeMerge(b) },
seat.MergesByGroupValue{V: 1, Group: "g", Ask: "merge.g.0123456789ab", Approved: map[string]string{"novox/a@7": "h1"},
Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 7, Head: "h1", State: "merged", Commit: "c1"}}},
`{"v":1,"group":"g","ask":"merge.g.0123456789ab","approved":{"novox/a@7":"h1"},"pulls":[{"repository":"novox/a","number":7,"head":"h1","state":"merged","commit":"c1"}]}`},
{"ask", func() ([]byte, error) {
return EncodeAsk(seat.AsksByMergeGroupHeadsValue{Group: "g", Asking: 2, Ask: json.RawMessage(`{"headline":"x"}`),
Heads: []seat.AsksByMergeGroupHeadsValueHeadsItem{{Repository: "novox/a", Number: 7, Head: "h1"}}})
}, func(b []byte) (any, error) { return DecodeAsk(b) },
seat.AsksByMergeGroupHeadsValue{V: 1, Group: "g", Asking: 2, Ask: map[string]any{"headline": "x"},
Heads: []seat.AsksByMergeGroupHeadsValueHeadsItem{{Repository: "novox/a", Number: 7, Head: "h1"}}},
`{"v":1,"group":"g","heads":[{"repository":"novox/a","number":7,"head":"h1"}],"ask":{"headline":"x"},"asking":2}`},
{"owed", func() ([]byte, error) {
return EncodeOwed(seat.OwedByRepositoryNumberHeadActValue{Act: OwedStatus, Context: StatusContextCheck, Target: "t", State: "pending"})
}, func(b []byte) (any, error) { return DecodeOwed(b) },
seat.OwedByRepositoryNumberHeadActValue{V: 1, Act: OwedStatus, Context: StatusContextCheck, Target: "t", State: "pending"},
`{"v":1,"act":"status","context":"mesh/check","target":"t","state":"pending"}`},
{"turn", func() ([]byte, error) {
return EncodeTurn(seat.TurnsByRepositoryValue{Repository: "novox/a", Waiting: []seat.TurnsByRepositoryValueWaitingItem{{
Group: "g", Heads: "0123456789ab", Ask: "k", Approved: ap, Since: ap.Add(time.Minute), WaitsFor: "g.0123456789ab", WaitsSince: &wait, Step: "s"}}})
}, func(b []byte) (any, error) { return DecodeTurn(b) },
seat.TurnsByRepositoryValue{V: 1, Repository: "novox/a", Waiting: []seat.TurnsByRepositoryValueWaitingItem{{
Group: "g", Heads: "0123456789ab", Ask: "k", Approved: ap, Since: ap.Add(time.Minute), WaitsFor: "g.0123456789ab", WaitsSince: &wait, Step: "s"}}},
`{"v":1,"repository":"novox/a","waiting":[{"group":"g","heads":"0123456789ab","ask":"k","approved":"2026-11-05T10:00:00Z","since":"2026-11-05T10:01:00Z","waits-for":"g.0123456789ab","waits-since":"2026-11-05T10:02:00Z","step":"s"}]}`},
{"answer", func() ([]byte, error) { return EncodeAnswer(Answer{Option: "merge", Warrant: "w", Words: "ok"}) },
func(b []byte) (any, error) { return DecodeAnswer(b) }, Answer{V: 1, Option: "merge", Warrant: "w", Words: "ok"},
`{"v":1,"option":"merge","warrant":"w","words":"ok"}`},
} {
raw, err := c.encode()
if err != nil {
t.Fatalf("%s: %v", c.name, err)
}
sameJSON(t, raw, c.golden)
back, err := c.decode([]byte(c.golden))
if err != nil || !reflect.DeepEqual(back, c.value) {
t.Errorf("%s decoded %+v, %v; want %+v", c.name, back, err, c.value)
}
}
}
func TestGateKeysAsGoDeliveryHadThem(t *testing.T) {
for in, want := range map[string]string{"novox/Mesh-sdk": "novox.mesh-sdk", "novox/mesh-sdk": "novox.mesh-sdk", "novox/mesh.x": "novox.mesh=2ex"} {
got, err := TurnsKey(in)
if err != nil || got != want {
t.Errorf("TurnsKey(%q) = %q, %v; want %q", in, got, err, want)
}
}
if r, err := ParseTurnsKey("novox.mesh=2ex"); err != nil || r != "novox/mesh.x" {
t.Errorf("%q %v", r, err)
}
for _, bad := range []string{"novox", "novox.Mesh", "a.b.c", ".x", "novox.mesh-sdk."} {
if _, err := ParseTurnsKey(bad); err == nil {
t.Errorf("%q parses as a turns key", bad)
}
}
if _, err := TurnsKey("novox"); err == nil {
t.Error("a repository with no slash keys")
}
if k := ApprovedKey("novox/a", 7); k != "novox/a@7" {
t.Error(k)
}
if SourceMainKey != seat.SourceByMainKey() {
t.Error("the source's key is the seat's")
}
}
func TestGateValuesRefuseWhatTheirRulesRefuse(t *testing.T) {
ap := time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)
pull := seat.MergesByGroupValuePullsItem{Repository: "novox/a", Number: 7, Head: "h1", State: "merging"}
merge := func(edit func(*seat.MergesByGroupValue)) error {
m := seat.MergesByGroupValue{Group: "g", Ask: "k", Pulls: []seat.MergesByGroupValuePullsItem{pull}}
edit(&m)
_, err := EncodeMerge(m)
return err
}
turn := func(edit func(*seat.TurnsByRepositoryValue)) error {
tv := seat.TurnsByRepositoryValue{Repository: "novox/a", Waiting: []seat.TurnsByRepositoryValueWaitingItem{{Group: "g", Heads: "0123456789ab", Approved: ap, Since: ap}}}
edit(&tv)
_, err := EncodeTurn(tv)
return err
}
for name, c := range map[string]struct {
err error
want string
}{
"merge: no group": {merge(func(m *seat.MergesByGroupValue) { m.Group = "" }), "no group"},
"merge: no pull": {merge(func(m *seat.MergesByGroupValue) { m.Pulls = nil }), "no pull request"},
"merge: not approved": {merge(func(m *seat.MergesByGroupValue) { m.Approved = map[string]string{"novox/b@1": "x"} }), "not among the approved"},
"merge: other head": {merge(func(m *seat.MergesByGroupValue) { m.Approved = map[string]string{"novox/a@7": "x"} }), "Approve was for"},
"merge: merged no commit": {merge(func(m *seat.MergesByGroupValue) { m.Pulls[0].State = "merged" }), "no merge commit"},
"merge: refused no words": {merge(func(m *seat.MergesByGroupValue) { m.Pulls[0].State = "refused" }), "no words"},
"merge: unknown state": {merge(func(m *seat.MergesByGroupValue) { m.Pulls[0].State = "lost" }), "not merging, merged or refused"},
"turn: upper case": {turn(func(v *seat.TurnsByRepositoryValue) { v.Repository = "Novox/a" }), "not lowercase"},
"turn: empty": {turn(func(v *seat.TurnsByRepositoryValue) { v.Waiting = nil }), "names no delivery"},
"turn: bad hash": {turn(func(v *seat.TurnsByRepositoryValue) { v.Waiting[0].Heads = "xyz" }), "12 lowercase hex"},
"turn: no time": {turn(func(v *seat.TurnsByRepositoryValue) { v.Waiting[0].Since = time.Time{} }), "no time"},
"turn: step with no wait": {turn(func(v *seat.TurnsByRepositoryValue) { v.Waiting[0].Step = "s" }), "names no check"},
"turn: not a verdict key": {turn(func(v *seat.TurnsByRepositoryValue) { v.Waiting[0].WaitsFor = "nope" }), "not a verdict key"},
} {
if c.err == nil || !strings.Contains(c.err.Error(), c.want) {
t.Errorf("%s: %v, want %q", name, c.err, c.want)
}
}
if _, err := EncodeSource(seat.SourceByMainValue{Group: "g"}); err == nil {
t.Error("an empty source encodes")
}
if _, err := EncodeAsk(seat.AsksByMergeGroupHeadsValue{Group: "g", Ask: "text", Heads: []seat.AsksByMergeGroupHeadsValueHeadsItem{{Repository: "novox/a", Number: 1, Head: "h"}}}); err == nil {
t.Error("an ask that is no object encodes")
}
if _, err := EncodeAnswer(Answer{Option: "merge"}); err == nil {
t.Error("an answer with no warrant encodes")
}
if _, err := DecodeSource([]byte(`{"v":2,"group":"g","heads":{"a":"b"}}`)); !errors.Is(err, keys.ErrUnknownVersion) {
t.Errorf("another version: %v", err)
}
if _, err := DecodeMerge([]byte(`{"v":1,"group":"g","ask":"k","pulls":[],"extra":1}`)); err == nil {
t.Error("an unknown field decodes")
}
}
func TestTurnHoldsAndSameTurn(t *testing.T) {
ap := time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)
tv := seat.TurnsByRepositoryValue{V: 1, Repository: "novox/a", Waiting: []seat.TurnsByRepositoryValueWaitingItem{{Group: "g", Heads: "0123456789ab", Approved: ap, Since: ap}}}
if TurnHolds(tv, "0123456789ab") || !TurnHolds(tv, "ba9876543210") || TurnHolds(seat.TurnsByRepositoryValue{}, "x") {
t.Error("a turn holds a delivery that is not in it while others wait")
}
other := tv
other.Waiting = []seat.TurnsByRepositoryValueWaitingItem{tv.Waiting[0]}
other.Waiting[0].Approved = ap.In(time.FixedZone("x", 3600))
if !SameTurn(tv, other) {
t.Error("the same instant in another zone is another turn")
}
other.Waiting[0].Since = ap.Add(time.Second)
if SameTurn(tv, other) {
t.Error("another instant is the same turn")
}
}