diff --git a/cmd/summer/main_test.go b/cmd/summer/main_test.go index 1d95625..53b1411 100644 --- a/cmd/summer/main_test.go +++ b/cmd/summer/main_test.go @@ -25,17 +25,41 @@ func TestToolCommandNames(t *testing.T) { } } + helpWants := map[string][]string{ + "make:plugin": {"vendor.plugin"}, + "make:model": {"[plugin] [name]", "no-migration"}, + "make:migration": {"[plugin] [name]"}, + "make:command": {"[plugin] [name]"}, + "make:job": {"[plugin] [name]"}, + "make:admin-controller": {"[plugin] [name]"}, + } + for cmd, wants := range helpWants { + var buf bytes.Buffer + root, err := bonfire.NewRoot("summer", toolCommands(), &buf) + if err != nil { + t.Fatal(err) + } + root.SetArgs([]string{cmd, "--help"}) + if err := root.Execute(); err != nil { + t.Fatalf("%s --help: %v", cmd, err) + } + out := buf.String() + for _, want := range wants { + if !strings.Contains(out, want) { + t.Fatalf("%s help missing %q:\n%s", cmd, want, out) + } + } + } + var buf bytes.Buffer root, err := bonfire.NewRoot("summer", toolCommands(), &buf) if err != nil { t.Fatal(err) } - root.SetArgs([]string{"make:plugin", "--help"}) - if err := root.Execute(); err != nil { - t.Fatal(err) - } - if !strings.Contains(buf.String(), "vendor.plugin") { - t.Fatalf("make:plugin help missing argument text:\n%s", buf.String()) + root.SetArgs([]string{"make:model"}) + err = root.Execute() + if err == nil || !strings.Contains(err.Error(), "vendor.plugin") || !strings.Contains(err.Error(), "Name") { + t.Fatalf("make:model missing args error = %v", err) } } @@ -68,3 +92,121 @@ func TestToolDoesNotImportExamplePlugins(t *testing.T) { } } } + +func TestMakeCommandsViaCLI(t *testing.T) { + dir := copyHelloApp(t) + t.Chdir(dir) + + runSummer := func(args ...string) string { + t.Helper() + var buf bytes.Buffer + root, err := bonfire.NewRoot("summer", toolCommands(), &buf) + if err != nil { + t.Fatal(err) + } + root.SetArgs(args) + if err := root.Execute(); err != nil { + t.Fatalf("%v: %v\n%s", args, err, buf.String()) + } + return buf.String() + } + + out := runSummer("make:plugin", "golem15.demo") + if !strings.Contains(out, filepath.Join("plugins", "demo")) { + t.Fatalf("make:plugin output missing plugin dir:\n%s", out) + } + pluginDir := filepath.Join(dir, "plugins", "demo") + if _, err := os.Stat(filepath.Join(pluginDir, "plugin.go")); err != nil { + t.Fatal(err) + } + + out = runSummer("make:model", "golem15.demo", "Album", "--no-migration") + if !strings.Contains(out, filepath.Join("models", "album.go")) { + t.Fatalf("make:model output missing album.go:\n%s", out) + } + if strings.Contains(out, "create_golem15_demo_albums") { + t.Fatalf("--no-migration still printed a migration:\n%s", out) + } + + out = runSummer("make:job", "golem15.demo", "Reindex") + if !strings.Contains(out, filepath.Join("jobs", "reindex.go")) { + t.Fatalf("make:job output missing reindex.go:\n%s", out) + } + + t.Chdir(pluginDir) + out = runSummer("make:command", "Purge") + if !strings.Contains(out, filepath.Join("console", "purge.go")) { + t.Fatalf("one-arg make:command output missing purge.go:\n%s", out) + } + + pluginGo, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go")) + if err != nil { + t.Fatal(err) + } + if !bytes.Contains(pluginGo, []byte("generatedJobs()")) { + t.Fatal("scaffold plugin.go should already call generated accessors") + } +} + +func copyHelloApp(t *testing.T) string { + t.Helper() + src, err := filepath.Abs(filepath.Join("..", "..", "examples", "hello")) + if err != nil { + t.Fatal(err) + } + framework, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatal(err) + } + dst := t.TempDir() + if err := filepath.WalkDir(src, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + if rel == "bin" || strings.HasPrefix(rel, "bin"+string(os.PathSeparator)) { + if d.IsDir() { + return filepath.SkipDir + } + return nil + } + target := filepath.Join(dst, rel) + if d.IsDir() { + return os.MkdirAll(target, 0o755) + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + if filepath.Base(path) == "go.mod" { + data = rewriteFrameworkReplace(data, framework) + } + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return err + } + return os.WriteFile(target, data, 0o644) + }); err != nil { + t.Fatal(err) + } + return dst +} + +func rewriteFrameworkReplace(data []byte, framework string) []byte { + const module = "git.golem15.com/golem15/summercms" + lines := strings.Split(string(data), "\n") + for i, line := range lines { + trimmed := strings.TrimSpace(line) + switch { + case strings.HasPrefix(trimmed, "replace "+module+" =>"): + indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))] + lines[i] = indent + "replace " + module + " => " + framework + case strings.HasPrefix(trimmed, module+" =>"): + indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))] + lines[i] = indent + module + " => " + framework + } + } + return []byte(strings.Join(lines, "\n")) +} diff --git a/internal/build/build_test.go b/internal/build/build_test.go index d083d86..3f4e726 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -537,6 +537,11 @@ func TestScaffoldCoreArtifacts(t *testing.T) { t.Fatal("handwritten plugin.go was rewritten") } + greeterGo := filepath.Join(dir, "plugins", "greeter", "plugin.go") + greeterBefore, err := os.ReadFile(greeterGo) + if err != nil { + t.Fatal(err) + } greeter, err := MakeCommand(ctx, filepath.Join(dir, "plugins", "greeter"), "", "Wave") if err != nil { t.Fatal(err) @@ -544,6 +549,13 @@ func TestScaffoldCoreArtifacts(t *testing.T) { if greeter.Hint == "" || !strings.Contains(greeter.Hint, "generatedCommands()") { t.Fatalf("want accessor hint for handwritten plugin, got %q", greeter.Hint) } + greeterAfter, err := os.ReadFile(greeterGo) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(greeterBefore, greeterAfter) { + t.Fatal("handwritten greeter plugin.go was rewritten") + } if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil { t.Fatal(err) @@ -570,21 +582,90 @@ func TestScaffoldAllArtifacts(t *testing.T) { t.Fatal(err) } - if _, err := MakeModel(ctx, dir, "golem15.demo", "Album", false); err != nil { + model, err := MakeModel(ctx, dir, "golem15.demo", "Album", false) + if err != nil { t.Fatal(err) } - if _, err := MakeMigration(ctx, dir, "golem15.demo", "AddAlbumIndex"); err != nil { + wantModel := filepath.Join(pluginDir, "models", "album.go") + if len(model.Files) != 2 || model.Files[0] != wantModel { + t.Fatalf("make:model files = %v, want %s plus create-table migration", model.Files, wantModel) + } + albumSrc, err := os.ReadFile(wantModel) + if err != nil { t.Fatal(err) } - if _, err := MakeCommand(ctx, dir, "golem15.demo", "Purge"); err != nil { + for _, want := range []string{ + "type Album struct", + "CreatedAt", + "UpdatedAt", + `return "golem15_demo_albums"`, + } { + if !bytes.Contains(albumSrc, []byte(want)) { + t.Fatalf("album.go missing %s:\n%s", want, albumSrc) + } + } + createAlbum, err := filepath.Glob(filepath.Join(pluginDir, "updates", "*_create_golem15_demo_albums.go")) + if err != nil { t.Fatal(err) } + if len(createAlbum) != 1 || createAlbum[0] != model.Files[1] { + t.Fatalf("create-table migration files = %v want %v", createAlbum, model.Files[1:]) + } + migSrc, err := os.ReadFile(createAlbum[0]) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + "ID: \"", + "Migrate:", + "Rollback:", + "CREATE TABLE golem15_demo_albums", + "DROP TABLE IF EXISTS golem15_demo_albums", + } { + if !bytes.Contains(migSrc, []byte(want)) { + t.Fatalf("model migration missing %s:\n%s", want, migSrc) + } + } + + widget, err := MakeModel(ctx, pluginDir, "", "Widget", true) + if err != nil { + t.Fatal(err) + } + if len(widget.Files) != 1 || widget.Files[0] != filepath.Join(pluginDir, "models", "widget.go") { + t.Fatalf("--no-migration files = %v", widget.Files) + } + widgetMigs, err := filepath.Glob(filepath.Join(pluginDir, "updates", "*widget*")) + if err != nil { + t.Fatal(err) + } + if len(widgetMigs) != 0 { + t.Fatalf("--no-migration wrote %v", widgetMigs) + } + + migration, err := MakeMigration(ctx, dir, "golem15.demo", "AddAlbumIndex") + if err != nil { + t.Fatal(err) + } + if len(migration.Files) != 1 || !strings.Contains(migration.Files[0], "add_album_index.go") { + t.Fatalf("make:migration files = %v", migration.Files) + } + + command, err := MakeCommand(ctx, dir, "golem15.demo", "Purge") + if err != nil { + t.Fatal(err) + } + wantCommand := filepath.Join(pluginDir, "console", "purge.go") + if len(command.Files) != 1 || command.Files[0] != wantCommand { + t.Fatalf("make:command files = %v, want %s", command.Files, wantCommand) + } + job, err := MakeJob(ctx, dir, "golem15.demo", "Reindex") if err != nil { t.Fatal(err) } - if len(job.Files) != 1 { - t.Fatalf("make:job files = %v", job.Files) + wantJob := filepath.Join(pluginDir, "jobs", "reindex.go") + if len(job.Files) != 1 || job.Files[0] != wantJob { + t.Fatalf("make:job files = %v, want %s", job.Files, wantJob) } jobSrc, err := os.ReadFile(job.Files[0]) if err != nil { @@ -592,8 +673,10 @@ func TestScaffoldAllArtifacts(t *testing.T) { } for _, want := range []string{ "func (ReindexArgs) Kind()", + `Kind() string { return "golem15.demo.reindex" }`, "func (reindexJob) Work(", "unexpected args type", + "func ReindexJob()", } { if !bytes.Contains(jobSrc, []byte(want)) { t.Fatalf("job.go missing %s:\n%s", want, jobSrc) @@ -607,10 +690,20 @@ func TestScaffoldAllArtifacts(t *testing.T) { if err != nil { t.Fatal(err) } + wantAdmin := []string{ + filepath.Join(pluginDir, "controllers", "albums.go"), + filepath.Join(pluginDir, "controllers", "albums", "fields.yaml"), + filepath.Join(pluginDir, "controllers", "albums", "columns.yaml"), + } if len(admin.Files) != 3 { t.Fatalf("make:admin-controller files = %v", admin.Files) } - adminSrc, err := os.ReadFile(filepath.Join(pluginDir, "controllers", "albums.go")) + for i, want := range wantAdmin { + if admin.Files[i] != want { + t.Fatalf("admin file[%d] = %s, want %s", i, admin.Files[i], want) + } + } + adminSrc, err := os.ReadFile(wantAdmin[0]) if err != nil { t.Fatal(err) } @@ -646,6 +739,7 @@ func TestScaffoldAllArtifacts(t *testing.T) { } for _, want := range []string{ "&models.Album{}", + "&models.Widget{}", "updates.CreateAlbums()", "updates.AddAlbumIndex()", "console.PurgeCommand()", @@ -684,8 +778,13 @@ func TestScaffoldAllArtifacts(t *testing.T) { if err := App(ctx, dir, &buf); err != nil { t.Fatalf("app build: %v\n%s", err, buf.String()) } - vet := execGo(t, pluginDir, "vet", "./...") - if out, err := vet.CombinedOutput(); err != nil { + if out, err := execGo(t, dir, "build", "./...").CombinedOutput(); err != nil { + t.Fatalf("copied workspace go build: %v\n%s", err, out) + } + if out, err := execGo(t, dir, "vet", "./...").CombinedOutput(); err != nil { + t.Fatalf("copied workspace go vet: %v\n%s", err, out) + } + if out, err := execGo(t, pluginDir, "vet", "./...").CombinedOutput(); err != nil { t.Fatalf("plugin go vet: %v\n%s", err, out) } } @@ -721,6 +820,45 @@ func TestModelsLeaf(t *testing.T) { } } +func TestMakeArtifactRejectsMalformedNames(t *testing.T) { + dir := copyHelloApp(t) + ctx := t.Context() + if _, err := MakePlugin(ctx, dir, "golem15.demo"); err != nil { + t.Fatal(err) + } + + for _, name := range []string{"", "album", "../Evil", "Album;rm", `Album$(x)`, "Album/tmp", "2Album", "Album name"} { + if _, err := MakeModel(ctx, dir, "golem15.demo", name, true); err == nil { + t.Fatalf("make:model %q: want error", name) + } + if _, err := MakeMigration(ctx, dir, "golem15.demo", name); err == nil { + t.Fatalf("make:migration %q: want error", name) + } + if _, err := MakeCommand(ctx, dir, "golem15.demo", name); err == nil { + t.Fatalf("make:command %q: want error", name) + } + if _, err := MakeJob(ctx, dir, "golem15.demo", name); err == nil { + t.Fatalf("make:job %q: want error", name) + } + if _, err := MakeAdminController(ctx, dir, "golem15.demo", name); err == nil { + t.Fatalf("make:admin-controller %q: want error", name) + } + } + + for _, id := range []string{"golem15.demo/../tmp", "Golem15.Demo", "../evil.plugin", "golem15.demo;rm"} { + if _, err := MakeModel(ctx, dir, id, "Album", true); err == nil { + t.Fatalf("make:model plugin %q: want error", id) + } + } + + if _, err := MakeModel(ctx, dir, "golem15.demo", "Album", true); err != nil { + t.Fatal(err) + } + if _, err := MakeModel(ctx, dir, "golem15.demo", "Album", true); err == nil { + t.Fatal("duplicate make:model want error") + } +} + func assertScaffoldFiles(t *testing.T, pluginDir string) { t.Helper() assertWinterPluginShape(t, pluginDir)