From 3c1e7ad4642a2b81b65585d9eb72750648340564 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 18:53:07 +0000 Subject: [PATCH] Test the bridge's manifest and lock file conversion (issue 1448, design 47 R4c-2) --- .../internal/bridge/bridge_test.go | 28 ++-- .../internal/bridge/files_test.go | 142 ++++++++++++++++++ 2 files changed, 156 insertions(+), 14 deletions(-) create mode 100644 modules/mesh-delivery-forge-bridge/internal/bridge/files_test.go diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go index 60a7ca5..c2424a5 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go @@ -47,7 +47,7 @@ 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) + msg, err := Convert(updatedSubject, updated(nil), nil, anyFiles) if err != nil { t.Fatal(err) } @@ -59,14 +59,14 @@ func TestUpdatedBecomesAPullMessage(t *testing.T) { // 1235/AC2 (the conversion; t.Fatal(err) } want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, - Head: head, Action: delivery.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true} + Head: head, Action: delivery.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true, Manifests: map[string]string{"modules/a": "{}"}} if !delivery.SameForge(f, want) { t.Errorf("message = %+v, want %+v", f, want) } } func TestDirsNotSaidAreNotKnown(t *testing.T) { // 1235/AC3 - msg, err := Convert(updatedSubject, updated(map[string]any{"module_dirs": nil, "module_dirs_said": nil}), nil) + msg, err := Convert(updatedSubject, updated(map[string]any{"module_dirs": nil, "module_dirs_said": nil}), nil, anyFiles) if err != nil { t.Fatal(err) } @@ -75,7 +75,7 @@ func TestDirsNotSaidAreNotKnown(t *testing.T) { // 1235/AC3 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) + msg, err = Convert(updatedSubject, updated(map[string]any{"module_dirs": []string{}, "module_dirs_said": true}), nil, anyFiles) if err != nil { t.Fatal(err) } @@ -84,7 +84,7 @@ func TestDirsNotSaidAreNotKnown(t *testing.T) { // 1235/AC3 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) + msg, err = Convert(updatedSubject, updated(map[string]any{"module_dirs_said": nil}), nil, anyFiles) if err != nil { t.Fatal(err) } @@ -94,7 +94,7 @@ func TestDirsNotSaidAreNotKnown(t *testing.T) { // 1235/AC3 } func TestClosedBecomesAClosedPullMessage(t *testing.T) { // 1235/AC4 - msg, err := Convert(closedSubject, closed(), nil) + msg, err := Convert(closedSubject, closed(), nil, anyFiles) if err != nil { t.Fatal(err) } @@ -130,7 +130,7 @@ func TestUnreadableEventsWriteNothing(t *testing.T) { // 1235/AC5 "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) + msg, err := Convert(c.subject, c.data, nil, anyFiles) if err == nil { t.Errorf("%s: a message %q was made", name, msg.Event) continue @@ -157,7 +157,7 @@ func mustDrop(body []byte, key string) []byte { func TestOtherSubjectsAreNotHandled(t *testing.T) { // 1235 for _, s := range []string{"mesh.mod.gitea.event.something.else", "mesh.mod.other.event.pull.updated"} { - if _, err := Convert(s, updated(nil), nil); !errors.Is(err, ErrNotHandled) { + if _, err := Convert(s, updated(nil), nil, anyFiles); !errors.Is(err, ErrNotHandled) { t.Errorf("%s: err = %v, want ErrNotHandled", s, err) } } @@ -200,7 +200,7 @@ func treesOf(calls *[]call, root string, trees map[string]string, err error) Tre 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)) + msg, err := Convert(mergedSubject, mergedBody(nil), treesOf(&calls, "r0", answer, nil), anyFiles) if err != nil { t.Fatal(err) } @@ -212,7 +212,7 @@ func TestMergeIntoMainBecomesAMergedMessage(t *testing.T) { // 1236/AC1 t.Fatal(err) } want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, - Head: head, Merge: merge, Root: "r0", Trees: answer} + Head: head, Merge: merge, Root: "r0", Trees: answer, Manifests: map[string]string{"modules/a": "{}", "modules/b": "{}"}} if !delivery.SameForge(f, want) { t.Errorf("message = %+v, want %+v", f, want) } @@ -223,7 +223,7 @@ func TestMergeIntoMainBecomesAMergedMessage(t *testing.T) { // 1236/AC1 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)) + 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) } @@ -243,7 +243,7 @@ func TestMergeIntoAnotherBranchIsAClosedPull(t *testing.T) { // 1236/AC2 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"))) + 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) } @@ -256,7 +256,7 @@ func TestTreesRefusedWritesNothingAndSaysWhy(t *testing.T) { // 1236/AC3 (the co func TestNoModuleJSONAnywhereWritesNoMergedMessage(t *testing.T) { // 1236 (notes: a repository with no module.json) var calls []call - msg, err := Convert(mergedSubject, mergedBody(nil), treesOf(&calls, "r0", map[string]string{}, nil)) + msg, err := Convert(mergedSubject, mergedBody(nil), treesOf(&calls, "r0", map[string]string{}, nil), anyFiles) if !errors.Is(err, ErrNoModules) || msg.Data != nil { t.Errorf("err = %v data = %s", err, msg.Data) } @@ -264,7 +264,7 @@ func TestNoModuleJSONAnywhereWritesNoMergedMessage(t *testing.T) { // 1236 (note func TestMergedWithoutAMergeCommitIsUnreadable(t *testing.T) { // 1236 var calls []call - _, err := Convert(mergedSubject, mergedBody(map[string]any{"merge_commit_sha": "abc"}), treesOf(&calls, "r0", map[string]string{"a": "t"}, nil)) + _, 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("err = %v calls = %v", err, calls) } diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/files_test.go b/modules/mesh-delivery-forge-bridge/internal/bridge/files_test.go new file mode 100644 index 0000000..67e33b1 --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/files_test.go @@ -0,0 +1,142 @@ +package bridge + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "os" + "reflect" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// The bytes the forge answers in these tests are two real files of this repository, read from the working tree: the +// module.json and the go.sum of modules/mesh-delivery-forge-bridge (two directories up). The forge's own answer shape +// is mesh-catalog modules/gitea tools/index.ts gitea_get_file; the bridge sees only the bytes (issue 1448). +func real(t *testing.T, name string) []byte { + t.Helper() + b, err := os.ReadFile("../../" + name) + if err != nil { + t.Fatal(err) + } + return b +} + +func hexSum(b []byte) string { s := sha256.Sum256(b); return hex.EncodeToString(s[:]) } + +type fileCall struct{ owner, repo, path, ref string } + +// forgeFiles answers as the forge does for a path: its bytes, ErrNotFound for a path in none, the error in errs. +type forgeFiles struct { + have map[string][]byte + errs map[string]error + calls []fileCall +} + +func (f *forgeFiles) get(owner, repo, path, ref string) ([]byte, error) { + f.calls = append(f.calls, fileCall{owner, repo, path, ref}) + if err, ok := f.errs[path]; ok { + return nil, err + } + if b, ok := f.have[path]; ok { + return b, nil + } + return nil, ErrNotFound +} + +// anyFiles is a forge that has a module.json at every path and no lock file, for tests that are not about the files. +func anyFiles(owner, repo, path, ref string) ([]byte, error) { + if strings.HasSuffix(path, "module.json") { + return []byte("{}"), nil + } + return nil, ErrNotFound +} + +func TestMergedCarriesManifestsAndLocks(t *testing.T) { // 1448/AC1 + manifest, sum := real(t, "module.json"), real(t, "go.sum") + forge := &forgeFiles{have: map[string][]byte{"modules/a/module.json": manifest, "modules/a/go.sum": sum, "modules/b/module.json": []byte(`{"module":"b"}`)}} + var calls []call + msg, err := Convert(mergedSubject, mergedBody(nil), treesOf(&calls, "r0", map[string]string{"modules/a": "t1", "modules/b": "t2"}, nil), forge.get) + if err != nil { + t.Fatal(err) + } + f, err := delivery.DecodeForge(msg.Data) + if err != nil { + t.Fatal(err) + } + if want := map[string]string{"modules/a": string(manifest), "modules/b": `{"module":"b"}`}; !reflect.DeepEqual(f.Manifests, want) { + t.Errorf("manifests = %v", f.Manifests) + } + if want := map[string]map[string]string{"modules/a": {"go.sum": hexSum(sum)}}; !reflect.DeepEqual(f.Locks, want) { + t.Errorf("locks = %v, want %v", f.Locks, want) + } +} + +func TestPullWithKnownDirsCarriesTheSameAtTheHead(t *testing.T) { // 1448/AC2 + manifest := real(t, "module.json") + forge := &forgeFiles{have: map[string][]byte{"modules/a/module.json": manifest}} + msg, err := Convert(updatedSubject, updated(nil), nil, forge.get) + if err != nil { + t.Fatal(err) + } + f, _ := delivery.DecodeForge(msg.Data) + if !reflect.DeepEqual(f.Manifests, map[string]string{"modules/a": string(manifest)}) || len(f.Locks) != 0 { + t.Errorf("manifests %v locks %v", f.Manifests, f.Locks) + } + if len(forge.calls) == 0 || forge.calls[0].ref != head { + t.Errorf("calls %v, want the head %s", forge.calls, head) + } + // Dirs not known: neither field, and the forge is not asked. + forge = &forgeFiles{} + msg, err = Convert(updatedSubject, updated(map[string]any{"module_dirs": nil, "module_dirs_said": nil}), nil, forge.get) + if err != nil { + t.Fatal(err) + } + f, _ = delivery.DecodeForge(msg.Data) + if f.Manifests != nil || f.Locks != nil || len(forge.calls) != 0 { + t.Errorf("manifests %v locks %v calls %v", f.Manifests, f.Locks, forge.calls) + } +} + +func TestAForgeErrorOrAMissingManifestIsRetryAndALockNotFoundIsLeftOut(t *testing.T) { // 1448/AC3 + boom := errors.New("Gitea API /repos/novox/mesh-delivery/contents/modules/a/go.sum?ref=x: 502 bad gateway") + trees := func() Trees { var c []call; return treesOf(&c, "r0", map[string]string{"modules/a": "t1"}, nil) } + for name, forge := range map[string]*forgeFiles{ + "a manifest error": {errs: map[string]error{"modules/a/module.json": boom}}, + "a manifest not found": {}, + "a lock file error": {have: map[string][]byte{"modules/a/module.json": []byte("{}")}, errs: map[string]error{"modules/a/go.sum": boom}}, + "a second lock error": {have: map[string][]byte{"modules/a/module.json": []byte("{}")}, errs: map[string]error{"modules/a/package-lock.json": boom}}, + } { + msg, err := Convert(mergedSubject, mergedBody(nil), trees(), forge.get) + if !errors.Is(err, ErrRetry) || msg.Data != nil { + t.Errorf("%s: err = %v data = %s, want ErrRetry and no message", name, err, msg.Data) + } + _, err = Convert(updatedSubject, updated(nil), nil, forge.get) + if !errors.Is(err, ErrRetry) { + t.Errorf("%s (pull): err = %v, want ErrRetry", name, err) + } + } + forge := &forgeFiles{have: map[string][]byte{"modules/a/module.json": []byte("{}")}} + msg, err := Convert(mergedSubject, mergedBody(nil), trees(), forge.get) + if err != nil { + t.Fatal(err) + } + if f, _ := delivery.DecodeForge(msg.Data); f.Locks != nil { + t.Errorf("locks = %v, want none for lock files the forge does not have", f.Locks) + } +} + +func TestTheFilesAreAskedAtTheMergeCommitByOwnerRepoPathAndRef(t *testing.T) { // 1448/AC4 (the call's arguments; the manifest is in manifest_test) + forge := &forgeFiles{have: map[string][]byte{"modules/a/module.json": []byte("{}")}} + var calls []call + if _, err := Convert(mergedSubject, mergedBody(nil), treesOf(&calls, "r0", map[string]string{"modules/a": "t1"}, nil), forge.get); err != nil { + t.Fatal(err) + } + want := []fileCall{{"novox", "mesh-delivery", "modules/a/module.json", merge}, {"novox", "mesh-delivery", "modules/a/go.sum", merge}, + {"novox", "mesh-delivery", "modules/a/package-lock.json", merge}} + if !reflect.DeepEqual(forge.calls, want) { + t.Errorf("calls = %v, want %v", forge.calls, want) + } +}