feat(sunrise): update an application in place with summercms.io/update
- new.sh gains --update, run inside one application directory (go.mod and summer.yaml required); it changes only that application - the framework checkout is read from the app's go.mod replace (single-line or block form); module-version replaces, missing targets and foreign or dirty checkouts are refused through the shared check_framework_dir - a detached checkout moves fast-forward only to the newest release (or SUMMERCMS_REF) through the shared move_detached; a branch checkout is pulled with git pull --no-rebase --ff-only, refusing a missing upstream or a diverged branch with the checkout unchanged - other sm-*-app directories on the same checkout are named on one "not rebuilt" line and never built or written - summer is reinstalled, a missing SUMMER_ADMIN__JWT__SECRET is appended to this app's .env (openssl, else head and base64; mode kept, never printed), then summer build runs and the script ends with the migrate and restart reminder and the shared path_hint - sunrise.UpdateScript() serves the same script with its last line changed to main "$@" --update, so truncated downloads still run nothing - check-phase14.3.sh --forbidden now allows only the literal SUMMER_ADMIN__JWT__SECRET in new.sh and still fails on any other SUMMER_ name or on a read or export of one; TestUpdateScript and TestUpdateScriptRoute are required tests
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -16,16 +17,17 @@ import (
|
||||
// `go -C <dir> install ./cmd/summer`, writes a stub summer into the Go bin
|
||||
// directory that records its working directory, arguments and stdin.
|
||||
type scriptHarness struct {
|
||||
t *testing.T
|
||||
bash string
|
||||
script string
|
||||
root string // per-case temp root
|
||||
work string // the directory the script runs in
|
||||
tools string // the PATH directory
|
||||
logs string // stub call records
|
||||
gobin string
|
||||
gopath string
|
||||
remote *fakeRemote
|
||||
t *testing.T
|
||||
bash string
|
||||
script string
|
||||
updateFile string // UpdateScript() bytes
|
||||
root string // per-case temp root
|
||||
work string // the directory the script runs in
|
||||
tools string // the PATH directory
|
||||
logs string // stub call records
|
||||
gobin string
|
||||
gopath string
|
||||
remote *fakeRemote
|
||||
|
||||
goVersion string // go env GOVERSION
|
||||
useGOBIN bool // go env GOBIN prints gobin; otherwise empty, GOPATH/bin is used
|
||||
@@ -176,14 +178,33 @@ func newScriptHarness(t *testing.T) *scriptHarness {
|
||||
if err := os.WriteFile(h.script, Script(), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h.updateFile = filepath.Join(root, "update.sh")
|
||||
if err := os.WriteFile(h.updateFile, UpdateScript(), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(gitBin, filepath.Join(h.tools, "git")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Update mode makes the admin token secret with head and base64 when
|
||||
// openssl is missing; a case adds an openssl stub with stubOpenssl.
|
||||
for _, tool := range []string{"head", "base64"} {
|
||||
p, err := exec.LookPath(tool)
|
||||
if err != nil {
|
||||
t.Skip(tool + " not installed")
|
||||
}
|
||||
if p, err = filepath.Abs(p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(p, filepath.Join(h.tools, tool)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
summerStub := "#!" + bash + `
|
||||
{ printf 'cwd=%s\n' "$(pwd)"; printf 'arg=%s\n' "$@"; } >"$STUB_LOG/summer.call"
|
||||
data=""
|
||||
while IFS= read -r line || [[ -n $line ]]; do data+="$line"; done
|
||||
printf '%s' "$data" >"$STUB_LOG/summer.stdin"
|
||||
[[ -z ${STUB_SUMMER_FAIL:-} ]] || exit 1
|
||||
`
|
||||
goStub := "#!" + bash + `
|
||||
printf '%s\n' "$*" >>"$STUB_LOG/go.calls"
|
||||
@@ -226,6 +247,24 @@ func (h *scriptHarness) without(tool string) {
|
||||
|
||||
// run executes the script with args in h.work.
|
||||
func (h *scriptHarness) run(args ...string) scriptResult {
|
||||
h.t.Helper()
|
||||
return h.runFile(h.script, h.work, args...)
|
||||
}
|
||||
|
||||
// runIn executes the script with args in dir.
|
||||
func (h *scriptHarness) runIn(dir string, args ...string) scriptResult {
|
||||
h.t.Helper()
|
||||
return h.runFile(h.script, dir, args...)
|
||||
}
|
||||
|
||||
// runUpdate executes UpdateScript() with args in dir.
|
||||
func (h *scriptHarness) runUpdate(dir string, args ...string) scriptResult {
|
||||
h.t.Helper()
|
||||
return h.runFile(h.updateFile, dir, args...)
|
||||
}
|
||||
|
||||
// runFile executes file with args in dir; cmd.Dir and PWD both name dir.
|
||||
func (h *scriptHarness) runFile(file, dir string, args ...string) scriptResult {
|
||||
h.t.Helper()
|
||||
path := h.tools
|
||||
if h.gobinOnPath {
|
||||
@@ -237,7 +276,7 @@ func (h *scriptHarness) run(args ...string) scriptResult {
|
||||
}
|
||||
env := append(gitTestEnv(filepath.Join(h.root, "home")),
|
||||
"PATH="+path,
|
||||
"PWD="+h.work,
|
||||
"PWD="+dir,
|
||||
"GIT_CEILING_DIRECTORIES="+h.root,
|
||||
"SUMMERCMS_REPO="+h.remote.bare,
|
||||
"STUB_LOG="+h.logs,
|
||||
@@ -258,13 +297,13 @@ func (h *scriptHarness) run(args ...string) scriptResult {
|
||||
filtered = append(filtered, kv)
|
||||
}
|
||||
env = append(filtered, h.env...)
|
||||
cmd := exec.Command(h.bash, append([]string{h.script}, args...)...)
|
||||
cmd := exec.Command(h.bash, append([]string{file}, args...)...)
|
||||
if h.unsetShell {
|
||||
// Unset SHELL inside bash, after startup filled it, then source the
|
||||
// script ($0) with the case arguments so it runs with SHELL unset.
|
||||
cmd = exec.Command(h.bash, append([]string{"-c", `unset SHELL; . "$0" "$@"`, h.script}, args...)...)
|
||||
cmd = exec.Command(h.bash, append([]string{"-c", `unset SHELL; . "$0" "$@"`, file}, args...)...)
|
||||
}
|
||||
cmd.Dir = h.work
|
||||
cmd.Dir = dir
|
||||
cmd.Env = env
|
||||
cmd.Stdin = strings.NewReader(stdinProbe)
|
||||
var stdout, stderr bytes.Buffer
|
||||
@@ -411,11 +450,159 @@ func expectRefusedUnchanged(t *testing.T, h *scriptHarness, res scriptResult, be
|
||||
// resetLogs clears stub records between runs of one case.
|
||||
func (h *scriptHarness) resetLogs() {
|
||||
h.t.Helper()
|
||||
for _, n := range []string{"go.calls", "go.install", "summer.call", "summer.stdin", "docker.calls"} {
|
||||
for _, n := range []string{"go.calls", "go.install", "summer.call", "summer.stdin", "docker.calls", "openssl.calls"} {
|
||||
_ = os.Remove(filepath.Join(h.logs, n))
|
||||
}
|
||||
}
|
||||
|
||||
// replaceLine is a single-line go.mod replace of the framework.
|
||||
func replaceLine(target string) string {
|
||||
return "replace " + fwModule + " => " + target
|
||||
}
|
||||
|
||||
// appAt writes an application fixture into dir: go.mod and summer.yaml.
|
||||
func (h *scriptHarness) appAt(dir, gomod string) string {
|
||||
h.t.Helper()
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
h.t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0o644); err != nil {
|
||||
h.t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "summer.yaml"), []byte("name: x\n"), 0o644); err != nil {
|
||||
h.t.Fatal(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// app writes an application fixture at <work>/sm-<name>-app.
|
||||
func (h *scriptHarness) app(name, gomod string) string {
|
||||
h.t.Helper()
|
||||
return h.appAt(filepath.Join(h.work, "sm-"+name+"-app"), gomod)
|
||||
}
|
||||
|
||||
// appGoMod is a go.mod whose framework replace points at target.
|
||||
func appGoMod(name, target string) string {
|
||||
return "module example.com/" + name + "\n\n" + replaceLine(target) + "\n"
|
||||
}
|
||||
|
||||
// updateReady clones the framework with a create run, writes sm-blog-app
|
||||
// pointing at it and clears the stub records; it returns the app directory.
|
||||
func (h *scriptHarness) updateReady() string {
|
||||
h.t.Helper()
|
||||
h.mustRun("demo")
|
||||
app := h.app("blog", appGoMod("blog", "../summercms"))
|
||||
h.resetLogs()
|
||||
return app
|
||||
}
|
||||
|
||||
// mustUpdate runs --update in dir and requires success.
|
||||
func (h *scriptHarness) mustUpdate(dir string) scriptResult {
|
||||
h.t.Helper()
|
||||
res := h.runIn(dir, "--update")
|
||||
if res.code != 0 {
|
||||
h.t.Fatalf("update exit %d\nstdout:\n%s\nstderr:\n%s", res.code, res.stdout, res.stderr)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// stubOpenssl puts an openssl stub on PATH that records its arguments and
|
||||
// prints output.
|
||||
func (h *scriptHarness) stubOpenssl(output string) {
|
||||
h.t.Helper()
|
||||
body := "#!" + h.bash + "\nprintf '%s\\n' \"$*\" >>\"$STUB_LOG/openssl.calls\"\nprintf '%s\\n' '" + output + "'\n"
|
||||
if err := os.WriteFile(filepath.Join(h.tools, "openssl"), []byte(body), 0o755); err != nil {
|
||||
h.t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// expectBuilt asserts summer build ran once, in app, with no stdin.
|
||||
func expectBuilt(t *testing.T, h *scriptHarness, app string) {
|
||||
t.Helper()
|
||||
if got, want := h.log("summer.call"), "cwd="+app+"\narg=build\n"; got != want {
|
||||
t.Fatalf("summer call = %q, want %q", got, want)
|
||||
}
|
||||
if got := h.log("summer.stdin"); got != "" {
|
||||
t.Fatalf("summer read stdin %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// expectNoInstallOrBuild asserts neither go install nor summer ran.
|
||||
func expectNoInstallOrBuild(t *testing.T, h *scriptHarness) {
|
||||
t.Helper()
|
||||
if c := h.log("go.install"); c != "" {
|
||||
t.Fatalf("go install ran:\n%s", c)
|
||||
}
|
||||
if c := h.log("summer.call"); c != "" {
|
||||
t.Fatalf("summer was called:\n%s", c)
|
||||
}
|
||||
}
|
||||
|
||||
// branch prints the branch checked out in the framework checkout.
|
||||
func (h *scriptHarness) branch() string {
|
||||
h.t.Helper()
|
||||
return h.remote.git(h.checkout(), "symbolic-ref", "--short", "HEAD")
|
||||
}
|
||||
|
||||
// writeEnv writes a .env file with exactly mode, whatever the umask.
|
||||
func writeEnv(t *testing.T, path, data string, mode os.FileMode) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(data), mode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chmod(path, mode); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// readFile returns the bytes of path as a string.
|
||||
func readFile(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// secretRe is the shape of a generated admin token secret.
|
||||
var secretRe = regexp.MustCompile(`^[A-Za-z0-9+/]{43}=$`)
|
||||
|
||||
const adminKey = "SUMMER_ADMIN__JWT__SECRET"
|
||||
|
||||
// expectAppended asserts the .env at path is before plus one generated
|
||||
// admin secret line, that the secret is well-formed and that neither output
|
||||
// stream holds it. It returns the secret. Failure messages name only the key
|
||||
// and the value length, never .env bytes.
|
||||
func expectAppended(t *testing.T, res scriptResult, path, before string) string {
|
||||
t.Helper()
|
||||
got := readFile(t, path)
|
||||
prefix := before + adminKey + "="
|
||||
if !strings.HasPrefix(got, prefix) || !strings.HasSuffix(got, "\n") {
|
||||
t.Fatalf(".env is not the original bytes plus one %s line (length %d, original %d)", adminKey, len(got), len(before))
|
||||
}
|
||||
v := strings.TrimSuffix(strings.TrimPrefix(got, prefix), "\n")
|
||||
if !secretRe.MatchString(v) {
|
||||
t.Fatalf("%s value of length %d is not 44-character base64", adminKey, len(v))
|
||||
}
|
||||
if strings.Contains(res.stdout, v) || strings.Contains(res.stderr, v) {
|
||||
t.Fatalf("the output contains the generated %s value", adminKey)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// expectMode asserts the permission bits of path.
|
||||
func expectMode(t *testing.T, path string, mode os.FileMode) {
|
||||
t.Helper()
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := fi.Mode().Perm(); got != mode {
|
||||
t.Fatalf("mode of %s = %v, want %v", path, got, mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScriptBehavior(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -710,8 +897,407 @@ func TestScriptBehavior(t *testing.T) {
|
||||
expectContains(t, "stderr", res.stderr, "installing summer into "+bin)
|
||||
expectContains(t, "stdout", res.stdout, `export PATH="`+bin+`:$PATH"`)
|
||||
}},
|
||||
{"update: help names the update forms", func(t *testing.T, h *scriptHarness) {
|
||||
res := h.run("--help")
|
||||
expectCode(t, res, 0)
|
||||
expectContains(t, "stdout", res.stdout, "Usage: curl -fsSL https://summercms.io/new | bash -s -- <name>", "SUMMERCMS_REF",
|
||||
"https://summercms.io/update | bash", "bash -s -- --update")
|
||||
// The rejected flag name, split so a grep of the sources finds none.
|
||||
expectLacks(t, "stdout", res.stdout, "--"+"upgrade")
|
||||
expectNothingStarted(t, h)
|
||||
}},
|
||||
{"update: --update with a name is a usage error", func(t *testing.T, h *scriptHarness) {
|
||||
for _, args := range [][]string{{"--update", "demo"}, {"demo", "--update"}} {
|
||||
res := h.run(args...)
|
||||
expectCode(t, res, 2)
|
||||
expectContains(t, "stderr", res.stderr, "--update takes no other argument", "Usage:")
|
||||
expectNothingStarted(t, h)
|
||||
}
|
||||
}},
|
||||
{"update: a repeated --update is accepted", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
res := h.runIn(app, "--update", "--update")
|
||||
expectCode(t, res, 0)
|
||||
expectBuilt(t, h, app)
|
||||
}},
|
||||
{"update: outside an application refuses", func(t *testing.T, h *scriptHarness) {
|
||||
res := h.run("--update")
|
||||
expectCode(t, res, 1)
|
||||
expectContains(t, "stderr", res.stderr, "run this from inside an sm-<name>-app directory")
|
||||
expectNothingStarted(t, h)
|
||||
}},
|
||||
{"update: an app without summer.yaml refuses", func(t *testing.T, h *scriptHarness) {
|
||||
app := filepath.Join(h.work, "sm-blog-app")
|
||||
if err := os.MkdirAll(app, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(app, "go.mod"), []byte(appGoMod("blog", "../summercms")), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res := h.runIn(app, "--update")
|
||||
expectCode(t, res, 1)
|
||||
expectContains(t, "stderr", res.stderr, "run this from inside an sm-<name>-app directory", "summer.yaml")
|
||||
if c := h.log("go.calls"); c != "" {
|
||||
t.Fatalf("go was called:\n%s", c)
|
||||
}
|
||||
}},
|
||||
{"update: go.mod without a framework replace refuses", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.app("blog", "module example.com/blog\n\nrequire "+fwModule+" v0.1.13\n\nreplace example.com/x => ../x\n")
|
||||
res := h.runIn(app, "--update")
|
||||
expectCode(t, res, 1)
|
||||
expectContains(t, "stderr", res.stderr, "has no replace for "+fwModule)
|
||||
if c := h.log("go.calls"); c != "" {
|
||||
t.Fatalf("go was called:\n%s", c)
|
||||
}
|
||||
}},
|
||||
{"update: a module-version replace refuses", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.app("blog", "module example.com/blog\n\n"+replaceLine(fwModule+" v0.1.13")+"\n")
|
||||
res := h.runIn(app, "--update")
|
||||
expectCode(t, res, 1)
|
||||
expectContains(t, "stderr", res.stderr, "not a local checkout", fwModule+" v0.1.13")
|
||||
if c := h.log("go.calls"); c != "" {
|
||||
t.Fatalf("go was called:\n%s", c)
|
||||
}
|
||||
}},
|
||||
{"update: a missing replace target refuses", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.app("blog", appGoMod("blog", "../nowhere"))
|
||||
res := h.runIn(app, "--update")
|
||||
expectCode(t, res, 1)
|
||||
expectContains(t, "stderr", res.stderr, filepath.Join(h.work, "nowhere")+", which does not exist")
|
||||
if c := h.log("go.calls"); c != "" {
|
||||
t.Fatalf("go was called:\n%s", c)
|
||||
}
|
||||
}},
|
||||
{"update: a replace target that is another module refuses", func(t *testing.T, h *scriptHarness) {
|
||||
other := filepath.Join(h.work, "other")
|
||||
if err := os.Mkdir(other, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(other, "go.mod"), []byte("module example.com/other\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app := h.app("blog", appGoMod("blog", "../other"))
|
||||
res := h.runIn(app, "--update")
|
||||
expectCode(t, res, 1)
|
||||
expectContains(t, "stderr", res.stderr, "is not a SummerCMS framework checkout", "example.com/other")
|
||||
expectNoInstallOrBuild(t, h)
|
||||
}},
|
||||
{"update: a detached checkout moves to the new release and the app is rebuilt", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
next := h.remote.tag("v0.11.0")
|
||||
res := h.mustUpdate(app)
|
||||
if got := h.head(); got != next {
|
||||
t.Fatalf("HEAD = %s, want v0.11.0 %s", got, next)
|
||||
}
|
||||
if err := exec.Command("git", "-C", h.checkout(), "symbolic-ref", "-q", "HEAD").Run(); err == nil {
|
||||
t.Fatal("checkout is on a branch, want detached")
|
||||
}
|
||||
expectContains(t, "stderr", res.stderr, "updating the framework checkout", "framework target: v0.11.0")
|
||||
if got, want := h.log("go.install"), "GOWORK=off dir="+h.checkout()+"\n"; got != want {
|
||||
t.Fatalf("go install = %q, want %q", got, want)
|
||||
}
|
||||
expectBuilt(t, h, app)
|
||||
expectHintSuffix(t, res.stdout, "\n==> Done. Run summer migrate, then restart summer serve.\n"+exportHint(h.gobin, "and add it to your shell profile"))
|
||||
}},
|
||||
{"update: at the newest release it still reinstalls and rebuilds", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
before := h.head()
|
||||
res := h.mustUpdate(app)
|
||||
if got := h.head(); got != before {
|
||||
t.Fatalf("HEAD moved from %s to %s", before, got)
|
||||
}
|
||||
expectContains(t, "stderr", res.stderr, "already at the target")
|
||||
if h.log("go.install") == "" {
|
||||
t.Fatal("go install did not run")
|
||||
}
|
||||
expectBuilt(t, h, app)
|
||||
}},
|
||||
{"update: SUMMERCMS_REF selects the tag of a detached checkout", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
want := h.remote.tag("v0.11.0")
|
||||
h.remote.tag("v0.12.0")
|
||||
h.env = append(h.env, "SUMMERCMS_REF=v0.11.0")
|
||||
h.mustUpdate(app)
|
||||
if got := h.head(); got != want {
|
||||
t.Fatalf("HEAD = %s, want v0.11.0 %s", got, want)
|
||||
}
|
||||
}},
|
||||
{"update: a block replace with an absolute path", func(t *testing.T, h *scriptHarness) {
|
||||
h.mustRun("demo")
|
||||
app := h.app("blog", "module example.com/blog\n\nreplace (\n\texample.com/x => ./x\n\t"+fwModule+" v0.0.0 => "+h.checkout()+" // local\n)\n")
|
||||
h.resetLogs()
|
||||
next := h.remote.tag("v0.11.0")
|
||||
h.mustUpdate(app)
|
||||
if got := h.head(); got != next {
|
||||
t.Fatalf("HEAD = %s, want v0.11.0 %s", got, next)
|
||||
}
|
||||
expectBuilt(t, h, app)
|
||||
}},
|
||||
{"update: a failed build exits 1 naming the app", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
h.env = append(h.env, "STUB_SUMMER_FAIL=1")
|
||||
res := h.runIn(app, "--update")
|
||||
expectCode(t, res, 1)
|
||||
expectContains(t, "stderr", res.stderr, "summer build failed in "+app)
|
||||
expectLacks(t, "stdout", res.stdout, "==> Done.")
|
||||
}},
|
||||
{"update: fish SHELL ends with fish_add_path", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
h.env = append(h.env, "SHELL=/usr/bin/fish")
|
||||
res := h.mustUpdate(app)
|
||||
expectHintSuffix(t, res.stdout, "\n==> Done. Run summer migrate, then restart summer serve.\n"+fishHint(h.gobin))
|
||||
}},
|
||||
{"update: GOBIN on PATH prints no hint", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
h.gobinOnPath = true
|
||||
res := h.mustUpdate(app)
|
||||
if res.stdout != "\n==> Done. Run summer migrate, then restart summer serve.\n" {
|
||||
t.Fatalf("stdout = %q", res.stdout)
|
||||
}
|
||||
}},
|
||||
{"update: create-mode stdout is unchanged", func(t *testing.T, h *scriptHarness) {
|
||||
res := h.mustRun("demo")
|
||||
want := "\n==> Done. Start with:\n cd " + filepath.Join(h.work, "sm-demo-app") + "\n" + exportHint(h.gobin, "and add it to your shell profile")
|
||||
if res.stdout != want {
|
||||
t.Fatalf("stdout = %q\nwant %q", res.stdout, want)
|
||||
}
|
||||
}},
|
||||
{"update: UpdateScript runs update mode with no arguments", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
res := h.runUpdate(app)
|
||||
expectCode(t, res, 0)
|
||||
expectBuilt(t, h, app)
|
||||
h.resetLogs()
|
||||
res = h.runUpdate(app, "demo")
|
||||
expectCode(t, res, 2)
|
||||
expectContains(t, "stderr", res.stderr, "--update takes no other argument")
|
||||
expectNoInstallOrBuild(t, h)
|
||||
}},
|
||||
{"update: a branch checkout is pulled fast-forward", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
h.remote.git(h.checkout(), "switch", "-q", "main")
|
||||
next := h.remote.commit()
|
||||
res := h.mustUpdate(app)
|
||||
if got := h.head(); got != next {
|
||||
t.Fatalf("HEAD = %s, want the new main %s", got, next)
|
||||
}
|
||||
if got := h.branch(); got != "main" {
|
||||
t.Fatalf("branch = %s", got)
|
||||
}
|
||||
expectContains(t, "stderr", res.stderr, "pulling main from origin/main (fast-forward only)", "framework checkout moved from")
|
||||
if h.log("go.install") == "" {
|
||||
t.Fatal("go install did not run")
|
||||
}
|
||||
expectBuilt(t, h, app)
|
||||
|
||||
h.resetLogs()
|
||||
res = h.mustUpdate(app)
|
||||
expectContains(t, "stderr", res.stderr, "framework checkout is already up to date")
|
||||
if got := h.head(); got != next {
|
||||
t.Fatalf("HEAD moved to %s", got)
|
||||
}
|
||||
}},
|
||||
{"update: a dirty branch refuses and is kept", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
h.remote.git(h.checkout(), "switch", "-q", "main")
|
||||
version := filepath.Join(h.checkout(), "VERSION")
|
||||
if err := os.WriteFile(version, []byte("local edit\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h.remote.commit()
|
||||
before := h.head()
|
||||
res := h.runIn(app, "--update")
|
||||
expectRefusedUnchanged(t, h, res, before, "uncommitted changes to tracked files")
|
||||
if got := h.branch(); got != "main" {
|
||||
t.Fatalf("branch = %s", got)
|
||||
}
|
||||
if got := readFile(t, version); got != "local edit\n" {
|
||||
t.Fatalf("VERSION = %q", got)
|
||||
}
|
||||
expectNoInstallOrBuild(t, h)
|
||||
}},
|
||||
{"update: a branch without an upstream refuses", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
h.remote.git(h.checkout(), "switch", "-q", "-c", "mine")
|
||||
before := h.head()
|
||||
res := h.runIn(app, "--update")
|
||||
expectRefusedUnchanged(t, h, res, before, "has no upstream")
|
||||
if got := h.branch(); got != "mine" {
|
||||
t.Fatalf("branch = %s", got)
|
||||
}
|
||||
expectNoInstallOrBuild(t, h)
|
||||
}},
|
||||
{"update: a diverged branch refuses", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
h.remote.git(h.checkout(), "switch", "-q", "main")
|
||||
h.remote.git(h.checkout(), "commit", "-q", "--allow-empty", "-m", "local work")
|
||||
h.remote.commit()
|
||||
before := h.head()
|
||||
// With an identity a plain pull could merge; only --ff-only refuses.
|
||||
h.env = append(h.env, "GIT_AUTHOR_NAME=U", "GIT_AUTHOR_EMAIL=u@example.com", "GIT_COMMITTER_NAME=U", "GIT_COMMITTER_EMAIL=u@example.com")
|
||||
res := h.runIn(app, "--update")
|
||||
expectRefusedUnchanged(t, h, res, before, "not a fast-forward")
|
||||
expectNoInstallOrBuild(t, h)
|
||||
}},
|
||||
{"update: SUMMERCMS_REF is ignored on a branch", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
h.remote.git(h.checkout(), "switch", "-q", "main")
|
||||
next := h.remote.commit()
|
||||
h.env = append(h.env, "SUMMERCMS_REF=v0.2.0")
|
||||
res := h.mustUpdate(app)
|
||||
expectContains(t, "stderr", res.stderr, "SUMMERCMS_REF is ignored for a branch checkout")
|
||||
if got := h.head(); got != next {
|
||||
t.Fatalf("HEAD = %s, want the new main %s (not v0.2.0)", got, next)
|
||||
}
|
||||
}},
|
||||
{"update: SUMMERCMS_DIR is ignored", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
h.env = append(h.env, "SUMMERCMS_DIR=elsewhere")
|
||||
res := h.mustUpdate(app)
|
||||
expectContains(t, "stderr", res.stderr, "SUMMERCMS_DIR is ignored by --update")
|
||||
if got, want := h.log("go.install"), "GOWORK=off dir="+h.checkout()+"\n"; got != want {
|
||||
t.Fatalf("go install = %q, want %q", got, want)
|
||||
}
|
||||
}},
|
||||
{"update: other apps on the same checkout are named, not rebuilt", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
alpha := h.app("alpha", appGoMod("alpha", "../summercms"))
|
||||
beta := h.app("beta", "module example.com/beta\n\nreplace (\n\t"+fwModule+" => "+h.checkout()+"\n)\n")
|
||||
other := h.app("other", appGoMod("other", "../elsewhere"))
|
||||
for _, d := range []string{alpha, beta, other} {
|
||||
writeEnv(t, filepath.Join(d, ".env"), "SUMMER_DATABASE__DSN=x\n", 0o600)
|
||||
}
|
||||
res := h.mustUpdate(app)
|
||||
var lines []string
|
||||
for _, l := range strings.Split(res.stderr, "\n") {
|
||||
if strings.Contains(l, "not rebuilt") {
|
||||
lines = append(lines, l)
|
||||
}
|
||||
}
|
||||
if len(lines) != 1 {
|
||||
t.Fatalf("want one not rebuilt line, got %d:\n%s", len(lines), res.stderr)
|
||||
}
|
||||
expectContains(t, "not rebuilt line", lines[0], alpha, beta)
|
||||
expectLacks(t, "not rebuilt line", lines[0], "sm-other-app", "sm-blog-app")
|
||||
expectBuilt(t, h, app)
|
||||
for _, d := range []string{alpha, beta} {
|
||||
if got := readFile(t, filepath.Join(d, ".env")); got != "SUMMER_DATABASE__DSN=x\n" {
|
||||
t.Fatalf("%s/.env changed (length %d)", d, len(got))
|
||||
}
|
||||
}
|
||||
}},
|
||||
{"update: an app elsewhere still names apps next to the checkout", func(t *testing.T, h *scriptHarness) {
|
||||
h.mustRun("demo")
|
||||
alpha := h.app("alpha", appGoMod("alpha", "../summercms"))
|
||||
far := h.appAt(filepath.Join(h.root, "apps", "sm-far-app"), appGoMod("far", h.checkout()))
|
||||
h.resetLogs()
|
||||
res := h.mustUpdate(far)
|
||||
expectContains(t, "stderr", res.stderr, "not rebuilt", alpha)
|
||||
expectBuilt(t, h, far)
|
||||
}},
|
||||
{"update: no other apps prints no not rebuilt line", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
res := h.mustUpdate(app)
|
||||
expectLacks(t, "stderr", res.stderr, "not rebuilt")
|
||||
}},
|
||||
{"update: a missing admin secret is appended with head and base64", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
env := filepath.Join(app, ".env")
|
||||
writeEnv(t, env, "SUMMER_DATABASE__DSN=x\n", 0o640)
|
||||
res := h.mustUpdate(app)
|
||||
expectAppended(t, res, env, "SUMMER_DATABASE__DSN=x\n")
|
||||
expectMode(t, env, 0o640)
|
||||
expectContains(t, "stderr", res.stderr, "added "+adminKey+" to "+env)
|
||||
expectBuilt(t, h, app)
|
||||
|
||||
after := readFile(t, env)
|
||||
h.resetLogs()
|
||||
res = h.mustUpdate(app)
|
||||
if readFile(t, env) != after {
|
||||
t.Fatal(".env changed on the second run")
|
||||
}
|
||||
expectLacks(t, "stderr", res.stderr, "added "+adminKey)
|
||||
}},
|
||||
{"update: a .env without a final newline gets one first", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
env := filepath.Join(app, ".env")
|
||||
writeEnv(t, env, "SUMMER_DATABASE__DSN=x", 0o600)
|
||||
res := h.mustUpdate(app)
|
||||
expectAppended(t, res, env, "SUMMER_DATABASE__DSN=x\n")
|
||||
}},
|
||||
{"update: an export-form admin secret counts as present", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
env := filepath.Join(app, ".env")
|
||||
data := " export " + adminKey + " = abc\n"
|
||||
writeEnv(t, env, data, 0o600)
|
||||
res := h.mustUpdate(app)
|
||||
if readFile(t, env) != data {
|
||||
t.Fatal(".env changed")
|
||||
}
|
||||
expectLacks(t, "stderr", res.stderr, "added "+adminKey, "is empty")
|
||||
}},
|
||||
{"update: a commented admin secret does not count", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
env := filepath.Join(app, ".env")
|
||||
data := "# " + adminKey + "=old\n"
|
||||
writeEnv(t, env, data, 0o600)
|
||||
res := h.mustUpdate(app)
|
||||
expectAppended(t, res, env, data)
|
||||
}},
|
||||
{"update: an empty admin secret is kept with a warning", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
env := filepath.Join(app, ".env")
|
||||
data := adminKey + "=\n"
|
||||
writeEnv(t, env, data, 0o600)
|
||||
res := h.mustUpdate(app)
|
||||
if readFile(t, env) != data {
|
||||
t.Fatal(".env changed")
|
||||
}
|
||||
expectContains(t, "stderr", res.stderr, adminKey+" in "+env+" is empty")
|
||||
expectBuilt(t, h, app)
|
||||
}},
|
||||
{"update: openssl is preferred for the admin secret", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
const value = "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWY="
|
||||
h.stubOpenssl(value)
|
||||
env := filepath.Join(app, ".env")
|
||||
writeEnv(t, env, "SUMMER_DATABASE__DSN=x\n", 0o600)
|
||||
res := h.mustUpdate(app)
|
||||
if got := h.log("openssl.calls"); got != "rand -base64 32\n" {
|
||||
t.Fatalf("openssl calls = %q", got)
|
||||
}
|
||||
if got := expectAppended(t, res, env, "SUMMER_DATABASE__DSN=x\n"); got != value {
|
||||
t.Fatal("the appended value is not the openssl output")
|
||||
}
|
||||
}},
|
||||
{"update: a failed secret generator exits 1 and keeps .env", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
h.stubOpenssl("short")
|
||||
h.without("head")
|
||||
env := filepath.Join(app, ".env")
|
||||
data := "SUMMER_DATABASE__DSN=x\n"
|
||||
writeEnv(t, env, data, 0o600)
|
||||
res := h.runIn(app, "--update")
|
||||
expectCode(t, res, 1)
|
||||
if readFile(t, env) != data {
|
||||
t.Fatal(".env changed")
|
||||
}
|
||||
expectContains(t, "stderr", res.stderr, "add a "+adminKey+" line")
|
||||
if c := h.log("summer.call"); c != "" {
|
||||
t.Fatalf("summer was called:\n%s", c)
|
||||
}
|
||||
}},
|
||||
{"update: no .env is skipped", func(t *testing.T, h *scriptHarness) {
|
||||
app := h.updateReady()
|
||||
res := h.mustUpdate(app)
|
||||
if _, err := os.Lstat(filepath.Join(app, ".env")); !os.IsNotExist(err) {
|
||||
t.Fatalf(".env was created: %v", err)
|
||||
}
|
||||
expectContains(t, "stderr", res.stderr, "no .env in "+app)
|
||||
expectBuilt(t, h, app)
|
||||
}},
|
||||
}
|
||||
if len(cases) < 32 {
|
||||
if len(cases) < 67 {
|
||||
t.Fatalf("only %d cases", len(cases))
|
||||
}
|
||||
for _, tc := range cases {
|
||||
|
||||
Reference in New Issue
Block a user