package delivery import ( "encoding/json" "errors" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/asks" ) func gateHeads() []Head { return []Head{ {Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa"}, {Repository: "novox/mesh-control", Number: 7, Head: "bbbb"}, } } func TestMergeKeys(t *testing.T) { // 1201/AC1 if k, err := MergesKey("feat/x"); err != nil || k != "feat/x" { t.Fatalf("merges key = %q, %v", k, err) } if _, err := MergesKey("a.b"); err == nil { t.Error("a group with a dot accepted") } hh, _ := HeadsHash(gateHeads()) key, err := MergeAskKey("feat/x", gateHeads()) if err != nil || key != "merge.feat/x."+hh { t.Fatalf("ask key = %q, %v", key, err) } kind, group, hash, err := ParseAskKey(key) if err != nil || kind != AskKindMerge || group != "feat/x" || hash != hh { t.Fatalf("parse ask key = %q %q %q %v", kind, group, hash, err) } if AnswerKey(key) != key { t.Error("the answer's key is not the ask's key") } if _, err := MergeAskKey("feat/x", nil); err == nil { t.Error("an ask key without heads accepted") } if _, err := MergeAskKey("", gateHeads()); err == nil { t.Error("an ask key without a group accepted") } for _, bad := range []string{"", "merge", "merge.g", "merge.g.h.x", "other.g.0123456789ab", "merge..0123456789ab", "merge.g.nothex"} { if _, _, _, err := ParseAskKey(bad); err == nil { t.Errorf("parse ask key %q accepted", bad) } } } func TestAskID(t *testing.T) { // 1201/AC2 k1, _ := MergeAskKey("feat/x", gateHeads()) k2, _ := MergeAskKey("feat/y", gateHeads()) k3, _ := MergeAskKey("feat/x", gateHeads()[:1]) long, _ := MergeAskKey("feat/"+strings.Repeat("long-", 20), gateHeads()) long2, _ := MergeAskKey("feat/"+strings.Repeat("long-", 20)+"2", gateHeads()) seen := map[string]string{} for _, k := range []string{k1, k2, k3, long, long2} { id := AskID(k) if strings.Contains(id, ".") { t.Errorf("id %q holds a dot", id) } if !asks.UsableID(id) { t.Errorf("id %q is not usable by the router", id) } if other, dup := seen[id]; dup { t.Errorf("keys %q and %q give the id %q", k, other, id) } seen[id] = k } if AskID(k1) != AskID(k1) { t.Error("the id is not stable") } } func TestOwedKey(t *testing.T) { // 1201/AC3 for _, act := range []string{"status=mesh/merge-gate", "view", "note", "comment"} { key, err := OwedKey("novox/mesh-sdk", 36, "aaaa", act) if err != nil || key != "novox/mesh-sdk.36.aaaa."+act { t.Fatalf("owed key for %s = %q, %v", act, key, err) } repo, number, head, gotAct, err := ParseOwedKey(key) if err != nil || repo != "novox/mesh-sdk" || number != 36 || head != "aaaa" || gotAct != act { t.Fatalf("parse %q = %q %d %q %q %v", key, repo, number, head, gotAct, err) } } kind, context, err := ParseOwedAct("status=mesh/merge-gate") if err != nil || kind != OwedStatus || context != "mesh/merge-gate" { t.Errorf("owed act = %q %q %v", kind, context, err) } for _, bad := range []string{"", "merge", "status", "status=", "view=x", "status=a.b", "statuses=x"} { if _, err := OwedKey("novox/mesh-sdk", 36, "aaaa", bad); err == nil { t.Errorf("owed act %q accepted", bad) } } if _, err := OwedKey("nope", 36, "aaaa", "view"); err == nil { t.Error("a bad repository accepted") } if _, err := OwedKey("novox/mesh-sdk", 0, "aaaa", "view"); err == nil { t.Error("pull request number 0 accepted") } if _, err := OwedKey("novox/mesh-sdk", 36, "", "view"); err == nil { t.Error("an empty head accepted") } for _, bad := range []string{"", "novox/mesh-sdk.36.aaaa", "novox/mesh-sdk.x.aaaa.view", "novox/mesh-sdk.36.aaaa.merge", "a.b.36.aaaa.view"} { if _, _, _, _, err := ParseOwedKey(bad); err == nil { t.Errorf("parse owed key %q accepted", bad) } } } func sampleMerge() Merge { return Merge{Group: "feat/x", Ask: "merge.feat/x.0123456789ab", Pulls: []MergePull{ {Head: gateHeads()[0], State: MergeMerged, Commit: "cccc"}, {Head: gateHeads()[1], State: MergeRefused, Words: "the forge says no"}, {Head: Head{Repository: "novox/other", Number: 1, Head: "dddd"}, State: MergeMerging}, }} } func sampleAsk() Ask { key, _ := MergeAskKey("feat/x", gateHeads()) return Ask{Group: "feat/x", Heads: gateHeads(), Ask: asks.Ask{ ID: AskID(key), Headline: "Merge into main: group feat/x", Explanation: "The group passed its checks.", Who: asks.Operator, Expires: time.Date(2026, 10, 20, 8, 0, 0, 0, time.UTC), OnExpiry: "nothing is merged", Options: []asks.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: asks.Approve, Binds: "sha256:00"}}, }} } func sampleOwed() []Owed { return []Owed{ {Act: OwedStatus, Context: "mesh/merge-gate", State: "success"}, {Act: OwedView, Text: "the delivery reaches ace"}, {Act: OwedNote, Text: "plan: mesh-mail to ace", Ref: "mesh-plan"}, {Act: OwedComment, Text: "merged"}, } } func TestGateValuesRoundTrip(t *testing.T) { // 1201/AC4 hasV := func(name string, data []byte, err error) { t.Helper() if err != nil { t.Fatalf("%s: %v", name, err) } var probe map[string]any if json.Unmarshal(data, &probe) != nil || probe["v"] != float64(1) { t.Fatalf("%s: %s does not carry v:1", name, data) } } m := sampleMerge() data, err := EncodeMerge(m) hasV("merge", data, err) m.V = Version if got, err := DecodeMerge(data); err != nil || !SameMerge(got, m) { t.Errorf("merge round trip: %+v %v", got, err) } a := sampleAsk() data, err = EncodeAsk(a) hasV("ask", data, err) a.V = Version if got, err := DecodeAsk(data); err != nil || !SameAsk(got, a) { t.Errorf("ask round trip: %+v %v", got, err) } an := Answer{Option: "approve", Warrant: "w-1", Words: "approved on telegram"} data, err = EncodeAnswer(an) hasV("answer", data, err) an.V = Version if got, err := DecodeAnswer(data); err != nil || !SameAnswer(got, an) { t.Errorf("answer round trip: %+v %v", got, err) } for _, o := range sampleOwed() { data, err = EncodeOwed(o) hasV("owed "+o.Act, data, err) o.V = Version if got, err := DecodeOwed(data); err != nil || !SameOwed(got, o) { t.Errorf("owed %s round trip: %+v %v", o.Act, got, err) } } for name, dec := range map[string]func([]byte) error{ "merge": func(b []byte) error { _, err := DecodeMerge(b); return err }, "ask": func(b []byte) error { _, err := DecodeAsk(b); return err }, "answer": func(b []byte) error { _, err := DecodeAnswer(b); return err }, "owed": func(b []byte) error { _, err := DecodeOwed(b); return err }, } { for _, in := range []string{`{"group":"g"}`, `{"v":2}`} { if err := dec([]byte(in)); !errors.Is(err, ErrUnknownVersion) { t.Errorf("%s decode of %s = %v, want ErrUnknownVersion", name, in, err) } } } } func TestGateValuesRefused(t *testing.T) { // 1201/AC5 wantErr := func(name string, err error, words ...string) { t.Helper() if err == nil { t.Errorf("%s accepted", name) return } for _, w := range words { if !strings.Contains(err.Error(), w) { t.Errorf("%s: error %q does not name %q", name, err, w) } } } m := sampleMerge() m.Pulls[0].Commit = "" _, err := EncodeMerge(m) wantErr("merged without a merge commit", err, "commit") m = sampleMerge() m.Pulls[1].Words = "" _, err = EncodeMerge(m) wantErr("refused without words", err, "words") m = sampleMerge() m.Pulls[2].State = "done" _, err = EncodeMerge(m) wantErr("unknown state", err, "done") m = sampleMerge() m.Pulls = nil _, err = EncodeMerge(m) wantErr("merge without pulls", err, "pull") m = sampleMerge() m.Ask = "" _, err = EncodeMerge(m) wantErr("merge without its ask", err, "ask") a := sampleAsk() a.Ask.ID = "something-else" _, err = EncodeAsk(a) wantErr("ask with another id", err, "something-else", AskID("merge.feat/x."+mustHash(gateHeads()))) a = sampleAsk() a.Ask.Options = nil _, err = EncodeAsk(a) wantErr("ask without options", err, "option") a = sampleAsk() a.Heads = nil _, err = EncodeAsk(a) wantErr("ask without heads", err, "pull request") for name, an := range map[string]Answer{ "option": {Warrant: "w", Words: "x"}, "warrant": {Option: "approve", Words: "x"}, "words": {Option: "approve", Warrant: "w"}, } { _, err = EncodeAnswer(an) wantErr("answer without "+name, err, name) } for name, o := range map[string]Owed{ "context": {Act: OwedStatus, State: "success"}, "state": {Act: OwedStatus, Context: "mesh/merge-gate", State: "green"}, "text": {Act: OwedView}, "ref": {Act: OwedNote, Text: "line"}, "comment": {Act: OwedComment}, "act": {Act: "merge"}, } { _, err = EncodeOwed(o) wantErr("owed missing "+name, err) } _, err = EncodeOwed(Owed{Act: OwedNote, Text: "line"}) wantErr("note without ref", err, "ref") _, err = EncodeOwed(Owed{Act: OwedStatus, State: "success"}) wantErr("status without context", err, "context") } func mustHash(h []Head) string { s, _ := HeadsHash(h); return s } func TestGateSame(t *testing.T) { // 1201/AC6 if !SameMerge(sampleMerge(), sampleMerge()) || !SameAsk(sampleAsk(), sampleAsk()) { t.Error("equal values differ") } m := sampleMerge() m.Pulls[0].Commit = "other" if SameMerge(m, sampleMerge()) { t.Error("different merges are the same") } a := sampleAsk() a.Ask.Headline = "other" if SameAsk(a, sampleAsk()) { t.Error("different asks are the same") } if !SameAnswer(Answer{Option: "a", Warrant: "w", Words: "x"}, Answer{Option: "a", Warrant: "w", Words: "x"}) || SameAnswer(Answer{Option: "a", Warrant: "w", Words: "x"}, Answer{Option: "b", Warrant: "w", Words: "x"}) { t.Error("SameAnswer wrong") } if !SameOwed(sampleOwed()[0], sampleOwed()[0]) || SameOwed(sampleOwed()[0], sampleOwed()[1]) { t.Error("SameOwed wrong") } } func TestGateBuckets(t *testing.T) { if MergesBucket != "mesh-delivery-gate_merges" || AsksBucket != "mesh-delivery-gate_asks" || OwedBucket != "mesh-delivery-gate_owed" || AnswersBucket != "mesh-delivery-teller_answers" || MergesHistory != 10 || AsksHistory != 5 || OwedHistory != 5 || AnswersHistory != 5 { t.Error("a bucket or history differs from the decided layout") } }