Files
mesh-catalog/modules/gitea/cmd/forge/patchfile_test.go
T
mesh-toolsandClaude d4b8b24701
mesh/merge-gate head d4b8b2470159 merged into trunk aee97bf8ad42 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head d4b8b2470159 merged into trunk aee97bf8ad42 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Let gitea_commit take a large patch from a hash-checked spool file (issue 852)
A feature's diff is megabytes, too large to type into a call. gitea_commit now also takes patch_file, the
random hex name of a file in the module's spool directory /var/lib/mesh/forge-patches (mode 0733, owned by
the machine's account), with patch_sha256. The tool reads only that directory, never through a symlink or a
hard link, refuses a file whose SHA-256 differs, applies it as it applies patch, and removes it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-15 20:39:13 +00:00

184 lines
6.8 KiB
Go

package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"
)
// novox/hq issue 852: a patch too large to type into a call is handed over as a file in one spool directory.
const spoolName = "0123456789abcdef0123456789abcdef"
const spoolPatch = "diff --git a/README.md b/README.md\n--- a/README.md\n+++ b/README.md\n@@ -1 +1 @@\n-hello\n+hi\n"
func sum(s string) string { h := sha256.Sum256([]byte(s)); return hex.EncodeToString(h[:]) }
func spoolWith(t *testing.T, name, content string) string {
t.Helper()
dir := t.TempDir()
if name != "" {
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil {
t.Fatal(err)
}
}
return dir
}
func fileAsk(t *testing.T, name, sha string) CommitAsk {
t.Helper()
return ask(t, map[string]any{"branch": "fix/9", "patch_file": name, "patch_sha256": sha})
}
func TestAPatchFileIsAppliedAsThePatchWouldBeAndTheFileIsGone(t *testing.T) {
f := newFake(t)
dir := spoolWith(t, spoolName, spoolPatch)
got, err := commitAsk(context.Background(), f.serve(), dir, fileAsk(t, spoolName, sum(spoolPatch)))
if err != nil {
t.Fatal(err)
}
if len(f.patches) != 1 || f.patches[0]["content"] != spoolPatch || f.patches[0]["new_branch"] != "fix/9" || got.Commit != "p1" {
t.Fatalf("%v %+v", f.patches, got)
}
if _, err := os.Lstat(filepath.Join(dir, spoolName)); !os.IsNotExist(err) {
t.Fatalf("the file is still there: %v", err)
}
}
func TestAPatchFileIsRemovedWhenTheForgeRefusesIt(t *testing.T) {
f := newFake(t)
dir := spoolWith(t, spoolName, spoolPatch)
// the default branch is refused after the file was read
a := ask(t, map[string]any{"branch": "main", "patch_file": spoolName, "patch_sha256": sum(spoolPatch)})
if _, err := commitAsk(context.Background(), f.serve(), dir, a); err == nil {
t.Fatal("taken")
}
if _, err := os.Lstat(filepath.Join(dir, spoolName)); !os.IsNotExist(err) {
t.Fatalf("the file is still there: %v", err)
}
}
func TestAPatchFileThatCannotBeTakenIsRefusedBeforeAnythingIsSent(t *testing.T) {
outside := filepath.Join(t.TempDir(), "secret")
if err := os.WriteFile(outside, []byte(spoolPatch), 0o600); err != nil {
t.Fatal(err)
}
type tc struct {
name string
setup func(dir string)
file string
sha string
want string
}
for _, c := range []tc{
{"wrong sha", func(d string) { _ = os.WriteFile(filepath.Join(d, spoolName), []byte(spoolPatch), 0o600) }, spoolName, sum("other"), "sha256"},
{"not found", func(string) {}, spoolName, sum(spoolPatch), "no such file"},
{"symlink", func(d string) { _ = os.Symlink(outside, filepath.Join(d, spoolName)) }, spoolName, sum(spoolPatch), "regular file"},
{"hard link", func(d string) { _ = os.Link(outside, filepath.Join(d, spoolName)) }, spoolName, sum(spoolPatch), "hard link"},
{"directory", func(d string) { _ = os.Mkdir(filepath.Join(d, spoolName), 0o700) }, spoolName, sum(spoolPatch), "regular file"},
{"too large", func(d string) {
_ = os.WriteFile(filepath.Join(d, spoolName), []byte(strings.Repeat("x", maxPatchBytes+1)), 0o600)
}, spoolName, sum(spoolPatch), "MiB"},
{"empty", func(d string) { _ = os.WriteFile(filepath.Join(d, spoolName), nil, 0o600) }, spoolName, sum(""), "empty"},
} {
f := newFake(t)
dir := t.TempDir()
c.setup(dir)
_, err := commitAsk(context.Background(), f.serve(), dir, fileAsk(t, c.file, c.sha))
if err == nil || !strings.Contains(err.Error(), c.want) || !strings.Contains(err.Error(), "Nothing was sent") {
t.Errorf("%s: %v", c.name, err)
}
if len(f.patches)+len(f.writes) != 0 {
t.Errorf("%s: something was sent", c.name)
}
}
if b, err := os.ReadFile(outside); err != nil || string(b) != spoolPatch {
t.Errorf("the file outside the spool was touched: %v", err)
}
}
func TestAPatchFileAskThatCannotBeReadIsRefused(t *testing.T) {
ok := sum(spoolPatch)
for name, args := range map[string]map[string]any{
"no sha": {"patch_file": spoolName},
"sha not hex": {"patch_file": spoolName, "patch_sha256": strings.Repeat("z", 64)},
"sha too short": {"patch_file": spoolName, "patch_sha256": ok[:63]},
"empty name": {"patch_file": "", "patch_sha256": ok, "files": filesArg()},
"dotdot": {"patch_file": "../" + spoolName, "patch_sha256": ok},
"slash": {"patch_file": "a/" + spoolName, "patch_sha256": ok},
"absolute": {"patch_file": "/etc/passwd", "patch_sha256": ok},
"too short": {"patch_file": strings.Repeat("a", 31), "patch_sha256": ok},
"too long": {"patch_file": strings.Repeat("a", 65), "patch_sha256": ok},
"upper case": {"patch_file": strings.ToUpper(spoolName), "patch_sha256": ok},
"not hex": {"patch_file": strings.Repeat("g", 32), "patch_sha256": ok},
"with patch": {"patch_file": spoolName, "patch_sha256": ok, "patch": "diff"},
"with files": {"patch_file": spoolName, "patch_sha256": ok, "files": filesArg(map[string]any{"path": "x", "content": "x"})},
"sha without it": {"patch": "diff", "patch_sha256": ok},
} {
full := map[string]any{"owner": "acme", "repo": "app", "message": "m", "branch": "b"}
for k, v := range args {
full[k] = v
}
_, err := readCommitAsk(full)
if err == nil || !strings.Contains(err.Error(), "Nothing was sent") {
t.Errorf("%s: %v", name, err)
}
}
// 64 characters is taken; so is an upper-case sha.
for _, n := range []string{spoolName, strings.Repeat("ab", 32)} {
if _, err := readCommitAsk(map[string]any{"owner": "acme", "repo": "app", "message": "m", "branch": "b",
"patch_file": n, "patch_sha256": strings.ToUpper(sum(spoolPatch))}); err != nil {
t.Errorf("%s: %v", n, err)
}
}
}
func TestAPatchFileWithoutASpoolDirectoryIsRefused(t *testing.T) {
f := newFake(t)
_, err := commitAsk(context.Background(), f.serve(), "", fileAsk(t, spoolName, sum(spoolPatch)))
if err == nil || !strings.Contains(err.Error(), "MESH_GITEA_PATCH_DIR") || !strings.Contains(err.Error(), "Nothing was sent") {
t.Fatalf("%v", err)
}
}
func TestTheManifestDeclaresTheSpoolDirectoryAndGivesItToTheBundle(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Resources []struct {
ID, Type, Path, Mode, Owner string
} `json:"resources"`
Build struct {
Artifacts []struct {
From string `json:"from"`
Env map[string]string `json:"env"`
} `json:"artifacts"`
} `json:"build"`
}
if err := jsonInto(raw, &m); err != nil {
t.Fatal(err)
}
found := false
for _, r := range m.Resources {
if r.ID == "forge-patches" {
found = true
if r.Type != "directory" || r.Mode != "0733" || r.Owner != "${machine:account}" || r.Path == "" {
t.Errorf("%+v", r)
}
}
}
if !found {
t.Error("no forge-patches directory")
}
for _, a := range m.Build.Artifacts {
if a.From == "cmd/forge" && a.Env["MESH_GITEA_PATCH_DIR"] != "${dir:forge-patches}" {
t.Errorf("env %v", a.Env)
}
}
}