package build import ( "bytes" "encoding/base64" "flag" "os" "path/filepath" "strings" "testing" ) var update = flag.Bool("update", false, "rewrite golden files") // goldenAppKey is the base64 of the bytes 0x00..0x1f: a fixed, non-secret // key that keeps .env byte-stable in the goldens. var goldenAppKey = func() string { raw := make([]byte, 32) for i := range raw { raw[i] = byte(i) } return base64.StdEncoding.EncodeToString(raw) }() // goldenAdminSecret is the base64 of the bytes 0x20..0x3f: a fixed, // non-secret admin token secret that keeps .env byte-stable in the goldens. var goldenAdminSecret = func() string { raw := make([]byte, 32) for i := range raw { raw[i] = byte(0x20 + i) } return base64.StdEncoding.EncodeToString(raw) }() // newGoldenApp runs NewApp for "demo" in a temp parent whose sibling // summercms is a symlink to this repository, so auto-detection yields the // relative replace ../summercms. It returns the app dir and the output with // the parent path replaced by . func newGoldenApp(t *testing.T, noPlugin bool) (string, []byte) { t.Helper() repo, err := filepath.Abs(filepath.Join("..", "..")) if err != nil { t.Fatal(err) } parent := t.TempDir() if err := os.Symlink(repo, filepath.Join(parent, "summercms")); err != nil { t.Fatal(err) } var out bytes.Buffer dir, err := NewApp(t.Context(), NewOptions{ Name: "demo", Parent: parent, AppKey: goldenAppKey, AdminSecret: goldenAdminSecret, NoGit: true, NoPlugin: noPlugin, }, &out) if err != nil { t.Fatalf("NewApp: %v\n%s", err, out.String()) } return dir, bytes.ReplaceAll(out.Bytes(), []byte(parent), []byte("")) } // goldenName maps an app-relative path to its golden file name: "/" becomes // "__" and a leading "." becomes "dot-", which keeps .env goldens clear of // the framework's .env ignore rules. func goldenName(rel string) string { name := strings.ReplaceAll(rel, "/", "__") if strings.HasPrefix(name, ".") { name = "dot-" + name[1:] } return name + ".golden" } // assertGolden compares got with testdata//, rewriting the file // first when -update is set. func assertGolden(t *testing.T, dir, rel string, got []byte) { t.Helper() path := filepath.Join("testdata", dir, goldenName(rel)) if *update { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, got, 0o644); err != nil { t.Fatal(err) } } want, err := os.ReadFile(path) if err != nil { t.Fatalf("%s: %v (run go test -run %s -update to create it)", rel, err, t.Name()) } if !bytes.Equal(got, want) { t.Errorf("%s differs from %s\n%s", rel, path, firstDiff(want, got)) } } // firstDiff prints the first differing lines of want and got with a little // context, in a unified-diff-like form. func firstDiff(want, got []byte) string { wl := strings.Split(string(want), "\n") gl := strings.Split(string(got), "\n") i := 0 for i < len(wl) && i < len(gl) && wl[i] == gl[i] { i++ } var b strings.Builder start := max(i-2, 0) for j := start; j < i; j++ { b.WriteString(" " + wl[j] + "\n") } for j := i; j < len(wl) && j < i+3; j++ { b.WriteString("- " + wl[j] + "\n") } for j := i; j < len(gl) && j < i+3; j++ { b.WriteString("+ " + gl[j] + "\n") } return b.String() } func readAppFile(t *testing.T, dir, rel string) []byte { t.Helper() data, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel))) if err != nil { t.Fatal(err) } return data } // TestNewGolden pins every file summer new writes for the default app. // AGENTS.md (a symlink) and the starter plugin's go.mod/go.sum (dependency // versions drift) are asserted instead of pinned. func TestNewGolden(t *testing.T) { dir, out := newGoldenApp(t, false) for _, rel := range []string{ "go.mod", "go.work", "summer.yaml", "main.go", "plugins.gen.go", "config/app.yaml", "config/database.yaml", "config/http.yaml", "config/storage.yaml", ".env", ".env.example", ".gitignore", "compose.yaml", "CLAUDE.md", "plugins/demo/plugin.go", "plugins/demo/routes.go", } { assertGolden(t, "new", rel, readAppFile(t, dir, rel)) } assertGolden(t, "new", "next-steps.txt", out) pluginMod := readAppFile(t, dir, "plugins/demo/go.mod") if !bytes.Contains(pluginMod, []byte("replace git.golem15.com/golem15/summercms => ../../../summercms")) { t.Errorf("plugins/demo/go.mod lacks the relative framework replace:\n%s", pluginMod) } if _, err := os.Stat(filepath.Join(dir, "go.sum")); !os.IsNotExist(err) { t.Errorf("app root go.sum exists or stat failed: %v", err) } } // TestNewGoldenNoPlugin pins the files that differ for summer new --no-plugin. func TestNewGoldenNoPlugin(t *testing.T) { dir, out := newGoldenApp(t, true) for _, rel := range []string{"go.mod", "go.work", "summer.yaml", "plugins.gen.go", "CLAUDE.md"} { assertGolden(t, "new-no-plugin", rel, readAppFile(t, dir, rel)) } assertGolden(t, "new-no-plugin", "next-steps.txt", out) if _, err := os.Stat(filepath.Join(dir, "plugins")); !os.IsNotExist(err) { t.Errorf("--no-plugin created plugins/ or stat failed: %v", err) } }