382 lines
13 KiB
Go
382 lines
13 KiB
Go
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(""); 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)
|
|
}
|
|
}
|