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) } } }