The delivery repository's contracts and SDK live under modules/ as mesh-delivery-contracts and mesh-delivery-sdk, their Go module paths renamed to match the folders (the TypeScript names already did), and every importer in mesh-delivery, mesh-catalog, mesh-host, mesh-tools and mesh-sdk follows in the same group: a design as it should be, one path per module, checked by the group's check building every importer. Builds: design 47 G §427, §88 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
493 lines
22 KiB
Go
493 lines
22 KiB
Go
package bridge
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
|
|
"mesh-delivery-forge-bridge/internal/gitfixture"
|
|
)
|
|
|
|
const head = "0123456789abcdef0123456789abcdef01234567"
|
|
|
|
// updated is the body of the seat git's pull.updated (modules/mesh-delivery-contracts/seats/git): the pull request, its base's head and its
|
|
// change. extra names the parts a test changes by their short names (owner, repo, number, head for the branch,
|
|
// head_sha for the commit, base, module_dirs, module_dirs_said), a nil value leaving the part out (seatPull).
|
|
func updated(extra map[string]any) []byte {
|
|
return seatPull(map[string]any{"base_sha": "ffffffffffffffffffffffffffffffffffffffff",
|
|
"paths": []string{"modules/a/x.go"}, "module_dirs": []string{"modules/a"}, "module_dirs_said": true}, extra)
|
|
}
|
|
|
|
// closed is the body of the seat git's pull.closed.
|
|
func closed() []byte { return seatPull(nil, nil) }
|
|
|
|
// seatPull is a pull event of the seat git: the pull request novox/mesh-delivery#12 of feat/x at head into main, with
|
|
// the parts of base and extra laid over it by their short names, a nil value leaving the part out. module_dirs is said
|
|
// in the change only when module_dirs_said is true: its absence is "could not be read".
|
|
func seatPull(base, extra map[string]any) []byte {
|
|
m := map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "title": "A change", "head": "feat/x",
|
|
"head_sha": head, "base": "main", "html_url": "https://forge.example/novox/mesh-delivery/pulls/12"}
|
|
for _, over := range []map[string]any{base, extra} {
|
|
for k, v := range over {
|
|
if v == nil {
|
|
delete(m, k)
|
|
} else {
|
|
m[k] = v
|
|
}
|
|
}
|
|
}
|
|
e := map[string]any{}
|
|
owner, _ := m["owner"].(string)
|
|
repo, _ := m["repo"].(string)
|
|
if owner != "" || repo != "" {
|
|
e["repository"] = owner + "/" + repo
|
|
}
|
|
for short, name := range map[string]string{"number": "number", "title": "title", "head": "branch", "head_sha": "head",
|
|
"base": "base", "html_url": "url", "merge_commit_sha": "merge-commit", "base_sha": "base-head", "merged_at": "merged-at"} {
|
|
if v, ok := m[short]; ok {
|
|
e[name] = v
|
|
}
|
|
}
|
|
if paths, ok := m["paths"]; ok {
|
|
c := map[string]any{"paths": paths}
|
|
if m["module_dirs_said"] == true && m["module_dirs"] != nil {
|
|
c["module-dirs"] = m["module_dirs"]
|
|
}
|
|
e["change"] = c
|
|
}
|
|
b, _ := json.Marshal(e)
|
|
return b
|
|
}
|
|
|
|
const updatedSubject = "mesh.seat.git.event.pull-updated"
|
|
const closedSubject = "mesh.seat.git.event.pull-closed"
|
|
|
|
func TestUpdatedBecomesAPullMessage(t *testing.T) { // 1235/AC2 (the conversion; the bus is the integration test)
|
|
msg, err := Convert(updatedSubject, updated(nil), nil, anyFiles)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if msg.Event != "forge.novox.mesh-delivery.pull" {
|
|
t.Errorf("event = %q", msg.Event)
|
|
}
|
|
f, err := forgemsg.DecodeForge(msg.Data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
|
|
Head: head, Action: forgemsg.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true, Manifests: map[string]string{"modules/a": "{}"}}
|
|
if !forgemsg.SameForge(f, want) {
|
|
t.Errorf("message = %+v, want %+v", f, want)
|
|
}
|
|
|
|
// 1235/AC3, merged into this test for the test budget: dirs not said are not known.
|
|
msg, err = Convert(updatedSubject, updated(map[string]any{"module_dirs": nil, "module_dirs_said": nil}), nil, anyFiles)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, _ = forgemsg.DecodeForge(msg.Data)
|
|
if f.DirsKnown || len(f.Dirs) != 0 {
|
|
t.Errorf("dirs = %v known = %v, want not known", f.Dirs, f.DirsKnown)
|
|
}
|
|
// Said and empty is known and empty, not unknown.
|
|
msg, err = Convert(updatedSubject, updated(map[string]any{"module_dirs": []string{}, "module_dirs_said": true}), nil, anyFiles)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, _ = forgemsg.DecodeForge(msg.Data)
|
|
if !f.DirsKnown || len(f.Dirs) != 0 {
|
|
t.Errorf("dirs = %v known = %v, want known and empty", f.Dirs, f.DirsKnown)
|
|
}
|
|
// Dirs listed without the forge saying it could read them are not trusted.
|
|
msg, err = Convert(updatedSubject, updated(map[string]any{"module_dirs_said": nil}), nil, anyFiles)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f, _ = forgemsg.DecodeForge(msg.Data); f.DirsKnown || len(f.Dirs) != 0 {
|
|
t.Errorf("dirs %v known %v without module_dirs_said", f.Dirs, f.DirsKnown)
|
|
}
|
|
}
|
|
|
|
func TestClosedBecomesAClosedPullMessage(t *testing.T) { // 1235/AC4
|
|
msg, err := Convert(closedSubject, closed(), nil, anyFiles)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if msg.Event != "forge.novox.mesh-delivery.pull" {
|
|
t.Errorf("event = %q", msg.Event)
|
|
}
|
|
f, err := forgemsg.DecodeForge(msg.Data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
|
|
Head: head, Action: forgemsg.PullClosed}
|
|
if !forgemsg.SameForge(f, want) {
|
|
t.Errorf("message = %+v, want %+v", f, want)
|
|
}
|
|
// Issue 1422, ADR 0484 §11: a head pushed to a pull request into another base is written closed, read nothing for, and said;
|
|
// one into main is no closed message, and an event naming no base is written as before.
|
|
msg, err = Convert(updatedSubject, updated(map[string]any{"base": "nox-mesh"}), nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f, err = forgemsg.DecodeForge(msg.Data); err != nil || !forgemsg.SameForge(f, want) || !strings.Contains(msg.Said, "is into nox-mesh, not main") {
|
|
t.Errorf("an update into nox-mesh is %+v (%v), said %q; want it closed and said", f, err, msg.Said)
|
|
}
|
|
for _, base := range []any{"main", nil} {
|
|
msg, err = Convert(updatedSubject, updated(map[string]any{"base": base}), nil, anyFiles)
|
|
if f, _ = forgemsg.DecodeForge(msg.Data); err != nil || f.Action != forgemsg.PullUpdated || msg.Said != "" {
|
|
t.Errorf("an update with base %v is %+v (%v), said %q; want it updated", base, f, err, msg.Said)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnreadableEventsWriteNothing(t *testing.T) { // 1235/AC5
|
|
for name, c := range map[string]struct {
|
|
subject string
|
|
data []byte
|
|
why string
|
|
}{
|
|
"no owner": {updatedSubject, updated(map[string]any{"owner": nil}), "owner"},
|
|
"no repo": {updatedSubject, updated(map[string]any{"repo": nil}), "repo"},
|
|
"no number": {updatedSubject, updated(map[string]any{"number": nil}), "number"},
|
|
"number zero": {updatedSubject, updated(map[string]any{"number": 0}), "number"},
|
|
"short head": {updatedSubject, updated(map[string]any{"head_sha": "abc123"}), "head"},
|
|
"head not hex": {updatedSubject, updated(map[string]any{"head_sha": strings.Repeat("g", 40)}), "head"},
|
|
"upper-case head": {updatedSubject, updated(map[string]any{"head_sha": strings.ToUpper(head)}), "head"},
|
|
"no branch": {updatedSubject, updated(map[string]any{"head": nil}), "branch"},
|
|
"not json": {updatedSubject, []byte("{"), "json"},
|
|
"closed no head": {closedSubject, mustDrop(closed(), "head"), "head"},
|
|
"closed no repo": {closedSubject, mustDrop(closed(), "repository"), "repo"},
|
|
} {
|
|
msg, err := Convert(c.subject, c.data, nil, anyFiles)
|
|
if err == nil {
|
|
t.Errorf("%s: a message %q was made", name, msg.Event)
|
|
continue
|
|
}
|
|
if !errors.Is(err, ErrUnreadable) {
|
|
t.Errorf("%s: error %v is not ErrUnreadable", name, err)
|
|
}
|
|
if !strings.Contains(err.Error(), c.why) || !strings.Contains(err.Error(), c.subject) {
|
|
t.Errorf("%s: error %q names neither its subject nor %q", name, err, c.why)
|
|
}
|
|
if msg.Data != nil {
|
|
t.Errorf("%s: data was made", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func mustDrop(body []byte, key string) []byte {
|
|
var m map[string]any
|
|
_ = json.Unmarshal(body, &m)
|
|
delete(m, key)
|
|
b, _ := json.Marshal(m)
|
|
return b
|
|
}
|
|
|
|
func TestOtherSubjectsAreNotHandled(t *testing.T) { // 1235, 1236
|
|
for _, s := range []string{"mesh.seat.git.event.something.else", "mesh.mod.gitea.event.pull.updated", "mesh.mod.other.event.forge.delete"} {
|
|
if _, err := Convert(s, updated(nil), nil, anyFiles); !errors.Is(err, ErrNotHandled) {
|
|
t.Errorf("%s: err = %v, want ErrNotHandled", s, err)
|
|
}
|
|
}
|
|
// The seat's forge.delete of a branch is a branch-deleted message (DLV-FB1, design 51); of a tag or the
|
|
// trunk it is not handled, and a body naming no repository is unreadable.
|
|
del := func(refType, ref string) []byte {
|
|
b, _ := json.Marshal(map[string]any{"repository": "novox/mesh-sdk", "ref": ref, "ref-type": refType, "sender": "jochen"})
|
|
return b
|
|
}
|
|
msg, err := Convert(DeletedSubject, del("branch", "feat/x"), nil, anyFiles)
|
|
var bd BranchDeleted
|
|
if err != nil || msg.Event != "forge.novox.mesh-sdk.branch-deleted" || json.Unmarshal(msg.Data, &bd) != nil ||
|
|
bd.Kind != ForgeBranchDeleted || bd.Repository != "novox/mesh-sdk" || bd.Branch != "feat/x" || bd.Head != "" || msg.Said == "" {
|
|
t.Errorf("forge.delete of a branch = %+v (%s), %v", msg, msg.Data, err)
|
|
}
|
|
for _, c := range []struct{ refType, ref string }{{"tag", "go/v0.1.13"}, {"branch", "main"}} {
|
|
if _, err := Convert(DeletedSubject, del(c.refType, c.ref), nil, anyFiles); !errors.Is(err, ErrNotHandled) {
|
|
t.Errorf("forge.delete of %s %s: err = %v, want ErrNotHandled", c.refType, c.ref, err)
|
|
}
|
|
}
|
|
if _, err := Convert(DeletedSubject, []byte(`{"ref":"x","ref-type":"branch"}`), nil, anyFiles); !errors.Is(err, ErrUnreadable) {
|
|
t.Errorf("forge.delete naming no repository: err = %v, want ErrUnreadable", err)
|
|
}
|
|
// 1236: a merged event without a full merge commit is unreadable, and the forge is not asked.
|
|
var calls []call
|
|
_, err = Convert(mergedSubject, mergedBody(map[string]any{"merge_commit_sha": "abc"}), treesOf(&calls, "r0", map[string]string{"a": "t"}, nil), anyFiles)
|
|
if !errors.Is(err, ErrUnreadable) || len(calls) != 0 {
|
|
t.Errorf("merged without a merge commit: err = %v calls = %v", err, calls)
|
|
}
|
|
}
|
|
|
|
const merge = "89abcdef0123456789abcdef0123456789abcdef"
|
|
|
|
// mergedBody is the seat git's pull.merged, extra by the short names seatPull reads.
|
|
func mergedBody(extra map[string]any) []byte {
|
|
return seatPull(map[string]any{"merge_commit_sha": merge, "merged_at": "2026-10-19T12:00:00Z",
|
|
"paths": []string{"modules/a/x.go"}, "module_dirs": []string{"modules/a"}, "module_dirs_said": true}, extra)
|
|
}
|
|
|
|
const mergedSubject = "mesh.seat.git.event.pull-merged"
|
|
|
|
type call struct{ owner, repo, sha string }
|
|
|
|
// treesOf answers as gitea_commit_trees does (issue 1234/AC4: root, and the directory to tree hash, the root spelled "").
|
|
func treesOf(calls *[]call, root string, trees map[string]string, err error) Trees {
|
|
return func(owner, repo, sha string) (string, map[string]string, error) {
|
|
*calls = append(*calls, call{owner, repo, sha})
|
|
return root, trees, err
|
|
}
|
|
}
|
|
|
|
func TestMergeIntoMainBecomesAMergedMessage(t *testing.T) { // 1236/AC1
|
|
var calls []call
|
|
answer := map[string]string{"modules/a": "t1", "modules/b": "t2"}
|
|
msg, err := Convert(mergedSubject, mergedBody(nil), treesOf(&calls, "r0", answer, nil), anyFiles)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if msg.Event != "forge.novox.mesh-delivery.merged" {
|
|
t.Errorf("event = %q", msg.Event)
|
|
}
|
|
f, err := forgemsg.DecodeForge(msg.Data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
|
|
Head: head, Merge: merge, Root: "r0", Trees: answer, Manifests: map[string]string{"modules/a": "{}", "modules/b": "{}"}}
|
|
if !forgemsg.SameForge(f, want) {
|
|
t.Errorf("message = %+v, want %+v", f, want)
|
|
}
|
|
if len(calls) != 1 || calls[0] != (call{"novox", "mesh-delivery", merge}) {
|
|
t.Errorf("the tool was called %v, want once with novox, mesh-delivery and the merge commit", calls)
|
|
}
|
|
}
|
|
|
|
func TestMergeIntoAnotherBranchIsAClosedPull(t *testing.T) { // 1236/AC2
|
|
var calls []call
|
|
msg, err := Convert(mergedSubject, mergedBody(map[string]any{"base": "feat/y"}), treesOf(&calls, "r0", map[string]string{"a": "t"}, nil), anyFiles)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, err := forgemsg.DecodeForge(msg.Data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
|
|
Head: head, Action: forgemsg.PullClosed}
|
|
if msg.Event != "forge.novox.mesh-delivery.pull" || !forgemsg.SameForge(f, want) {
|
|
t.Errorf("event %q message %+v, want a closed pull message", msg.Event, f)
|
|
}
|
|
if len(calls) != 0 {
|
|
t.Errorf("the tool was called for a merge into another branch: %v", calls)
|
|
}
|
|
}
|
|
|
|
func TestTreesRefusedWritesNothingAndSaysWhy(t *testing.T) { // 1236/AC3 (the conversion; the delay is in main_test)
|
|
var calls []call
|
|
msg, err := Convert(mergedSubject, mergedBody(nil), treesOf(&calls, "", nil, errors.New("the listing for novox/mesh-delivery was cut")), anyFiles)
|
|
if !errors.Is(err, ErrRetry) || msg.Data != nil {
|
|
t.Fatalf("err = %v data = %s, want ErrRetry and no message", err, msg.Data)
|
|
}
|
|
for _, w := range []string{"novox/mesh-delivery", merge, "the listing for novox/mesh-delivery was cut"} {
|
|
if !strings.Contains(err.Error(), w) {
|
|
t.Errorf("error %q does not name %q", err, w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1126 (note 94): a merge that leaves no module.json anywhere (hq, or a repository whose last module was removed) is a merged
|
|
// message with no trees, so the indexer takes it in and the repository's reach is not held for ever.
|
|
func TestNoModuleJSONAnywhereWritesAMergedMessageWithNoTrees(t *testing.T) {
|
|
var calls []call
|
|
msg, err := Convert(mergedSubject, mergedBody(nil), treesOf(&calls, "r0", map[string]string{}, nil), anyFiles)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, err := forgemsg.DecodeForge(msg.Data)
|
|
want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
|
|
Head: head, Merge: merge, Root: "r0"}
|
|
if err != nil || msg.Event != "forge.novox.mesh-delivery.merged" || !forgemsg.SameForge(f, want) || !strings.Contains(msg.Said, "no module.json") {
|
|
t.Errorf("event %q message %+v said %q, %v; want a merged message with no trees", msg.Event, f, msg.Said, err)
|
|
}
|
|
}
|
|
|
|
// openAnswer is the seat git's answer of open-pulls.
|
|
func openAnswer(pulls ...map[string]any) []byte {
|
|
b, _ := json.Marshal(map[string]any{"pulls": pulls})
|
|
return b
|
|
}
|
|
|
|
func openEntry(repo string, number int, base, branch, sha string) map[string]any {
|
|
return map[string]any{"pull": map[string]any{"repository": "novox/" + repo, "number": number, "base": base, "branch": branch, "head": sha,
|
|
"state": "open", "url": "https://forge.example/x"}, "required": []string{"mesh/check"}, "checks": map[string]any{}}
|
|
}
|
|
|
|
func TestOpenPullsAreUpdatedPullMessagesWithUnknownDirs(t *testing.T) { // 1237/AC1
|
|
h2 := strings.Repeat("a", 40)
|
|
msgs, skipped, err := OpenPulls(openAnswer(openEntry("mesh-delivery", 12, "main", "feat/x", head), openEntry("mesh-catalog", 300, "main", "feat/x", h2)))
|
|
if err != nil || len(skipped) != 0 || len(msgs) != 2 {
|
|
t.Fatalf("msgs %d skipped %v err %v", len(msgs), skipped, err)
|
|
}
|
|
for i, want := range []forgemsg.Forge{
|
|
{V: keys.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: forgemsg.PullUpdated},
|
|
{V: keys.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: forgemsg.PullUpdated},
|
|
} {
|
|
f, err := forgemsg.DecodeForge(msgs[i].Data)
|
|
if err != nil || !forgemsg.SameForge(f, want) {
|
|
t.Errorf("message %d = %+v err %v, want %+v", i, f, err, want)
|
|
}
|
|
if f.DirsKnown {
|
|
t.Errorf("message %d says its directories are known", i)
|
|
}
|
|
}
|
|
if msgs[0].Event != "forge.novox.mesh-delivery.pull" || msgs[1].Event != "forge.novox.mesh-catalog.pull" {
|
|
t.Errorf("events %q %q", msgs[0].Event, msgs[1].Event)
|
|
}
|
|
}
|
|
|
|
func TestOpenPullsUnreadableEntryIsNamedAndTheRestGo(t *testing.T) { // 1237, 1237/AC4
|
|
// 1237/AC4: an open pull request into another branch gets no message.
|
|
if msgs, skipped, err := OpenPulls(openAnswer(openEntry("mesh-delivery", 12, "feat/y", "feat/x", head))); err != nil || len(msgs) != 0 || len(skipped) != 0 {
|
|
t.Errorf("into another branch: msgs %d skipped %v err %v, want none", len(msgs), skipped, err)
|
|
}
|
|
msgs, skipped, err := OpenPulls(openAnswer(openEntry("a", 1, "main", "b", "short"), openEntry("mesh-delivery", 12, "main", "feat/x", head)))
|
|
if err != nil || len(msgs) != 1 || len(skipped) != 1 || !strings.Contains(skipped[0], "novox/a#1") {
|
|
t.Errorf("msgs %d skipped %v err %v", len(msgs), skipped, err)
|
|
}
|
|
for _, bad := range []string{`{"pulls":null}`, `{}`, `[]`, `x`, string(gitFixture(t, "open-pulls answers an error, never none", "verb:open-pulls.answer"))} {
|
|
if _, _, err := OpenPulls([]byte(bad)); err == nil {
|
|
t.Errorf("%s was taken as an answer", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpenMessageNamesEveryOpenPullIntoTheTrunk(t *testing.T) { // 1279/AC6
|
|
h2 := strings.Repeat("a", 40)
|
|
answer := openAnswer(openEntry("mesh-delivery", 12, "main", "feat/x", head), openEntry("mesh-catalog", 300, "main", "feat/x", h2),
|
|
openEntry("mesh-catalog", 301, "feat/y", "feat/z", h2), openEntry("a", 1, "main", "b", "short"))
|
|
m, err := OpenMessage(answer)
|
|
if err != nil || m.Event != "forge.open" {
|
|
t.Fatalf("message %+v err %v", m, err)
|
|
}
|
|
f, err := forgemsg.DecodeForge(m.Data)
|
|
// Every one into the trunk, whether its head reads or not (it is open); none into another branch; in a fixed order.
|
|
want := []forgemsg.OpenPull{{Repository: "novox/a", Number: 1}, {Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}}
|
|
if err != nil || f.Kind != forgemsg.ForgeOpen || len(f.Open) != 3 || f.Open[0] != want[0] || f.Open[1] != want[1] || f.Open[2] != want[2] {
|
|
t.Errorf("open message = %+v err %v, want %v", f, err, want)
|
|
}
|
|
// Nothing open is a message too: the indexer then drops every candidate.
|
|
m, err = OpenMessage([]byte(`{"pulls":[]}`))
|
|
if f, derr := forgemsg.DecodeForge(m.Data); err != nil || derr != nil || f.Kind != forgemsg.ForgeOpen || len(f.Open) != 0 {
|
|
t.Errorf("empty answer: %+v %v %v", f, err, derr)
|
|
}
|
|
for _, bad := range []string{`{"pulls":null}`, `{}`, `x`, `{"error":"the forge answered 502","pulls":[]}`} {
|
|
if _, err := OpenMessage([]byte(bad)); err == nil {
|
|
t.Errorf("%s was taken as an answer", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTrackSaysWhichHeadOfWhichPullRequestWasWritten(t *testing.T) { // 1279/AC6
|
|
pullMsg := func(action string) forgemsg.Forge {
|
|
return forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: action}
|
|
}
|
|
for _, a := range []string{forgemsg.PullOpened, forgemsg.PullUpdated} {
|
|
if key, h, ok := Track(pullMsg(a)); key != "novox.mesh-delivery.12" || h != head || !ok {
|
|
t.Errorf("%s: Track = %q %q %v, want the head kept under a bucket key", a, key, h, ok)
|
|
}
|
|
}
|
|
// A closed pull request and a merged one are no longer kept: their key goes.
|
|
if key, h, ok := Track(pullMsg(forgemsg.PullClosed)); key != "novox.mesh-delivery.12" || h != "" || !ok {
|
|
t.Errorf("closed: Track = %q %q %v", key, h, ok)
|
|
}
|
|
merged := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: "m", Root: "r", Trees: map[string]string{"": "t"}}
|
|
if key, h, ok := Track(merged); key != "novox.mesh-delivery.12" || h != "" || !ok {
|
|
t.Errorf("merged: Track = %q %q %v", key, h, ok)
|
|
}
|
|
// The open message and the rest hold no head.
|
|
if _, _, ok := Track(forgemsg.Forge{Kind: forgemsg.ForgeOpen}); ok {
|
|
t.Error("an open message was tracked")
|
|
}
|
|
}
|
|
|
|
// gitFixture is one answer of the seat git's fixtures, by its name and what it is of.
|
|
func gitFixture(t *testing.T, name, of string) json.RawMessage {
|
|
t.Helper()
|
|
return gitfixture.Given(t, name, of)
|
|
}
|
|
|
|
func TestBridgeTagsAnswersAPackagesTagsByPrefixPageByPage(t *testing.T) { // DLV-FB1, design 51 *The floor*; design 47 *The seat git*
|
|
more := gitFixture(t, "tags answers", "verb:tags.answer") // go/v0.1.15, go/v0.1.14, more
|
|
failed := gitFixture(t, "tags answers an error, never none", "verb:tags.answer")
|
|
last := json.RawMessage(`{"repository":"novox/mesh-sdk","tags":["go/v0.1.2","v0.0.1"],"page":2,"bound":50}`)
|
|
type asked struct {
|
|
key string
|
|
in string
|
|
}
|
|
for _, row := range []struct {
|
|
name string
|
|
answer func(page int) (json.RawMessage, error)
|
|
repo string
|
|
tags []string
|
|
more bool
|
|
pages int
|
|
failing string
|
|
}{
|
|
{name: "two pages, the second says no more", repo: "novox/mesh-sdk", tags: []string{"go/v0.1.14", "go/v0.1.15", "go/v0.1.2"}, pages: 2,
|
|
answer: func(p int) (json.RawMessage, error) {
|
|
if p == 1 {
|
|
return more, nil
|
|
}
|
|
return last, nil
|
|
}},
|
|
{name: "every page says more: stopped at the bound", repo: "novox/mesh-sdk", more: true, pages: TagsMaxPages,
|
|
answer: func(int) (json.RawMessage, error) { return json.RawMessage(`{"tags":[],"more":true}`), nil }},
|
|
{name: "the seat does not answer", repo: "novox/mesh-sdk", failing: "page 1", pages: 1,
|
|
answer: func(int) (json.RawMessage, error) { return nil, errors.New("down") }},
|
|
{name: "an error answer is an error, never none", repo: "novox/mesh-sdk", failing: "404", pages: 1,
|
|
answer: func(int) (json.RawMessage, error) { return failed, nil }},
|
|
{name: "a repository with no owner", repo: "mesh-sdk", failing: "<owner>/<name>"},
|
|
} {
|
|
var calls []asked
|
|
call := func(_ context.Context, key string, body any) (json.RawMessage, error) {
|
|
in, _ := json.Marshal(body)
|
|
calls = append(calls, asked{key, string(in)})
|
|
var p struct{ Page int }
|
|
_ = json.Unmarshal(in, &p)
|
|
return row.answer(p.Page)
|
|
}
|
|
a, err := Tags(context.Background(), call, row.repo, "go/v")
|
|
if row.failing != "" {
|
|
if err == nil || !strings.Contains(err.Error(), row.failing) {
|
|
t.Errorf("%s: %v, want an error naming %q", row.name, err, row.failing)
|
|
}
|
|
} else if err != nil || !slicesEqual(a.Tags, row.tags) || a.More != row.more {
|
|
t.Errorf("%s: Tags = %+v, %v", row.name, a, err)
|
|
}
|
|
if len(calls) != row.pages {
|
|
t.Errorf("%s: %d calls, want %d", row.name, len(calls), row.pages)
|
|
}
|
|
if len(calls) > 0 && (calls[0].key != "seat:git.tags" || calls[0].in != `{"page":1,"prefix":"go/v","repository":"novox/mesh-sdk"}`) {
|
|
t.Errorf("%s: first call %+v", row.name, calls[0])
|
|
}
|
|
}
|
|
}
|
|
|
|
func slicesEqual(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|