Files
member-console/test/scripts_args_test.go
T
cgalo5758 78f507d9d3 Refuse an argument that is not a slug in bootstrap-stack.sh
bootstrap-stack.sh took its first argument as the stack's slug,
whatever it was, so `./bootstrap-stack.sh --help` would have written
--help into test/.env as COMPOSE_PROJECT_NAME. The slug becomes the
Compose project name, so the script now holds an explicit slug to
Compose's project-name rule (lowercase letters, digits, '-' and '_',
starting with a letter or a digit). For any other argument, or for
more than one, it prints its usage line on stderr and exits 2 before
its first git call. No argument, and a slug that follows the rule,
behave as before.

TestScriptsRefuseUnknownArguments gains a bootstrap-stack.sh group of
8 leaf subtests: 2 accepted (no argument, a slug) that stop at the git
stub, and 6 refused (--help, -h, an empty argument, an uppercase slug,
a slug led by an underscore, two arguments). At the parent the same
test file fails the 6 refusal rows, each running on to the git stub
and exiting 1 where 2 and the usage line are wanted, and passes the 2
accepted rows. At the commit all 23 leaf subtests of the test pass.

Not probed: the real script was not run, with or without an argument,
because it writes test/.env for the worktree it runs in.
2026-10-15 20:34:32 -05:00

192 lines
5.6 KiB
Go

// 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 and bootstrap-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, "", ""},
},
},
{
script: "bootstrap-stack.sh",
usage: "usage: cd test && ./bootstrap-stack.sh [slug]\n",
accept: []argRow{
{"no argument", nil, 1, "", "git"},
{"a slug", []string{"mc-review_2"}, 1, "", "git"},
},
refuse: []argRow{
{"help", []string{"--help"}, 2, "", ""},
{"short help", []string{"-h"}, 2, "", ""},
{"an empty argument", []string{""}, 2, "", ""},
{"an uppercase slug", []string{"My-slug"}, 2, "", ""},
{"a slug led by an underscore", []string{"_slug"}, 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
}