package bridge import ( "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" ) const head = "0123456789abcdef0123456789abcdef01234567" // updated is the body of gitea.pull.updated as mesh-catalog modules/gitea/trunk.ts:233-250 (announcementOf) writes // it: owner, repo, number, base_sha, title, body, base, head (the branch), head_sha, clone_url, html_url, paths, // paths_truncated, removed, module_dirs, module_dirs_said, merge_check, merge_check_said. func updated(extra map[string]any) []byte { m := map[string]any{ "owner": "novox", "repo": "mesh-delivery", "number": 12, "base_sha": "ffffffffffffffffffffffffffffffffffffffff", "title": "A change", "body": "text", "base": "main", "head": "feat/x", "head_sha": head, "clone_url": "https://forge.example/novox/mesh-delivery.git", "html_url": "https://forge.example/novox/mesh-delivery/pulls/12", "paths": []string{"modules/a/x.go"}, "paths_truncated": false, "removed": []string{}, "module_dirs": []string{"modules/a"}, "module_dirs_said": true, "merge_check": true, "merge_check_said": true, } for k, v := range extra { if v == nil { delete(m, k) } else { m[k] = v } } b, _ := json.Marshal(m) return b } // closed is the body of gitea.pull.closed as mesh-catalog modules/gitea/index.ts:127-128 writes it: owner, repo, // number, title, head, head_sha, base, html_url. func closed() []byte { b, _ := json.Marshal(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"}) return b } const updatedSubject = "mesh.mod.gitea.event.pull.updated" const closedSubject = "mesh.mod.gitea.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_sha"), "head"}, "closed no repo": {closedSubject, mustDrop(closed(), "repo"), "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.mod.gitea.event.something.else", "mesh.mod.other.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 forge module'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{"owner": "novox", "repo": "mesh-sdk", "full_name": "novox/mesh-sdk", "ref": ref, "ref_type": refType, "sha": head}) 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 != 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 gitea.pull.merged as mesh-catalog modules/gitea/index.ts:186-207 writes it. func mergedBody(extra map[string]any) []byte { m := map[string]any{ "owner": "novox", "repo": "mesh-delivery", "number": 12, "title": "A change", "body": "text", "head_sha": head, "head_checks": map[string]string{"mesh/merge-gate": "success"}, "head": "feat/x", "base": "main", "merge_commit_sha": merge, "merged_at": "2026-10-19T12:00:00Z", "clone_url": "https://forge.example/novox/mesh-delivery.git", "html_url": "https://forge.example/novox/mesh-delivery/pulls/12", "paths": []string{"modules/a/x.go"}, "paths_truncated": false, "removed": []string{}, "module_dirs": []string{"modules/a"}, "module_dirs_said": true, } for k, v := range extra { if v == nil { delete(m, k) } else { m[k] = v } } b, _ := json.Marshal(m) return b } const mergedSubject = "mesh.mod.gitea.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 gitea_open_pulls's answer as mesh-catalog modules/gitea/client.ts:369-381 builds each entry // (owner, repo, number, base, head, head_sha, html_url, required, statuses, checks, labels) inside {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{"owner": "novox", "repo": repo, "number": number, "base": base, "head": branch, "head_sha": sha, "html_url": "https://forge.example/x", "required": []string{"mesh/merge-gate"}, "statuses": []string{}, "checks": map[string]string{}, "labels": []string{}} } 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`} { 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`} { 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") } } func TestBridgeTagsAnswersAPackagesTagsByPrefixPageByPage(t *testing.T) { // DLV-FB1, design 51 *The floor* var asked []string page := func(n int) json.RawMessage { var tags []map[string]string for i := 0; i < n; i++ { tags = append(tags, map[string]string{"name": "x" + strings.Repeat("y", i)}) } b, _ := json.Marshal(tags) return b } api := func(path string) (json.RawMessage, error) { asked = append(asked, path) if strings.Contains(path, "page=1&") { full := page(TagsPageSize - 2) var tags []map[string]string _ = json.Unmarshal(full, &tags) tags = append(tags, map[string]string{"name": "go/v0.1.13"}, map[string]string{"name": "v0.0.1"}) b, _ := json.Marshal(tags) return b, nil } return json.RawMessage(`[{"name":"go/v0.1.2"}]`), nil } a, err := Tags(api, "novox/mesh-sdk", "go/v") if err != nil || !slicesEqual(a.Tags, []string{"go/v0.1.13", "go/v0.1.2"}) || a.More || len(asked) != 2 || asked[0] != "/repos/novox/mesh-sdk/tags?page=1&limit=50" { t.Errorf("Tags = %+v, %v; asked %v", a, err, asked) } if _, err := Tags(func(string) (json.RawMessage, error) { return nil, errors.New("down") }, "novox/mesh-sdk", ""); err == nil || !strings.Contains(err.Error(), "page 1") { t.Errorf("a forge not answering = %v, want an error naming the page", err) } if _, err := Tags(api, "mesh-sdk", ""); err == nil { t.Error("a repository with no owner answered") } } 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 }