// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo package test import ( "bytes" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" ) // TestScriptsRefuseUnknownArguments runs a copy of each script from a // directory with no .env beside it, so the copy reaches no database or // container whatever it is given: an argument it accepts stops at the missing // .env (or, for teardown-stack.sh, at its first git call), and one it refuses // stops before that. The commands that reach a database, the session store or // the container engine are shims that record their name and fail, so a script // that ran on past both checks would show here instead of touching the stack. func TestScriptsRefuseUnknownArguments(t *testing.T) { for _, s := range []struct { script string usage string accept []argRow refuse []argRow }{ { script: "reset-app-db.sh", usage: "usage: cd test && ./reset-app-db.sh [--rebuild]\n", accept: []argRow{ {"no argument", nil, 1, "error: %s/.env not found. Run ./bootstrap-stack.sh first.\n", ""}, {"rebuild", []string{"--rebuild"}, 1, "error: %s/.env not found. Run ./bootstrap-stack.sh first.\n", ""}, }, refuse: []argRow{ {"help", []string{"--help"}, 2, "", ""}, {"short help", []string{"-h"}, 2, "", ""}, {"a word", []string{"rebuild"}, 2, "", ""}, {"an empty argument", []string{""}, 2, "", ""}, {"rebuild and another", []string{"--rebuild", "--help"}, 2, "", ""}, }, }, { script: "reset-test-db.sh", usage: "usage: cd test && ./reset-test-db.sh\n", accept: []argRow{ {"no argument", nil, 1, "error: %s/.env not found. Run ./bootstrap-stack.sh first.\n", ""}, }, refuse: []argRow{ {"help", []string{"--help"}, 2, "", ""}, {"an empty argument", []string{""}, 2, "", ""}, {"two arguments", []string{"a", "b"}, 2, "", ""}, }, }, { script: "teardown-stack.sh", usage: "usage: cd test && ./teardown-stack.sh\n", accept: []argRow{ {"no argument", nil, 1, "", "git"}, }, refuse: []argRow{ {"help", []string{"--help"}, 2, "", ""}, {"an empty argument", []string{""}, 2, "", ""}, {"two arguments", []string{"a", "b"}, 2, "", ""}, }, }, } { t.Run(s.script, func(t *testing.T) { sb := newScriptBox(t, s.script) rows := s.accept for _, r := range s.refuse { r.stderr = s.usage rows = append(rows, r) } for _, c := range rows { t.Run(c.name, func(t *testing.T) { c.check(t, sb) }) } }) } } // argRow is one invocation of a script and what it must do. type argRow struct { name string args []string code int stderr string // %s is the script's directory ran string // the shimmed commands the script may call } func (c argRow) check(t *testing.T, sb scriptBox) { t.Helper() want := c.stderr if strings.Contains(want, "%s") { want = fmt.Sprintf(want, sb.dir) } r := sb.run(t, c.args...) if r.code != c.code { t.Errorf("exit status %d, want %d (stderr %q)", r.code, c.code, r.stderr) } if r.stderr != want { t.Errorf("stderr %q, want %q", r.stderr, want) } if r.stdout != "" { t.Errorf("stdout %q, want nothing", r.stdout) } if r.ran != c.ran { t.Errorf("the script ran %q, want %q", r.ran, c.ran) } } // scriptBox holds a copy of one test/ script in a directory of its own, with // shims in place of every command that reaches the stack. type scriptBox struct { bash, dir, script, bin, calls string } type scriptRun struct { code int stdout, stderr string ran string // the shimmed commands the script called, in order } func newScriptBox(t *testing.T, name string) scriptBox { t.Helper() src, err := os.ReadFile(name) if err != nil { t.Fatal(err) } bash, err := exec.LookPath("bash") if err != nil { t.Fatalf("bash: %v", err) } dir := t.TempDir() sb := scriptBox{bash: bash, dir: dir, script: filepath.Join(dir, name), bin: filepath.Join(dir, "bin"), calls: filepath.Join(dir, "calls")} if err := os.WriteFile(sb.script, src, 0o755); err != nil { t.Fatal(err) } if err := os.Mkdir(sb.bin, 0o755); err != nil { t.Fatal(err) } for _, shim := range []string{"psql", "pg_dump", "docker", "valkey-cli", "redis-cli", "go", "git"} { body := "#!/bin/sh\necho " + shim + " >> \"$SHIM_LOG\"\nexit 1\n" if err := os.WriteFile(filepath.Join(sb.bin, shim), []byte(body), 0o755); err != nil { t.Fatal(err) } } return sb } func (sb scriptBox) run(t *testing.T, args ...string) scriptRun { t.Helper() _ = os.Remove(sb.calls) cmd := exec.Command(sb.bash, append([]string{sb.script}, args...)...) cmd.Dir = sb.dir cmd.Env = []string{"PATH=" + sb.bin + ":/usr/bin:/bin", "SHIM_LOG=" + sb.calls} var stdout, stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &stdout, &stderr r := scriptRun{} if err := cmd.Run(); err != nil { var exit *exec.ExitError if !errors.As(err, &exit) { t.Fatal(err) } r.code = exit.ExitCode() } r.stdout, r.stderr = stdout.String(), stderr.String() if got, err := os.ReadFile(sb.calls); err == nil { r.ran = strings.Join(strings.Fields(string(got)), ", ") } return r }