- NewOptions.AdminSecret: summer new writes SUMMER_ADMIN__JWT__SECRET to .env (still 0600), generated from 32 crypto/rand bytes when empty and validated as at least 32 .env-safe characters - .env.example gets an empty SUMMER_ADMIN__JWT__SECRET= placeholder with a comment; the secret never reaches the printed next steps - The next steps and the generated CLAUDE.md explain the admin at /backend: make:admin-controller, build, migrate and summer admin:create --email you@example.com --superuser - Goldens use a fixed, non-secret admin secret fixture - Scaffolding, installation and users-and-permissions docs updated
164 lines
5.0 KiB
Go
164 lines
5.0 KiB
Go
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 <parent>.
|
|
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("<parent>"))
|
|
}
|
|
|
|
// 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/<dir>/<name>, 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)
|
|
}
|
|
}
|