test(04-04): close scaffold and CLI command boundary coverage
- Compile and vet every generated artifact in a copied hello workspace - Reject malformed names, traversal, and duplicates through public Make* - Cover CLI argument forms, --no-migration, and one-arg make inside a plugin Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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{"make:plugin", "--help"})
|
||||
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)
|
||||
}
|
||||
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"))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user