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
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>
184 lines
6.8 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|