DB-backed repo_path_allowlist reader (deny-all until admin seeds), per-prefix admin test_commands (goose 00075), execution evidence table (goose 00074), isolated git-worktree patch executor with command allowlist/time/output caps and failure-stops evidence; controlled_patch creation/preview unlocked only with an allowlisted repo binding (projectworkflow gate + catalog flag); creator-only server endpoints for bindings/executions; merge/push/deploy stay behind a typed separate-authorization boundary. Module boundary: app->swboot composition import registered.
163 lines
5.5 KiB
Go
163 lines
5.5 KiB
Go
package swboot
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type fakeAllowlist struct {
|
|
paths map[string]bool
|
|
commands map[string][]string
|
|
}
|
|
|
|
func (f *fakeAllowlist) IsAllowlisted(p string) bool { return f.paths[filepath.Clean(p)] }
|
|
func (f *fakeAllowlist) AllowedCommands(p string) []string {
|
|
return f.commands[filepath.Clean(p)]
|
|
}
|
|
|
|
func initTestRepo(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
run := func(args ...string) {
|
|
c := exec.Command("git", args...)
|
|
c.Dir = dir
|
|
c.Env = []string{"PATH=" + os.Getenv("PATH"), "GIT_TERMINAL_PROMPT=0",
|
|
"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t",
|
|
"GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null"}
|
|
if out, err := c.CombinedOutput(); err != nil {
|
|
t.Fatalf("git %v: %v: %s", args, err, out)
|
|
}
|
|
}
|
|
run("init", "-q", "-b", "main")
|
|
if err := os.WriteFile(filepath.Join(dir, "hello.txt"), []byte("hello\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
run("add", ".")
|
|
run("commit", "-q", "-m", "init")
|
|
return dir
|
|
}
|
|
|
|
const samplePatch = `--- a/hello.txt
|
|
+++ b/hello.txt
|
|
@@ -1 +1 @@
|
|
-hello
|
|
+hello swboot
|
|
`
|
|
|
|
func newExecutor(allow RepoAllowlistReader) *ControlledPatchExecutor {
|
|
return &ControlledPatchExecutor{Allowlist: allow}
|
|
}
|
|
|
|
func TestExecutor_RejectsNonAllowlistedPath(t *testing.T) {
|
|
repo := initTestRepo(t)
|
|
e := newExecutor(&fakeAllowlist{paths: map[string]bool{"/definitely/not/allowed": true}})
|
|
res, err := e.Execute(context.Background(), ControlledPatchRequest{
|
|
ProjectID: 1, RepoPath: repo, BaseRef: "HEAD", Patches: []string{samplePatch},
|
|
})
|
|
if err == nil || res.Status != "failed" || !strings.Contains(res.Error, "ERR_REPO_PATH_NOT_ALLOWLISTED") {
|
|
t.Fatalf("expected allowlist rejection, got res=%+v err=%v", res, err)
|
|
}
|
|
if res.WorktreePath != "" {
|
|
t.Fatalf("no worktree must be created on rejection")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_AppliesPatchOnIsolatedWorktree(t *testing.T) {
|
|
repo := initTestRepo(t)
|
|
// Baseline untouched.
|
|
e := newExecutor(&fakeAllowlist{paths: map[string]bool{repo: true}})
|
|
res, err := e.Execute(context.Background(), ControlledPatchRequest{
|
|
ProjectID: 7, RepoPath: repo, BaseRef: "HEAD", Patches: []string{samplePatch},
|
|
})
|
|
if err != nil || res.Status != "succeeded" {
|
|
t.Fatalf("expected success, got res=%+v err=%v", res, err)
|
|
}
|
|
if res.PatchesApplied != 1 || len(res.Files) != 1 || res.Files[0].Path != "hello.txt" || res.Files[0].Sha == "" {
|
|
t.Fatalf("unexpected files evidence: %+v", res.Files)
|
|
}
|
|
if !strings.HasPrefix(res.Branch, "feature/swboot-") || res.WorktreePath == "" {
|
|
t.Fatalf("expected isolated branch/worktree: %+v", res)
|
|
}
|
|
// Patch landed only in the isolated worktree.
|
|
patched, err := os.ReadFile(filepath.Join(res.WorktreePath, "hello.txt"))
|
|
if err != nil || !strings.Contains(string(patched), "hello swboot") {
|
|
t.Fatalf("worktree not patched: %v %q", err, patched)
|
|
}
|
|
base, _ := os.ReadFile(filepath.Join(repo, "hello.txt"))
|
|
if strings.Contains(string(base), "swboot") {
|
|
t.Fatalf("repository working tree must stay untouched")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_RunsOnlyAllowlistedCommands(t *testing.T) {
|
|
repo := initTestRepo(t)
|
|
e := newExecutor(&fakeAllowlist{
|
|
paths: map[string]bool{repo: true},
|
|
commands: map[string][]string{repo: {"echo"}},
|
|
})
|
|
res, err := e.Execute(context.Background(), ControlledPatchRequest{
|
|
ProjectID: 8, RepoPath: repo, BaseRef: "HEAD",
|
|
Patches: []string{samplePatch},
|
|
Commands: []string{"echo ci-ok"},
|
|
})
|
|
if err != nil || res.Status != "succeeded" {
|
|
t.Fatalf("expected success: %+v %v", res, err)
|
|
}
|
|
if len(res.Commands) != 1 || res.Commands[0].ExitCode != 0 || !strings.Contains(res.Commands[0].Output, "ci-ok") {
|
|
t.Fatalf("command evidence wrong: %+v", res.Commands)
|
|
}
|
|
|
|
res2, err2 := e.Execute(context.Background(), ControlledPatchRequest{
|
|
ProjectID: 8, RepoPath: repo, BaseRef: "HEAD",
|
|
Patches: []string{samplePatch},
|
|
Commands: []string{"curl http://169.254.169.254/latest/meta-data"},
|
|
})
|
|
if err2 == nil || res2.Status != "failed" || !strings.Contains(res2.Error, "not in admin allowlist") {
|
|
t.Fatalf("non-allowlisted command must fail closed: %+v %v", res2, err2)
|
|
}
|
|
}
|
|
|
|
func TestExecutor_RejectsBrokenPatchAndKeepsEvidence(t *testing.T) {
|
|
repo := initTestRepo(t)
|
|
e := newExecutor(&fakeAllowlist{paths: map[string]bool{repo: true}})
|
|
res, err := e.Execute(context.Background(), ControlledPatchRequest{
|
|
ProjectID: 9, RepoPath: repo, BaseRef: "HEAD",
|
|
Patches: []string{"not a unified diff"},
|
|
})
|
|
if err == nil || res.Status != "failed" {
|
|
t.Fatalf("broken patch must fail: %+v %v", res, err)
|
|
}
|
|
if res.PatchesApplied != 0 {
|
|
t.Fatalf("no patch applied on failure: %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CommandFailureStopsPipeline(t *testing.T) {
|
|
repo := initTestRepo(t)
|
|
e := newExecutor(&fakeAllowlist{
|
|
paths: map[string]bool{repo: true},
|
|
commands: map[string][]string{repo: {"false", "echo"}},
|
|
})
|
|
res, err := e.Execute(context.Background(), ControlledPatchRequest{
|
|
ProjectID: 10, RepoPath: repo, BaseRef: "HEAD",
|
|
Patches: []string{samplePatch},
|
|
Commands: []string{"false", "echo unreachable"},
|
|
})
|
|
if err == nil || res.Status != "failed" {
|
|
t.Fatalf("failing command must stop: %+v %v", res, err)
|
|
}
|
|
if len(res.Commands) != 1 || res.Commands[0].ExitCode == 0 {
|
|
t.Fatalf("first command must record failure and stop: %+v", res.Commands)
|
|
}
|
|
}
|
|
|
|
func TestApproveMerge_NeverExecutes(t *testing.T) {
|
|
if err := ApproveMerge("anything"); err == nil || !strings.Contains(err.Error(), "ERR_MERGE_REQUIRES_SEPARATE_AUTHORIZATION") {
|
|
t.Fatalf("merge must stay behind separate authorization: %v", err)
|
|
}
|
|
}
|