package build import ( "bytes" "encoding/base64" "go/token" "io/fs" "os" "os/exec" "path/filepath" "strings" "testing" ) // testAppKey is a fixed, valid app key (base64 of 32 bytes) for hermetic tests. const testAppKey = "c3VtbWVyY21zLXRlc3Qta2V5LTMyLWJ5dGVzLWxvbmc=" // repoRoot is the absolute path of this framework checkout. func repoRoot(t *testing.T) string { t.Helper() repo, err := filepath.Abs(filepath.Join("..", "..")) if err != nil { t.Fatal(err) } return repo } // parentWithFramework returns a temp parent holding a summercms symlink to // this checkout, so NewApp finds the framework as a sibling. func parentWithFramework(t *testing.T) string { t.Helper() parent := t.TempDir() if err := os.Symlink(repoRoot(t), filepath.Join(parent, "summercms")); err != nil { t.Fatal(err) } return parent } // fakeFramework writes dir/go.mod declaring module and returns dir. func fakeFramework(t *testing.T, dir, module string) string { t.Helper() if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } write(t, filepath.Join(dir, "go.mod"), "module "+module+"\n\ngo 1.27.0\n") return dir } // stubBinaryFramework replaces frameworkFromBinary for the test. func stubBinaryFramework(t *testing.T, dir string) { t.Helper() orig := frameworkFromBinary frameworkFromBinary = func() string { return dir } t.Cleanup(func() { frameworkFromBinary = orig }) } // gitOut runs git in dir and returns its trimmed output. func gitOut(t *testing.T, dir string, args ...string) string { t.Helper() cmd := exec.CommandContext(t.Context(), "git", args...) cmd.Dir = dir out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } return strings.TrimSpace(string(out)) } // requireGit skips when git is not installed. func requireGit(t *testing.T) { t.Helper() if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } } // isolateGit points git at an empty configuration (no system or global // file, temp HOME) and clears every identity variable, while keeping the Go // caches where they are so the starter plugin's go mod tidy stays offline. func isolateGit(t *testing.T) { t.Helper() requireGit(t) envOut, err := exec.CommandContext(t.Context(), "go", "env", "GOMODCACHE", "GOCACHE", "GOPATH", "GOENV").Output() if err != nil { t.Fatalf("go env: %v", err) } vals := strings.Split(strings.TrimSpace(string(envOut)), "\n") if len(vals) != 4 { t.Fatalf("go env output %q", envOut) } for i, k := range []string{"GOMODCACHE", "GOCACHE", "GOPATH", "GOENV"} { t.Setenv(k, vals[i]) } home := t.TempDir() t.Setenv("HOME", home) t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config")) t.Setenv("GIT_CONFIG_NOSYSTEM", "1") t.Setenv("GIT_CONFIG_GLOBAL", os.DevNull) // Recent git runs auto maintenance detached after a commit; it would // write into .git while the temp dir is being removed. t.Setenv("GIT_CONFIG_COUNT", "2") t.Setenv("GIT_CONFIG_KEY_0", "maintenance.auto") t.Setenv("GIT_CONFIG_VALUE_0", "false") t.Setenv("GIT_CONFIG_KEY_1", "gc.auto") t.Setenv("GIT_CONFIG_VALUE_1", "0") for _, k := range []string{"GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL", "EMAIL", "GIT_DIR", "GIT_WORK_TREE"} { t.Setenv(k, "") os.Unsetenv(k) } } // TestNewAppSmoke writes an app next to a symlinked framework checkout, // builds it and lists the starter route. func TestNewAppSmoke(t *testing.T) { if testing.Short() { t.Skip("runs go mod tidy and go build") } parent := t.TempDir() fw := filepath.Join(parent, "summercms") if err := os.Symlink(repoRoot(t), fw); err != nil { t.Fatal(err) } ctx := t.Context() var log bytes.Buffer dir, err := NewApp(ctx, NewOptions{Name: "demo", Parent: parent, Framework: fw, AppKey: testAppKey, NoGit: true}, &log) if err != nil { t.Fatalf("NewApp: %v\n%s", err, log.String()) } if want := filepath.Join(parent, "sm-demo-app"); dir != want { t.Fatalf("dir = %s, want %s", dir, want) } mod, err := os.ReadFile(filepath.Join(dir, "go.mod")) if err != nil { t.Fatal(err) } if !bytes.Contains(mod, []byte("replace git.golem15.com/golem15/summercms => ../summercms")) { t.Fatalf("go.mod lacks the relative framework replace:\n%s", mod) } work, err := os.ReadFile(filepath.Join(dir, "go.work")) if err != nil { t.Fatal(err) } if !bytes.Contains(work, []byte("./plugins/demo")) { t.Fatalf("go.work lacks ./plugins/demo:\n%s", work) } for _, line := range strings.Split(string(work), "\n") { if strings.HasPrefix(strings.TrimSpace(line), "/") { t.Fatalf("go.work has an absolute path %q:\n%s", line, work) } } var buildOut bytes.Buffer if err := App(ctx, dir, &buildOut); err != nil { t.Fatalf("build: %v\n%s", err, buildOut.String()) } cmd := exec.CommandContext(ctx, filepath.Join(dir, "bin", "demo"), "route:list") cmd.Dir = dir listed, err := cmd.CombinedOutput() if err != nil { t.Fatalf("route:list: %v\n%s", err, listed) } for _, want := range []string{"/{$}", "golem15.demo"} { if !bytes.Contains(listed, []byte(want)) { t.Fatalf("route:list lacks %q:\n%s", want, listed) } } } // TestNewNoPluginBuilds builds an app created with --no-plugin: an empty // plugin list must compile. func TestNewNoPluginBuilds(t *testing.T) { if testing.Short() { t.Skip("runs go build") } parent := parentWithFramework(t) ctx := t.Context() dir, err := NewApp(ctx, NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true, NoPlugin: true}, nil) if err != nil { t.Fatal(err) } gen := readAppFile(t, dir, "plugins.gen.go") if !bytes.Contains(gen, []byte("var PluginIDs = []string{}")) { t.Fatalf("plugins.gen.go has plugins:\n%s", gen) } var out bytes.Buffer if err := App(ctx, dir, &out); err != nil { t.Fatalf("build: %v\n%s", err, out.String()) } if _, err := os.Stat(filepath.Join(dir, "bin", "demo")); err != nil { t.Fatalf("binary missing: %v", err) } } func TestNewValidateAppName(t *testing.T) { var keywords []string for tok := token.Token(0); tok < 256; tok++ { if tok.IsKeyword() { keywords = append(keywords, tok.String()) } } if len(keywords) != 25 { t.Fatalf("found %d Go keywords, want 25: %v", len(keywords), keywords) } for _, name := range []string{"", "Demo", "1x", "a-b", "a.b", "a/b", "a_b", "demo app", "main", "internal"} { if err := ValidateAppName(name); err == nil { t.Errorf("ValidateAppName(%q) = nil, want error", name) } } for _, kw := range keywords { err := ValidateAppName(kw) if err == nil || !strings.Contains(err.Error(), "keyword") { t.Errorf("ValidateAppName(%q) = %v, want a keyword error", kw, err) } } for _, name := range []string{"main", "internal"} { if err := ValidateAppName(name); err == nil || !strings.Contains(err.Error(), "reserved") { t.Errorf("ValidateAppName(%q) = %v, want a reserved error", name, err) } } for _, name := range []string{"init", "string", "vendor", "testdata", "a1", "demo"} { if err := ValidateAppName(name); err != nil { t.Errorf("ValidateAppName(%q) = %v, want nil", name, err) } } // NewApp applies the same rule before touching the filesystem. parent := t.TempDir() if _, err := NewApp(t.Context(), NewOptions{Name: "func", Parent: parent, AppKey: testAppKey, NoGit: true}, nil); err == nil { t.Fatal("NewApp accepted a keyword name") } assertDirEntries(t, parent, nil) } // assertDirEntries requires dir to hold exactly want (names, sorted). func assertDirEntries(t *testing.T, dir string, want []string) { t.Helper() entries, err := os.ReadDir(dir) if err != nil { t.Fatal(err) } var got []string for _, e := range entries { got = append(got, e.Name()) } if strings.Join(got, ",") != strings.Join(want, ",") { t.Fatalf("%s holds %v, want %v", dir, got, want) } } func TestNewRefusesExistingTarget(t *testing.T) { cases := []struct { name string setup func(t *testing.T, target string) check func(t *testing.T, target string) }{ { name: "directory", setup: func(t *testing.T, target string) { if err := os.Mkdir(target, 0o755); err != nil { t.Fatal(err) } write(t, filepath.Join(target, "keep.txt"), "mine\n") }, check: func(t *testing.T, target string) { assertDirEntries(t, target, []string{"keep.txt"}) if got := string(readAppFile(t, target, "keep.txt")); got != "mine\n" { t.Fatalf("keep.txt = %q", got) } }, }, { name: "file", setup: func(t *testing.T, target string) { write(t, target, "mine\n") }, check: func(t *testing.T, target string) { data, err := os.ReadFile(target) if err != nil || string(data) != "mine\n" { t.Fatalf("file = %q, %v", data, err) } }, }, { name: "symlink to a directory", setup: func(t *testing.T, target string) { dest := filepath.Join(filepath.Dir(target), "elsewhere") if err := os.Mkdir(dest, 0o755); err != nil { t.Fatal(err) } if err := os.Symlink("elsewhere", target); err != nil { t.Fatal(err) } }, check: func(t *testing.T, target string) { if link, err := os.Readlink(target); err != nil || link != "elsewhere" { t.Fatalf("symlink = %q, %v", link, err) } assertDirEntries(t, filepath.Join(filepath.Dir(target), "elsewhere"), nil) }, }, { name: "dangling symlink", setup: func(t *testing.T, target string) { _ = os.Symlink("missing", target) }, check: func(t *testing.T, target string) { if link, err := os.Readlink(target); err != nil || link != "missing" { t.Fatalf("symlink = %q, %v", link, err) } if _, err := os.Stat(filepath.Join(filepath.Dir(target), "missing")); !os.IsNotExist(err) { t.Fatalf("dangling symlink target was created: %v", err) } }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { parent := parentWithFramework(t) target := filepath.Join(parent, "sm-demo-app") tc.setup(t, target) _, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true}, nil) if err == nil || !strings.Contains(err.Error(), "already exists") { t.Fatalf("NewApp error = %v, want already exists", err) } tc.check(t, target) }) } } func TestNewFrameworkResolution(t *testing.T) { const fwModule = "git.golem15.com/golem15/summercms" t.Run("flag wins over siblings", func(t *testing.T) { stubBinaryFramework(t, "") parent := t.TempDir() fakeFramework(t, filepath.Join(parent, "summercms"), fwModule) flagDir := fakeFramework(t, filepath.Join(t.TempDir(), "fw"), fwModule) got, err := resolveFramework(parent, flagDir) if err != nil || got != flagDir { t.Fatalf("resolveFramework = %q, %v; want %q", got, err, flagDir) } }) t.Run("relative flag resolves against the parent", func(t *testing.T) { stubBinaryFramework(t, "") parent := t.TempDir() want := fakeFramework(t, filepath.Join(parent, "vendor", "fw"), fwModule) got, err := resolveFramework(parent, "vendor/fw") if err != nil || got != want { t.Fatalf("resolveFramework = %q, %v; want %q", got, err, want) } }) t.Run("wrong module in the flag is an error, not a fall-through", func(t *testing.T) { stubBinaryFramework(t, repoRoot(t)) parent := t.TempDir() fakeFramework(t, filepath.Join(parent, "summercms"), fwModule) flagDir := fakeFramework(t, filepath.Join(t.TempDir(), "other"), "example.com/other") got, err := resolveFramework(parent, flagDir) if err == nil || !strings.Contains(err.Error(), "--framework") || !strings.Contains(err.Error(), "example.com/other") { t.Fatalf("resolveFramework = %q, %v; want a --framework module error", got, err) } missing := filepath.Join(t.TempDir(), "nothing") if got, err := resolveFramework(parent, missing); err == nil { t.Fatalf("resolveFramework(missing flag) = %q, want error", got) } }) t.Run("summercms before summercms.go", func(t *testing.T) { stubBinaryFramework(t, "") parent := t.TempDir() want := fakeFramework(t, filepath.Join(parent, "summercms"), fwModule) fakeFramework(t, filepath.Join(parent, "summercms.go"), fwModule) got, err := resolveFramework(parent, "") if err != nil || got != want { t.Fatalf("resolveFramework = %q, %v; want %q", got, err, want) } }) t.Run("sibling with another module is skipped", func(t *testing.T) { stubBinaryFramework(t, "") parent := t.TempDir() fakeFramework(t, filepath.Join(parent, "summercms"), "example.com/notframework") want := fakeFramework(t, filepath.Join(parent, "summercms.go"), fwModule) got, err := resolveFramework(parent, "") if err != nil || got != want { t.Fatalf("resolveFramework = %q, %v; want %q", got, err, want) } }) t.Run("binary source is used last", func(t *testing.T) { bin := fakeFramework(t, filepath.Join(t.TempDir(), "src"), fwModule) stubBinaryFramework(t, bin) parent := t.TempDir() got, err := resolveFramework(parent, "") if err != nil || got != bin { t.Fatalf("resolveFramework = %q, %v; want %q", got, err, bin) } // A sibling still wins over the binary source. sib := fakeFramework(t, filepath.Join(parent, "summercms.go"), fwModule) if got, err := resolveFramework(parent, ""); err != nil || got != sib { t.Fatalf("resolveFramework = %q, %v; want sibling %q", got, err, sib) } }) t.Run("binary source with another module is ignored", func(t *testing.T) { stubBinaryFramework(t, fakeFramework(t, filepath.Join(t.TempDir(), "src"), "example.com/other")) if got, err := resolveFramework(t.TempDir(), ""); err == nil { t.Fatalf("resolveFramework = %q, want error", got) } }) t.Run("nothing found names --framework and writes nothing", func(t *testing.T) { stubBinaryFramework(t, "") parent := t.TempDir() _, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true}, nil) if err == nil || !strings.Contains(err.Error(), "--framework") { t.Fatalf("NewApp error = %v, want one naming --framework", err) } assertDirEntries(t, parent, nil) }) } func TestNewRefusesTargetInsideFramework(t *testing.T) { stubBinaryFramework(t, "") fw := fakeFramework(t, filepath.Join(t.TempDir(), "summercms"), frameworkModule) for _, tc := range []struct { name, parent, flag string }{ {"framework root", fw, "."}, {"subdirectory", filepath.Join(fw, "apps"), ".."}, } { t.Run(tc.name, func(t *testing.T) { if err := os.MkdirAll(tc.parent, 0o755); err != nil { t.Fatal(err) } before, err := os.ReadDir(tc.parent) if err != nil { t.Fatal(err) } _, err = NewApp(t.Context(), NewOptions{Name: "demo", Parent: tc.parent, Framework: tc.flag, AppKey: testAppKey, NoGit: true}, nil) if err == nil || !strings.Contains(err.Error(), "inside the framework checkout") { t.Fatalf("NewApp error = %v, want inside the framework checkout", err) } after, err := os.ReadDir(tc.parent) if err != nil { t.Fatal(err) } if len(after) != len(before) { t.Fatalf("%s changed: %d entries before, %d after", tc.parent, len(before), len(after)) } }) } } func TestNewCleansUpOnFailure(t *testing.T) { stubBinaryFramework(t, "") // The framework go.mod is valid but the checkout has no packages, so the // starter plugin's go mod tidy fails after the target was created. t.Setenv("GOPROXY", "off") parent := t.TempDir() fw := fakeFramework(t, filepath.Join(parent, "summercms"), frameworkModule) write(t, filepath.Join(parent, "notes.txt"), "keep me\n") var out bytes.Buffer _, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, Framework: fw, AppKey: testAppKey, NoGit: true}, &out) if err == nil { t.Fatalf("NewApp succeeded against an empty framework\n%s", out.String()) } if !strings.Contains(err.Error(), "removed it") { t.Fatalf("error %v does not say the target was removed", err) } if _, err := os.Lstat(filepath.Join(parent, "sm-demo-app")); !os.IsNotExist(err) { t.Fatalf("sm-demo-app remains after a failed run: %v", err) } assertDirEntries(t, parent, []string{"notes.txt", "summercms"}) if got := string(readAppFile(t, parent, "notes.txt")); got != "keep me\n" { t.Fatalf("notes.txt = %q", got) } assertDirEntries(t, fw, []string{"go.mod"}) } func TestNewRequiresValidAppKey(t *testing.T) { stubBinaryFramework(t, "") // No framework is reachable: a key error must come first. parent := t.TempDir() for _, key := range []string{"", "AAAA", "c2hvcnQ=", "not base64 at all!", testAppKey[:43]} { _, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: key, NoGit: true}, nil) if err == nil || !strings.Contains(err.Error(), "application key") { t.Errorf("key %q: error = %v, want an application key error", key, err) } } assertDirEntries(t, parent, nil) } func TestNewEnvFileMode(t *testing.T) { parent := parentWithFramework(t) dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true, NoPlugin: true}, nil) if err != nil { t.Fatal(err) } for rel, want := range map[string]fs.FileMode{".env": 0o600, ".env.example": 0o644, "compose.yaml": 0o644} { st, err := os.Stat(filepath.Join(dir, rel)) if err != nil { t.Fatal(err) } if got := st.Mode().Perm(); got != want { t.Errorf("%s mode = %o, want %o", rel, got, want) } } env := string(readAppFile(t, dir, ".env")) if !strings.Contains(env, "SUMMER_APP__KEY="+testAppKey+"\n") { t.Fatalf(".env lacks the key:\n%s", env) } } func TestNewAgentsSymlink(t *testing.T) { parent := parentWithFramework(t) dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true, NoPlugin: true}, nil) if err != nil { t.Fatal(err) } link, err := os.Readlink(filepath.Join(dir, "AGENTS.md")) if err != nil || link != "CLAUDE.md" { t.Fatalf("AGENTS.md -> %q, %v; want CLAUDE.md", link, err) } if !bytes.Equal(readAppFile(t, dir, "AGENTS.md"), readAppFile(t, dir, "CLAUDE.md")) { t.Fatal("AGENTS.md does not resolve to CLAUDE.md") } } func TestNewGitFirstCommit(t *testing.T) { isolateGit(t) parent := parentWithFramework(t) var out bytes.Buffer dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey}, &out) if err != nil { t.Fatalf("NewApp: %v\n%s", err, out.String()) } if got := gitOut(t, dir, "rev-list", "--count", "HEAD"); got != "1" { t.Fatalf("commit count = %s, want 1", got) } if got := gitOut(t, dir, "symbolic-ref", "--short", "HEAD"); got != "main" { t.Fatalf("branch = %s, want main", got) } if got := gitOut(t, dir, "log", "-1", "--format=%an <%ae>|%cn <%ce>"); got != "SummerCMS |SummerCMS " { t.Fatalf("identity = %s", got) } if !strings.Contains(out.String(), "warning: git user.name and user.email not configured") { t.Fatalf("no fallback identity warning:\n%s", out.String()) } // The fallback is passed per command, never written to the repository. for _, key := range []string{"user.name", "user.email"} { cmd := exec.CommandContext(t.Context(), "git", "config", key) cmd.Dir = dir if res, err := cmd.CombinedOutput(); err == nil { t.Fatalf("git config %s is set after summer new: %s", key, res) } } tracked := strings.Split(gitOut(t, dir, "ls-files"), "\n") has := func(name string) bool { for _, f := range tracked { if f == name { return true } } return false } for _, want := range []string{".env.example", ".gitignore", "go.work", "main.go", "plugins.gen.go", "AGENTS.md", "CLAUDE.md", "summer.yaml", "plugins/demo/plugin.go"} { if !has(want) { t.Errorf("%s is not tracked: %v", want, tracked) } } if has(".env") { t.Errorf(".env is tracked: %v", tracked) } if got := gitOut(t, dir, "status", "--porcelain"); got != "" { t.Errorf("working tree not clean after the first commit:\n%s", got) } if got := gitOut(t, dir, "log", "-1", "--format=%B"); strings.Contains(got, "Co-Authored-By") || got != "Create sm-demo-app with summer new" { t.Errorf("commit message = %q", got) } } func TestNewGitUsesConfiguredIdentity(t *testing.T) { isolateGit(t) t.Setenv("GIT_AUTHOR_NAME", "Ada Lovelace") t.Setenv("GIT_AUTHOR_EMAIL", "ada@example.com") t.Setenv("GIT_COMMITTER_NAME", "Ada Lovelace") t.Setenv("GIT_COMMITTER_EMAIL", "ada@example.com") parent := parentWithFramework(t) var out bytes.Buffer dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoPlugin: true}, &out) if err != nil { t.Fatalf("NewApp: %v\n%s", err, out.String()) } if got := gitOut(t, dir, "log", "-1", "--format=%an <%ae>|%cn <%ce>"); got != "Ada Lovelace |Ada Lovelace " { t.Fatalf("identity = %s", got) } if strings.Contains(out.String(), "warning") { t.Fatalf("unexpected warning with a configured identity:\n%s", out.String()) } } func TestNewGitMissingIsWarning(t *testing.T) { goBin, err := exec.LookPath("go") if err != nil { t.Fatal(err) } goBin, err = filepath.Abs(goBin) if err != nil { t.Fatal(err) } tools := t.TempDir() if err := os.Symlink(goBin, filepath.Join(tools, "go")); err != nil { t.Fatal(err) } t.Setenv("PATH", tools) if _, err := exec.LookPath("git"); err == nil { t.Fatal("git still on PATH") } parent := parentWithFramework(t) var out bytes.Buffer dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey}, &out) if err != nil { t.Fatalf("NewApp without git: %v\n%s", err, out.String()) } if !strings.Contains(out.String(), "warning: git not found on PATH") { t.Fatalf("no git warning:\n%s", out.String()) } if _, err := os.Stat(filepath.Join(dir, "summer.yaml")); err != nil { t.Fatalf("app incomplete: %v", err) } if _, err := os.Lstat(filepath.Join(dir, ".git")); !os.IsNotExist(err) { t.Fatalf(".git exists without git: %v", err) } } func TestNewNoGit(t *testing.T) { requireGit(t) parent := parentWithFramework(t) var out bytes.Buffer dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true, NoPlugin: true}, &out) if err != nil { t.Fatal(err) } if _, err := os.Lstat(filepath.Join(dir, ".git")); !os.IsNotExist(err) { t.Fatalf("--no-git created .git: %v", err) } if strings.Contains(out.String(), "warning") { t.Fatalf("--no-git printed a warning:\n%s", out.String()) } if !strings.Contains(out.String(), "created "+dir) { t.Fatalf("output lacks the created line:\n%s", out.String()) } } // envValue returns the value of key in an env file body, or "" and false. func envValue(body []byte, key string) (string, bool) { for _, line := range strings.Split(string(body), "\n") { if v, ok := strings.CutPrefix(line, key+"="); ok { return v, true } } return "", false } // TestNewAdminSecret writes SUMMER_ADMIN__JWT__SECRET to .env only, generates // a random one when none is given and rejects values unsafe for .env. func TestNewAdminSecret(t *testing.T) { const key = "SUMMER_ADMIN__JWT__SECRET" t.Run("explicit", func(t *testing.T) { const secret = "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWY=" parent := parentWithFramework(t) var out bytes.Buffer dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, AdminSecret: secret, NoGit: true, NoPlugin: true}, &out) if err != nil { t.Fatal(err) } env := readAppFile(t, dir, ".env") if !strings.Contains(string(env), "\n"+key+"="+secret+"\n") { t.Fatalf(".env does not hold %s=%s:\n%s", key, secret, env) } st, err := os.Stat(filepath.Join(dir, ".env")) if err != nil { t.Fatal(err) } if st.Mode().Perm() != 0o600 { t.Fatalf(".env mode = %o, want 600", st.Mode().Perm()) } example := readAppFile(t, dir, ".env.example") if v, ok := envValue(example, key); !ok || v != "" { t.Fatalf(".env.example %s = (%q, %v), want an empty placeholder", key, v, ok) } if strings.Contains(string(example), secret) { t.Fatal(".env.example contains the admin secret") } if strings.Contains(out.String(), secret) { t.Fatal("the next steps print the admin secret") } }) t.Run("generated", func(t *testing.T) { var seen []string for range 2 { parent := parentWithFramework(t) dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true, NoPlugin: true}, nil) if err != nil { t.Fatal(err) } v, ok := envValue(readAppFile(t, dir, ".env"), key) if !ok { t.Fatalf(".env has no %s", key) } raw, err := base64.StdEncoding.DecodeString(v) if err != nil || len(raw) != 32 { t.Fatalf("generated secret %q is not the base64 of 32 bytes (%v)", v, err) } seen = append(seen, v) } if seen[0] == seen[1] { t.Fatal("two apps got the same admin secret") } }) t.Run("invalid", func(t *testing.T) { for _, secret := range []string{ "short", "abcdefghijklmnopqrst uvwxyzABCDEFGHIJKLMN", "abcdefghijklmnopqrst\nuvwxyzABCDEFGHIJKLMN", } { parent := parentWithFramework(t) _, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, AdminSecret: secret, NoGit: true, NoPlugin: true}, nil) if err == nil || !strings.Contains(err.Error(), "admin secret") { t.Fatalf("NewApp(AdminSecret %q) = %v, want an admin secret error", secret, err) } assertDirEntries(t, parent, []string{"summercms"}) } }) }