package delivery import ( "encoding/json" "errors" "strconv" "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(""); err == nil { t.Error("an empty group 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", "merge.a=zz.0123456789ab", "merge.a=4.0123456789ab", "merge.a=4A.0123456789ab", "merge.a+b.0123456789ab"} { 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()) slash, _ := MergeAskKey("a/b", gateHeads()) under, _ := MergeAskKey("a_b", gateHeads()) eq, _ := MergeAskKey("feat=x", gateHeads()) eqs, _ := MergeAskKey("feat_x", gateHeads()) seen := map[string]string{} for _, k := range []string{k1, k2, k3, long, long2, slash, under, eq, eqs, "=odd.key"} { 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") } } // today's keys: names of letters, digits, - / and _ key exactly as before the escape. func TestKeysOfPlainNamesAreUnchanged(t *testing.T) { // 1323/AC2 hh, _ := HeadsHash(gateHeads()) for _, name := range []string{"feat/x", "fix/1323-escape", "feat/delivery_source-keys", "main", "A/B_c-9"} { if k, err := MergesKey(name); err != nil || k != name { t.Errorf("merges key %q = %q, %v", name, k, err) } if k, err := CandidatesKey(name); err != nil || k != name { t.Errorf("candidates key %q = %q, %v", name, k, err) } if k, err := MergeAskKey(name, gateHeads()); err != nil || k != "merge."+name+"."+hh { t.Errorf("ask key %q = %q, %v", name, k, err) } } if k, err := OwedKey("novox/mesh-sdk", 36, "aaaa", "status=mesh/merge-gate"); err != nil || k != "novox/mesh-sdk.36.aaaa.status=mesh/merge-gate" { t.Errorf("owed key = %q, %v", k, err) } } func TestKeysEscapeAndRoundTrip(t *testing.T) { // 1323/AC1 hh, _ := HeadsHash(gateHeads()) for _, name := range []string{"fix/1.2", "feat/a+b", "feat/a=b", "a.b+c=d", "fix/ΓΌ", "x y"} { esc := EscapeName(name) if strings.ContainsAny(esc, ".+ ") { t.Errorf("escape of %q = %q holds a dot, plus or space", name, esc) } if back, err := UnescapeName(esc); err != nil || back != name { t.Errorf("unescape(%q) = %q, %v; want %q", esc, back, err, name) } if k, err := MergesKey(name); err != nil || k != esc { t.Errorf("merges key %q = %q, %v", name, k, err) } ck, err := CandidatesKey(name) if err != nil { t.Fatal(err) } if g, err := ParseCandidatesKey(ck); err != nil || g != name { t.Errorf("candidates round trip %q = %q, %v", name, g, err) } ak, err := MergeAskKey(name, gateHeads()) if err != nil { t.Fatal(err) } if _, g, h, err := ParseAskKey(ak); err != nil || g != name || h != hh { t.Errorf("ask round trip %q = %q %q %v", name, g, h, err) } if AnswerKey(ak) != ak { t.Error("answer key differs from the ask key") } // the ask and the answer of such a group encode and decode ask := Ask{Group: name, Heads: gateHeads(), Ask: asks.Ask{ID: AskID(ak), Options: []asks.Option{{ID: "ok"}}}} if data, err := EncodeAsk(ask); err != nil { t.Errorf("encode ask for %q: %v", name, err) } else if _, err := DecodeAsk(data); err != nil { t.Errorf("decode ask for %q: %v", name, err) } repo := "novox/" + name[strings.Index(name, "/")+1:] if !strings.Contains(name, "/") { repo = "novox/" + name } ok, err := OwedKey(repo, 3, "aaaa", "view") if err != nil { t.Errorf("owed key for repository %q: %v", repo, err) continue } if r, n, h, a, err := ParseOwedKey(ok); err != nil || r != repo || n != 3 || h != "aaaa" || a != "view" { t.Errorf("owed round trip %q = %q %d %q %q %v", repo, r, n, h, a, err) } } if EscapeName("a.b") != "a=2eb" || EscapeName("a=b") != "a=3db" || EscapeName("a+b") != "a=2bb" { t.Errorf("escapes: %q %q %q", EscapeName("a.b"), EscapeName("a=b"), EscapeName("a+b")) } m := Merge{Group: "fix/1.2+x=y", Ask: "merge.x.0123456789ab", Pulls: []MergePull{{Head: gateHeads()[0], State: MergeMerging}}} if data, err := EncodeMerge(m); err != nil { t.Errorf("encode merge: %v", err) } else if back, err := DecodeMerge(data); err != nil || back.Group != m.Group { t.Errorf("decode merge: %v %v", back.Group, err) } } func approvedMerge() Merge { m := sampleMerge() m.Approved = map[string]string{ ApprovedKey("novox/mesh-sdk", 36): "aaaa", ApprovedKey("novox/mesh-control", 7): "bbbb", ApprovedKey("novox/other", 1): "dddd", } return m } func TestMergeCarriesApprovedHeads(t *testing.T) { // 1321/AC1 m := approvedMerge() m.Pulls = m.Pulls[:1] // two members approved besides the one handed so far m.Pulls[0].State = MergeMerging m.Pulls[0].Commit = "" data, err := EncodeMerge(m) if err != nil { t.Fatal(err) } back, err := DecodeMerge(data) if err != nil { t.Fatal(err) } m.V = Version if !SameMerge(back, m) || len(back.Approved) != 3 || back.Approved["novox/mesh-control@7"] != "bbbb" { t.Errorf("decoded %+v, want %+v", back, m) } } func TestMergeRefusesAPullNotApproved(t *testing.T) { // 1321/AC2 m := approvedMerge() delete(m.Approved, ApprovedKey("novox/other", 1)) if _, err := EncodeMerge(m); err == nil || !strings.Contains(err.Error(), "novox/other#1") { t.Errorf("a handed pull missing from the approved heads: %v", err) } m = approvedMerge() m.Approved[ApprovedKey("novox/other", 1)] = "eeee" if _, err := EncodeMerge(m); err == nil { t.Error("a handed pull at another head than the Approve's accepted") } m = approvedMerge() m.Approved[ApprovedKey("novox/other", 1)] = "" if _, err := EncodeMerge(m); err == nil { t.Error("an empty approved head accepted") } data, _ := json.Marshal(approvedMerge()) var raw map[string]any _ = json.Unmarshal(data, &raw) raw["v"] = Version raw["approved"] = map[string]string{"novox/mesh-sdk@36": "aaaa"} bad, _ := json.Marshal(raw) if _, err := DecodeMerge(bad); err == nil { t.Error("decoding a merge whose pulls are not all approved accepted") } } func TestMergeWithoutApprovedHeadsDecodes(t *testing.T) { // 1321/AC3 old := `{"v":` + strconv.Itoa(Version) + `,"group":"feat/x","ask":"merge.feat/x.0123456789ab","pulls":[{"repository":"novox/mesh-sdk","number":36,"head":"aaaa","state":"merging"}]}` m, err := DecodeMerge([]byte(old)) if err != nil || len(m.Approved) != 0 || len(m.Pulls) != 1 { t.Fatalf("old merge = %+v, %v", m, err) } data, err := EncodeMerge(sampleMerge()) if err != nil || strings.Contains(string(data), "approved") { t.Errorf("a merge without approved heads wrote them: %s %v", data, err) } }