Test the forge bridge's file reads against the stand-in forge and the bus (issue 1448, design 47 R4c-2)
This commit is contained in:
+76
-7
@@ -4,6 +4,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -29,8 +31,8 @@ import (
|
||||
// bucket performed is the bridge's own (its state), and the five forge tools of the gate's acts are invoked (issue 1341, 1342).
|
||||
//
|
||||
// The lists are what mesh-controller's broker.PermissionsFor answers (internal/broker/nats.go:577) for a module principal
|
||||
// with the manifest of module.json: consumes the three gitea.pull events, invokes gitea.gitea_open_pulls and
|
||||
// gitea.gitea_commit_trees, state heads, and the stream forge; `**` is written `>` on the wire (modstreams.go
|
||||
// with the manifest of module.json: consumes the three gitea.pull events, invokes gitea.gitea_open_pulls,
|
||||
// gitea.gitea_commit_trees and gitea.gitea_get_file, state heads, and the stream forge; `**` is written `>` on the wire (modstreams.go
|
||||
// WirePattern). The consumer's durable is <node>_<module> on EVENTS (nats.go consumerDurable). The controller's own
|
||||
// user keeps all rights, as it has.
|
||||
const busConf = `
|
||||
@@ -59,6 +61,10 @@ accounts {
|
||||
"mesh.mod.gitea.call.gitea_open_pulls.n1~mesh-delivery-forge-bridge"
|
||||
"mesh.mod.gitea.tool.gitea_commit_trees"
|
||||
"mesh.mod.gitea.tool.gitea_commit_trees.*"
|
||||
"mesh.mod.gitea.call.gitea_get_file.*.n1~mesh-delivery-forge-bridge"
|
||||
"mesh.mod.gitea.call.gitea_get_file.n1~mesh-delivery-forge-bridge"
|
||||
"mesh.mod.gitea.tool.gitea_get_file"
|
||||
"mesh.mod.gitea.tool.gitea_get_file.*"
|
||||
"mesh.mod.gitea.tool.gitea_open_pulls"
|
||||
"mesh.mod.gitea.tool.gitea_open_pulls.*"
|
||||
"$JS.API.CONSUMER.CREATE.KV_mesh-delivery-forge-bridge_performed.>"
|
||||
@@ -160,6 +166,21 @@ type bed struct {
|
||||
ctx context.Context
|
||||
done <-chan error
|
||||
reply func(string)
|
||||
// files sets what the stand-in forge answers to gitea_get_file from then on; it is asked with the tool's arguments.
|
||||
files func(func(owner, repo, path, ref string) (string, bool))
|
||||
}
|
||||
|
||||
// getFileReply is the reply envelope for gitea_get_file as mesh-catalog modules/gitea tools/index.ts and client.ts getFile
|
||||
// answer it ({file: {path, ref, sha, size, content}}), and for a path the forge does not have the error client.ts request
|
||||
// writes: `Gitea API <path>: 404 <body>`.
|
||||
func getFileReply(path, ref, content string, found bool) []byte {
|
||||
if !found {
|
||||
b, _ := json.Marshal(map[string]any{"error": "Gitea API /repos/novox/mesh-delivery/contents/" + path + "?ref=" + ref + `: 404 {"message":"GetContentsOrList","url":"https://forge.example/api/swagger"}`})
|
||||
return b
|
||||
}
|
||||
b, _ := json.Marshal(map[string]any{"result": map[string]any{"file": map[string]any{"path": path, "ref": ref, "sha": "0123456789abcdef0123456789abcdef01234567",
|
||||
"size": len(content), "content": content}}, "node": "n0"})
|
||||
return b
|
||||
}
|
||||
|
||||
// startBed is startBridge with a step between the streams and the consumer: prepare writes what the streams hold before
|
||||
@@ -222,6 +243,26 @@ func startBed(t *testing.T, openReply string, prepare func(ctx context.Context,
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// A stand-in answers the forge's gitea_get_file: by default a module.json of {} at every path and no other file.
|
||||
var fmu sync.Mutex
|
||||
filesOf := func(_, _, path, _ string) (string, bool) { return "{}", strings.HasSuffix(path, "module.json") }
|
||||
getFile, err := ctl.Subscribe("mesh.mod.gitea.tool.gitea_get_file", func(m *nats.Msg) {
|
||||
var a struct{ Owner, Repo, Path, Ref string }
|
||||
_ = json.Unmarshal(m.Data, &a)
|
||||
fmu.Lock()
|
||||
f := filesOf
|
||||
fmu.Unlock()
|
||||
content, found := f(a.Owner, a.Repo, a.Path, a.Ref)
|
||||
_ = m.Respond(getFileReply(a.Path, a.Ref, content, found))
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = getFile.Unsubscribe() })
|
||||
if err := ctl.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
broker := filepath.Join(t.TempDir(), "broker")
|
||||
cred, _ := json.Marshal(credential{URL: srv.ClientURL(), Node: "n1", Module: "mesh-delivery-forge-bridge", User: "n1.mesh-delivery-forge-bridge", Password: "pw"})
|
||||
if err := os.WriteFile(broker, cred, 0o600); err != nil {
|
||||
@@ -232,7 +273,8 @@ func startBed(t *testing.T, openReply string, prepare func(ctx context.Context,
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- run(runCtx, func(k string) string { return env[k] }, io.Discard) }()
|
||||
t.Cleanup(stop)
|
||||
return &bed{srv: srv, ctl: ctl, js: js, ctx: ctx, done: done, reply: func(s string) { mu.Lock(); current = s; mu.Unlock() }}
|
||||
return &bed{srv: srv, ctl: ctl, js: js, ctx: ctx, done: done, reply: func(s string) { mu.Lock(); current = s; mu.Unlock() },
|
||||
files: func(f func(owner, repo, path, ref string) (string, bool)) { fmu.Lock(); filesOf = f; fmu.Unlock() }}
|
||||
}
|
||||
|
||||
// TestBridgeWritesPullMessages starts the program the way the node does (its entry, run, with MESH_BROKER_FILE).
|
||||
@@ -270,7 +312,7 @@ func TestBridgeWritesPullMessages(t *testing.T) { // 1235/AC2
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := delivery.Forge{V: delivery.Version, Kind: "pull", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head,
|
||||
Action: "updated", Dirs: []string{"modules/a"}, DirsKnown: true}
|
||||
Action: "updated", 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)
|
||||
}
|
||||
@@ -295,8 +337,31 @@ const merge = "89abcdef0123456789abcdef0123456789abcdef"
|
||||
// TestBridgeWritesMergedMessage has a stand-in answer the forge's gitea_commit_trees on its tool subject
|
||||
// (mesh-control internal/broker/nats.go invokedSubjects: mesh.mod.<module>.tool.<tool>), in the reply envelope of
|
||||
// mesh-sdk go/process/call.go ({result, error, node}).
|
||||
func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4
|
||||
ctl, js, ctx, _ := startBridge(t, `{"result":{"pulls":[]}}`)
|
||||
func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4, 1448/AC5
|
||||
bed := startBed(t, `{"result":{"pulls":[]}}`, nil)
|
||||
ctl, js, ctx := bed.ctl, bed.js, bed.ctx
|
||||
// The stand-in forge's files are two real files of this repository (this module's module.json and go.sum, read from the
|
||||
// working tree), in gitea_get_file's real answer shape: the root has a manifest only, modules/a a manifest and go.sum.
|
||||
manifest, err := os.ReadFile("../../module.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sum, err := os.ReadFile("../../go.sum")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bed.files(func(_, _, path, ref string) (string, bool) {
|
||||
if ref != merge {
|
||||
t.Errorf("the forge was asked for %s at %s, want the merge commit %s", path, ref, merge)
|
||||
}
|
||||
switch path {
|
||||
case "module.json", "modules/a/module.json":
|
||||
return string(manifest), true
|
||||
case "modules/a/go.sum":
|
||||
return string(sum), true
|
||||
}
|
||||
return "", false
|
||||
})
|
||||
var asked chan []byte = make(chan []byte, 4)
|
||||
sub, err := ctl.Subscribe("mesh.mod.gitea.tool.gitea_commit_trees", func(m *nats.Msg) {
|
||||
asked <- m.Data
|
||||
@@ -342,7 +407,9 @@ func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := delivery.Forge{V: delivery.Version, Kind: "merged", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head,
|
||||
Merge: merge, Root: "r0", Trees: map[string]string{"": "t0", "modules/a": "t1"}}
|
||||
Merge: merge, Root: "r0", Trees: map[string]string{"": "t0", "modules/a": "t1"},
|
||||
Manifests: map[string]string{"": string(manifest), "modules/a": string(manifest)},
|
||||
Locks: map[string]map[string]string{"modules/a": {"go.sum": hex.EncodeToString(sha256Sum(sum))}}}
|
||||
if !delivery.SameForge(f, want) {
|
||||
t.Errorf("message = %+v, want %+v", f, want)
|
||||
}
|
||||
@@ -637,3 +704,5 @@ func forgeMerged(t *testing.T, b *bed) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sha256Sum(b []byte) []byte { s := sha256.Sum256(b); return s[:] }
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"mesh-delivery-forge-bridge/internal/bridge"
|
||||
)
|
||||
|
||||
const head = "0123456789abcdef0123456789abcdef01234567"
|
||||
@@ -64,7 +67,7 @@ func body(m map[string]any) []byte {
|
||||
func TestReadableEventIsWrittenOnForgeAndAcked(t *testing.T) { // 1235/AC2 (unit part; the bus is the integration test)
|
||||
var logs bytes.Buffer
|
||||
m, pub := &fakeMsg{subject: "mesh.mod.gitea.event.pull.updated", data: body(nil)}, &fakePub{}
|
||||
handle(context.Background(), m, pub, nil, log.New(&logs, "", 0))
|
||||
handle(context.Background(), m, pub, nil, someFiles, log.New(&logs, "", 0))
|
||||
if len(pub.subjects) != 1 || pub.subjects[0] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.pull" {
|
||||
t.Fatalf("published on %v", pub.subjects)
|
||||
}
|
||||
@@ -80,7 +83,7 @@ func TestReadableEventIsWrittenOnForgeAndAcked(t *testing.T) { // 1235/AC2 (unit
|
||||
func TestUnreadableEventIsTerminatedAndLogged(t *testing.T) { // 1235/AC5
|
||||
var logs bytes.Buffer
|
||||
m, pub := &fakeMsg{subject: "mesh.mod.gitea.event.pull.updated", data: body(map[string]any{"head_sha": "short"})}, &fakePub{}
|
||||
handle(context.Background(), m, pub, nil, log.New(&logs, "", 0))
|
||||
handle(context.Background(), m, pub, nil, someFiles, log.New(&logs, "", 0))
|
||||
if len(pub.subjects) != 0 {
|
||||
t.Errorf("a message was written: %v", pub.subjects)
|
||||
}
|
||||
@@ -95,7 +98,7 @@ func TestUnreadableEventIsTerminatedAndLogged(t *testing.T) { // 1235/AC5
|
||||
func TestUnwrittenMessageIsNotAcked(t *testing.T) { // 1235 (a failed write is redelivered by the server, not retried here)
|
||||
var logs bytes.Buffer
|
||||
m, pub := &fakeMsg{subject: "mesh.mod.gitea.event.pull.updated", data: body(nil)}, &fakePub{err: errors.New("no stream")}
|
||||
handle(context.Background(), m, pub, nil, log.New(&logs, "", 0))
|
||||
handle(context.Background(), m, pub, nil, someFiles, log.New(&logs, "", 0))
|
||||
if m.acked != 0 || m.termd != 0 {
|
||||
t.Errorf("acked %d terminated %d, want neither", m.acked, m.termd)
|
||||
}
|
||||
@@ -117,7 +120,7 @@ func TestTreesRefusedLeavesTheEventForTenSecondsAndLogs(t *testing.T) { // 1236/
|
||||
call := func(context.Context, string, any) (json.RawMessage, error) {
|
||||
return nil, errors.New("the tree listing of novox/mesh-delivery was cut")
|
||||
}
|
||||
handle(context.Background(), m, pub, treesVia(context.Background(), call), log.New(&logs, "", 0))
|
||||
handle(context.Background(), m, pub, treesVia(context.Background(), call), filesVia(context.Background(), call), log.New(&logs, "", 0))
|
||||
if len(pub.subjects) != 0 {
|
||||
t.Errorf("a message was written: %v", pub.subjects)
|
||||
}
|
||||
@@ -138,11 +141,14 @@ func TestMergeCallsTheToolOnceAndWritesTheMergedMessage(t *testing.T) { // 1236/
|
||||
var args []any
|
||||
call := func(_ context.Context, key string, body any) (json.RawMessage, error) {
|
||||
keys, args = append(keys, key), append(args, body)
|
||||
if key == "gitea.gitea_get_file" { // the manifests, 1448; the trees are what this test is about
|
||||
return json.RawMessage(`{"file":{"path":"p","sha":"0123","size":2,"content":"{}"}}`), nil
|
||||
}
|
||||
return json.RawMessage(`{"root":"r0","trees":{"":"t0","modules/a":"t1"}}`), nil
|
||||
}
|
||||
handle(context.Background(), m, pub, treesVia(context.Background(), call), log.New(&logs, "", 0))
|
||||
if len(keys) != 1 || keys[0] != "gitea.gitea_commit_trees" {
|
||||
t.Fatalf("calls %v", keys)
|
||||
handle(context.Background(), m, pub, treesVia(context.Background(), call), filesVia(context.Background(), call), log.New(&logs, "", 0))
|
||||
if len(keys) == 0 || keys[0] != "gitea.gitea_commit_trees" || slices.Index(keys[1:], "gitea.gitea_commit_trees") >= 0 {
|
||||
t.Fatalf("calls %v, want the trees once and first", keys)
|
||||
}
|
||||
if got, _ := json.Marshal(args[0]); string(got) != `{"owner":"novox","repo":"mesh-delivery","sha":"89abcdef0123456789abcdef0123456789abcdef"}` {
|
||||
t.Errorf("arguments %s", got)
|
||||
@@ -171,3 +177,52 @@ func TestAnAnswerWithoutARootIsNotUsed(t *testing.T) { // 1236 (a shape the tool
|
||||
t.Error("an answer that is not an object was taken")
|
||||
}
|
||||
}
|
||||
|
||||
// The answer of gitea_get_file as mesh-catalog modules/gitea tools/index.ts and client.ts getFile write it, and the
|
||||
// error of a path the forge does not have as client.ts request writes it (`Gitea API ${path}: ${status} ${text}`).
|
||||
const getFileAnswer = `{"file":{"path":"modules/a/go.sum","ref":"89abcdef0123456789abcdef0123456789abcdef","sha":"0123","size":4,"content":"x y\n"}}`
|
||||
const getFile404 = "Gitea API /repos/novox/mesh-delivery/contents/modules/a/go.sum?ref=89abcdef: 404 {\"message\":\"GetContentsOrList\",\"url\":\"https://forge.example/api/swagger\"}"
|
||||
|
||||
func TestFilesViaAsksByOwnerRepoPathRefAndTakesTheContent(t *testing.T) { // 1448/AC4
|
||||
var keys []string
|
||||
var args []any
|
||||
call := func(_ context.Context, key string, body any) (json.RawMessage, error) {
|
||||
keys, args = append(keys, key), append(args, body)
|
||||
return json.RawMessage(getFileAnswer), nil
|
||||
}
|
||||
got, err := filesVia(context.Background(), call)("novox", "mesh-delivery", "modules/a/go.sum", "89abcdef0123456789abcdef0123456789abcdef")
|
||||
if err != nil || string(got) != "x y\n" {
|
||||
t.Fatalf("got %q err %v", got, err)
|
||||
}
|
||||
if len(keys) != 1 || keys[0] != "gitea.gitea_get_file" {
|
||||
t.Fatalf("calls %v", keys)
|
||||
}
|
||||
if b, _ := json.Marshal(args[0]); string(b) != `{"owner":"novox","path":"modules/a/go.sum","ref":"89abcdef0123456789abcdef0123456789abcdef","repo":"mesh-delivery"}` {
|
||||
t.Errorf("arguments %s", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesViaTellsNotFoundFromOtherErrorsAndOtherShapes(t *testing.T) { // 1448/AC3
|
||||
call := func(err error, raw string) caller {
|
||||
return func(context.Context, string, any) (json.RawMessage, error) { return json.RawMessage(raw), err }
|
||||
}
|
||||
if _, err := filesVia(context.Background(), call(errors.New(getFile404), ""))("o", "r", "p", "s"); !errors.Is(err, bridge.ErrNotFound) {
|
||||
t.Errorf("a 404 is %v, want ErrNotFound", err)
|
||||
}
|
||||
if _, err := filesVia(context.Background(), call(errors.New("Gitea API /repos/o/r/contents/p: 502 bad gateway"), ""))("o", "r", "p", "s"); err == nil || errors.Is(err, bridge.ErrNotFound) {
|
||||
t.Errorf("a 502 is %v, want an error that is not ErrNotFound", err)
|
||||
}
|
||||
for _, raw := range []string{`{"file":null}`, `{}`, `[1]`, `{"file":{"path":"p"}}`} {
|
||||
if _, err := filesVia(context.Background(), call(nil, raw))("o", "r", "p", "s"); err == nil || errors.Is(err, bridge.ErrNotFound) {
|
||||
t.Errorf("the answer %s gave %v, want an error", raw, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// someFiles is a forge that has a module.json at every path and no other file, for the tests that are not about the files.
|
||||
func someFiles(_, _, path, _ string) ([]byte, error) {
|
||||
if strings.HasSuffix(path, "module.json") {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
return nil, bridge.ErrNotFound
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user