Scaffold pact.Job and pact.AdminController stubs with Winter YAML assets, and fail summer build when models imports a sibling package. Co-authored-by: Cursor <cursoragent@cursor.com>
900 lines
25 KiB
Go
900 lines
25 KiB
Go
package build
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadHelloManifest(t *testing.T) {
|
|
m, err := LoadManifest(filepath.Join("..", "..", "examples", "hello", "summer.yaml"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m.Binary != "hello" {
|
|
t.Fatalf("binary = %q", m.Binary)
|
|
}
|
|
if len(m.Plugins) != 3 || m.Plugins[0].ID != "golem15.hello" || m.Plugins[1].ID != "golem15.greeter" || m.Plugins[2].ID != "golem15.optional" {
|
|
t.Fatalf("plugins = %+v", m.Plugins)
|
|
}
|
|
}
|
|
|
|
func TestParseManifestRejectsDuplicatesAndInvalidPaths(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
t.Run("duplicate id", func(t *testing.T) {
|
|
path := filepath.Join(dir, "dup.yaml")
|
|
write(t, path, "module: example.com/app\nbinary: app\nplugins:\n - id: a.b\n module: example.com/one\n - id: a.b\n module: example.com/two\n")
|
|
if _, err := LoadManifest(path); err == nil || !strings.Contains(err.Error(), "a.b") {
|
|
t.Fatalf("want duplicate id error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid module", func(t *testing.T) {
|
|
path := filepath.Join(dir, "bad.yaml")
|
|
write(t, path, "module: example.com/app\nbinary: app\nplugins:\n - id: a.b\n module: \"example.com/one; rm -rf /\"\n")
|
|
if _, err := LoadManifest(path); err == nil {
|
|
t.Fatal("want invalid module error")
|
|
}
|
|
})
|
|
|
|
t.Run("path traversal module", func(t *testing.T) {
|
|
path := filepath.Join(dir, "dotdot.yaml")
|
|
write(t, path, "module: example.com/app\nbinary: app\nplugins:\n - id: a.b\n module: example.com/../evil\n")
|
|
if _, err := LoadManifest(path); err == nil {
|
|
t.Fatal("want invalid module error")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGenerateStableQuotedImportsInManifestOrder(t *testing.T) {
|
|
m := Manifest{
|
|
Module: "example.com/app",
|
|
Binary: "hello",
|
|
Plugins: []Plugin{
|
|
{ID: "golem15.hello", Module: "example.com/app/plugins/base"},
|
|
{ID: "golem15.greeter", Module: "example.com/app/plugins/greeter"},
|
|
},
|
|
}
|
|
first, err := generatePluginsGen(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := generatePluginsGen(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(first, second) {
|
|
t.Fatal("generated plugins.gen.go is not byte-stable")
|
|
}
|
|
src := string(first)
|
|
baseAt := strings.Index(src, `example.com/app/plugins/base`)
|
|
greeterAt := strings.Index(src, `example.com/app/plugins/greeter`)
|
|
if baseAt < 0 || greeterAt < 0 || baseAt > greeterAt {
|
|
t.Fatalf("imports not in manifest order:\n%s", src)
|
|
}
|
|
if !strings.Contains(src, "\t_ \"example.com/app/plugins/base\"") {
|
|
t.Fatalf("import is not Go-quoted:\n%s", src)
|
|
}
|
|
|
|
mainSrc, err := generateMain(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
again, err := generateMain(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(mainSrc, again) {
|
|
t.Fatal("generated main.go is not byte-stable")
|
|
}
|
|
if !bytes.Contains(mainSrc, []byte(`bonfire.NewRoot("hello"`)) {
|
|
t.Fatalf("main does not use manifest binary:\n%s", mainSrc)
|
|
}
|
|
if !bytes.Contains(mainSrc, []byte("lagoon.RuntimeCommands")) {
|
|
t.Fatalf("main does not register lagoon runtime commands:\n%s", mainSrc)
|
|
}
|
|
if !bytes.Contains(mainSrc, []byte("surf.ServeCommand")) {
|
|
t.Fatalf("main does not register serve:\n%s", mainSrc)
|
|
}
|
|
if bytes.Contains(mainSrc, []byte("examples/hello")) {
|
|
t.Fatal("generated main hard-codes examples/hello")
|
|
}
|
|
}
|
|
|
|
func TestWriteIfChangedSkipsIdenticalBytes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "out.go")
|
|
content := []byte("package main\n")
|
|
if err := writeIfChanged(path, content); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writeIfChanged(path, content); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
again, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !info.ModTime().Equal(again.ModTime()) {
|
|
t.Fatal("identical content rewrote the file")
|
|
}
|
|
}
|
|
|
|
func TestMakePluginRejectsInvalidID(t *testing.T) {
|
|
dir := t.TempDir()
|
|
write(t, filepath.Join(dir, "summer.yaml"), "module: example.com/app\nbinary: app\nplugins:\n")
|
|
write(t, filepath.Join(dir, "go.mod"), "module example.com/app\n\ngo 1.27.0\n")
|
|
|
|
for _, id := range []string{"Golem15.Demo", "golem15", "golem15.demo/../tmp", "../evil.plugin"} {
|
|
if _, err := MakePlugin(t.Context(), dir, id); err == nil {
|
|
t.Fatalf("id %q: want error", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMakeAndAddPluginOnHelloCopy(t *testing.T) {
|
|
dir := copyHelloApp(t)
|
|
ctx := t.Context()
|
|
|
|
pluginDir, err := MakePlugin(ctx, dir, "golem15.demo")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantDir := filepath.Join(dir, "plugins", "demo")
|
|
if pluginDir != wantDir {
|
|
t.Fatalf("plugin dir = %s, want %s", pluginDir, wantDir)
|
|
}
|
|
assertScaffoldFiles(t, pluginDir)
|
|
|
|
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
|
t.Fatalf("repeat plugin:add: %v", err)
|
|
}
|
|
|
|
m, err := LoadManifest(filepath.Join(dir, "summer.yaml"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(m.Plugins) != 4 {
|
|
t.Fatalf("plugins = %+v", m.Plugins)
|
|
}
|
|
last := m.Plugins[len(m.Plugins)-1]
|
|
if last.ID != "golem15.demo" {
|
|
t.Fatalf("last plugin id = %q", last.ID)
|
|
}
|
|
if last.Module != m.Module+"/plugins/demo" {
|
|
t.Fatalf("last plugin module = %q", last.Module)
|
|
}
|
|
demoCount := 0
|
|
for _, p := range m.Plugins {
|
|
if p.ID == "golem15.demo" {
|
|
demoCount++
|
|
}
|
|
}
|
|
if demoCount != 1 {
|
|
t.Fatalf("golem15.demo count = %d", demoCount)
|
|
}
|
|
|
|
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 missing plugins/demo:\n%s", work)
|
|
}
|
|
appMod, err := os.ReadFile(filepath.Join(dir, "go.mod"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Contains(appMod, []byte(last.Module)) {
|
|
t.Fatalf("app go.mod missing %s:\n%s", last.Module, appMod)
|
|
}
|
|
|
|
var first, second bytes.Buffer
|
|
if err := App(ctx, dir, &first); err != nil {
|
|
t.Fatalf("first build: %v\n%s", err, first.String())
|
|
}
|
|
if !strings.Contains(first.String(), "built hello in ") {
|
|
t.Fatalf("missing elapsed build time:\n%s", first.String())
|
|
}
|
|
main1, err := os.ReadFile(filepath.Join(dir, "main.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gen1, err := os.ReadFile(filepath.Join(dir, "plugins.gen.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Contains(gen1, []byte(last.Module)) {
|
|
t.Fatalf("plugins.gen.go missing %s:\n%s", last.Module, gen1)
|
|
}
|
|
if !bytes.Contains(gen1, []byte(`"golem15.demo"`)) {
|
|
t.Fatalf("plugins.gen.go missing id:\n%s", gen1)
|
|
}
|
|
|
|
if err := App(ctx, dir, &second); err != nil {
|
|
t.Fatalf("second build: %v\n%s", err, second.String())
|
|
}
|
|
main2, err := os.ReadFile(filepath.Join(dir, "main.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gen2, err := os.ReadFile(filepath.Join(dir, "plugins.gen.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(main1, main2) {
|
|
t.Fatal("main.go is not byte-stable")
|
|
}
|
|
if !bytes.Equal(gen1, gen2) {
|
|
t.Fatal("plugins.gen.go is not byte-stable")
|
|
}
|
|
}
|
|
|
|
func TestAddPluginRejectsConflictingID(t *testing.T) {
|
|
dir := copyHelloApp(t)
|
|
ctx := t.Context()
|
|
if _, err := MakePlugin(ctx, dir, "golem15.demo"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
src, err := os.ReadFile(filepath.Join(dir, "plugins", "demo", "plugin.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
patched := bytes.ReplaceAll(src, []byte(`return "golem15.demo"`), []byte(`return "golem15.hello"`))
|
|
if err := os.WriteFile(filepath.Join(dir, "plugins", "demo", "plugin.go"), patched, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = AddPlugin(ctx, dir, filepath.Join(dir, "plugins", "demo"))
|
|
if err == nil || !strings.Contains(err.Error(), "golem15.hello") {
|
|
t.Fatalf("want conflicting id error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAddPluginRejectsPathTraversal(t *testing.T) {
|
|
dir := copyHelloApp(t)
|
|
outside := t.TempDir()
|
|
if err := AddPlugin(t.Context(), dir, outside); err == nil {
|
|
t.Fatal("want outside-root error")
|
|
}
|
|
}
|
|
|
|
func TestParseManifestRejectsDuplicateModule(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "dupmod.yaml")
|
|
write(t, path, "module: example.com/app\nbinary: app\nplugins:\n - id: a.b\n module: example.com/one\n - id: a.c\n module: example.com/one\n")
|
|
if _, err := LoadManifest(path); err == nil || !strings.Contains(err.Error(), "example.com/one") {
|
|
t.Fatalf("want duplicate module error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMakePluginRejectsPathTraversalAndShellMeta(t *testing.T) {
|
|
dir := t.TempDir()
|
|
write(t, filepath.Join(dir, "summer.yaml"), "module: example.com/app\nbinary: app\nplugins:\n")
|
|
write(t, filepath.Join(dir, "go.mod"), "module example.com/app\n\ngo 1.27.0\n")
|
|
for _, id := range []string{
|
|
"golem15.demo/../tmp",
|
|
"golem15.demo;rm",
|
|
`golem15.demo$(x)`,
|
|
"golem15.demo`x`",
|
|
"/tmp.evil",
|
|
"..evil.plugin",
|
|
} {
|
|
if _, err := MakePlugin(t.Context(), dir, id); err == nil {
|
|
t.Fatalf("id %q: want error", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuiltHelloFailsOnInvalidRequires(t *testing.T) {
|
|
dir := copyHelloApp(t)
|
|
path := filepath.Join(dir, "plugins", "greeter", "plugin.go")
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
patched := bytes.ReplaceAll(src, []byte(`return []string{"golem15.hello"}`), []byte(`return []string{"golem15.hello", "golem15.missing"}`))
|
|
if bytes.Equal(src, patched) {
|
|
t.Fatal("failed to patch greeter Requires")
|
|
}
|
|
if err := os.WriteFile(path, patched, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := App(t.Context(), dir, &buf); err != nil {
|
|
t.Fatalf("build should succeed, got %v\n%s", err, buf.String())
|
|
}
|
|
bin := filepath.Join(dir, "bin", "hello")
|
|
cmd := exec.CommandContext(t.Context(), bin, "greeter:hello")
|
|
cmd.Dir = dir
|
|
out, err := cmd.CombinedOutput()
|
|
if err == nil {
|
|
t.Fatalf("expected non-zero exit for missing Requires, output:\n%s", out)
|
|
}
|
|
msg := string(out)
|
|
if !strings.Contains(msg, "golem15.greeter") || !strings.Contains(msg, "golem15.missing") {
|
|
t.Fatalf("error should name both plugin IDs, got %q", msg)
|
|
}
|
|
}
|
|
|
|
func TestParseManifestRejectsUppercaseID(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "bad.yaml")
|
|
write(t, path, "module: example.com/app\nbinary: app\nplugins:\n - id: Golem15.Demo\n module: example.com/one\n")
|
|
if _, err := LoadManifest(path); err == nil || !strings.Contains(err.Error(), "vendor.plugin") {
|
|
t.Fatalf("want id format error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestScaffoldPluginSmoke(t *testing.T) {
|
|
dir := copyHelloApp(t)
|
|
ctx := t.Context()
|
|
|
|
pluginDir, err := MakePlugin(ctx, dir, "golem15.demo")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertScaffoldFiles(t, pluginDir)
|
|
|
|
pluginSrc, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
regSrc, err := os.ReadFile(filepath.Join(pluginDir, "registry.gen.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
"generatedModels()",
|
|
"generatedMigrations()",
|
|
"generatedCommands()",
|
|
"generatedJobs()",
|
|
"generatedAdminControllers()",
|
|
"//go:embed config",
|
|
"//go:embed lang",
|
|
"//go:embed views/mail",
|
|
} {
|
|
if !bytes.Contains(pluginSrc, []byte(want)) {
|
|
t.Fatalf("plugin.go missing %s:\n%s", want, pluginSrc)
|
|
}
|
|
}
|
|
for _, want := range []string{
|
|
"func generatedModels()",
|
|
"func generatedMigrations()",
|
|
"func generatedCommands()",
|
|
"func generatedJobs()",
|
|
"func generatedAdminControllers()",
|
|
} {
|
|
if !bytes.Contains(regSrc, []byte(want)) {
|
|
t.Fatalf("registry.gen.go missing %s:\n%s", want, regSrc)
|
|
}
|
|
}
|
|
|
|
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var buf bytes.Buffer
|
|
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 {
|
|
t.Fatalf("plugin go vet: %v\n%s", err, out)
|
|
}
|
|
|
|
pluginAfter, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(pluginSrc, pluginAfter) {
|
|
t.Fatal("hand-written plugin.go was rewritten")
|
|
}
|
|
regAfter, err := os.ReadFile(filepath.Join(pluginDir, "registry.gen.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(regSrc, regAfter) {
|
|
t.Fatal("registry.gen.go is not byte-stable after add/build")
|
|
}
|
|
}
|
|
|
|
func TestScaffoldCoreArtifacts(t *testing.T) {
|
|
dir := copyHelloApp(t)
|
|
ctx := t.Context()
|
|
pluginDir, err := MakePlugin(ctx, dir, "golem15.demo")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pluginBefore, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
model, err := MakeModel(ctx, dir, "golem15.demo", "Album", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if model.Hint != "" {
|
|
t.Fatalf("new plugin printed accessor hint: %s", model.Hint)
|
|
}
|
|
if len(model.Files) != 2 {
|
|
t.Fatalf("make:model files = %v", model.Files)
|
|
}
|
|
albumSrc, err := os.ReadFile(filepath.Join(pluginDir, "models", "album.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
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 {
|
|
t.Fatalf("create-table migration files = %v", createAlbum)
|
|
}
|
|
migSrc, err := os.ReadFile(createAlbum[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
"func CreateAlbums()",
|
|
"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 || !strings.HasSuffix(widget.Files[0], filepath.Join("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)
|
|
}
|
|
|
|
if _, err := MakeMigration(ctx, dir, "golem15.demo", "AddWidgetIndex"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := MakeCommand(ctx, dir, "golem15.demo", "Purge"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmdSrc, err := os.ReadFile(filepath.Join(pluginDir, "console", "purge.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Contains(cmdSrc, []byte(`"demo:purge"`)) {
|
|
t.Fatalf("command name missing:\n%s", cmdSrc)
|
|
}
|
|
|
|
if _, err := MakeModel(ctx, dir, "golem15.demo", "Album", false); err == nil {
|
|
t.Fatal("duplicate make:model want error")
|
|
}
|
|
reg1, err := os.ReadFile(filepath.Join(pluginDir, "registry.gen.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := MakeCommand(ctx, dir, "golem15.demo", "Purge"); err == nil {
|
|
t.Fatal("duplicate make:command want error")
|
|
}
|
|
if err := refreshRegistry(pluginDir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reg2, err := os.ReadFile(filepath.Join(pluginDir, "registry.gen.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(reg1, reg2) {
|
|
t.Fatal("repeated generation changed registry.gen.go")
|
|
}
|
|
for _, want := range []string{
|
|
"&models.Album{}",
|
|
"&models.Widget{}",
|
|
"updates.CreateAlbums()",
|
|
"updates.AddWidgetIndex()",
|
|
"console.PurgeCommand()",
|
|
} {
|
|
if !bytes.Contains(reg1, []byte(want)) {
|
|
t.Fatalf("registry.gen.go missing %s:\n%s", want, reg1)
|
|
}
|
|
}
|
|
|
|
pluginAfter, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(pluginBefore, pluginAfter) {
|
|
t.Fatal("handwritten plugin.go was rewritten")
|
|
}
|
|
|
|
greeter, err := MakeCommand(ctx, filepath.Join(dir, "plugins", "greeter"), "", "Wave")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if greeter.Hint == "" || !strings.Contains(greeter.Hint, "generatedCommands()") {
|
|
t.Fatalf("want accessor hint for handwritten plugin, got %q", greeter.Hint)
|
|
}
|
|
|
|
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var buf bytes.Buffer
|
|
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 {
|
|
t.Fatalf("plugin go vet: %v\n%s", err, out)
|
|
}
|
|
}
|
|
|
|
func TestScaffoldAllArtifacts(t *testing.T) {
|
|
dir := copyHelloApp(t)
|
|
ctx := t.Context()
|
|
pluginDir, err := MakePlugin(ctx, dir, "golem15.demo")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pluginBefore, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := MakeModel(ctx, dir, "golem15.demo", "Album", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := MakeMigration(ctx, dir, "golem15.demo", "AddAlbumIndex"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := MakeCommand(ctx, dir, "golem15.demo", "Purge"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
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)
|
|
}
|
|
jobSrc, err := os.ReadFile(job.Files[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
"func (ReindexArgs) Kind()",
|
|
"func (reindexJob) Work(",
|
|
"unexpected args type",
|
|
} {
|
|
if !bytes.Contains(jobSrc, []byte(want)) {
|
|
t.Fatalf("job.go missing %s:\n%s", want, jobSrc)
|
|
}
|
|
}
|
|
if bytes.Contains(bytes.ToLower(jobSrc), []byte("river")) {
|
|
t.Fatal("job stub imports River")
|
|
}
|
|
|
|
admin, err := MakeAdminController(ctx, dir, "golem15.demo", "Albums")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(admin.Files) != 3 {
|
|
t.Fatalf("make:admin-controller files = %v", admin.Files)
|
|
}
|
|
adminSrc, err := os.ReadFile(filepath.Join(pluginDir, "controllers", "albums.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
`ID() string { return "golem15.demo.albums" }`,
|
|
`ModelName() string { return "Albums" }`,
|
|
`ConfigDir() string { return "controllers/albums" }`,
|
|
} {
|
|
if !bytes.Contains(adminSrc, []byte(want)) {
|
|
t.Fatalf("albums.go missing %s:\n%s", want, adminSrc)
|
|
}
|
|
}
|
|
for _, rel := range []string{
|
|
filepath.Join("controllers", "albums", "fields.yaml"),
|
|
filepath.Join("controllers", "albums", "columns.yaml"),
|
|
} {
|
|
body, err := os.ReadFile(filepath.Join(pluginDir, rel))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key := "fields:"
|
|
if strings.Contains(rel, "columns") {
|
|
key = "columns:"
|
|
}
|
|
if !bytes.Contains(body, []byte(key)) {
|
|
t.Fatalf("%s missing %s:\n%s", rel, key, body)
|
|
}
|
|
}
|
|
|
|
reg1, err := os.ReadFile(filepath.Join(pluginDir, "registry.gen.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
"&models.Album{}",
|
|
"updates.CreateAlbums()",
|
|
"updates.AddAlbumIndex()",
|
|
"console.PurgeCommand()",
|
|
"jobs.ReindexJob()",
|
|
"controllers.AlbumsController()",
|
|
} {
|
|
if !bytes.Contains(reg1, []byte(want)) {
|
|
t.Fatalf("registry.gen.go missing %s:\n%s", want, reg1)
|
|
}
|
|
}
|
|
if _, err := MakeJob(ctx, dir, "golem15.demo", "Reindex"); err == nil {
|
|
t.Fatal("duplicate make:job want error")
|
|
}
|
|
if err := refreshRegistry(pluginDir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reg2, err := os.ReadFile(filepath.Join(pluginDir, "registry.gen.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(reg1, reg2) {
|
|
t.Fatal("repeated generation changed registry.gen.go")
|
|
}
|
|
pluginAfter, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(pluginBefore, pluginAfter) {
|
|
t.Fatal("handwritten plugin.go was rewritten")
|
|
}
|
|
|
|
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var buf bytes.Buffer
|
|
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 {
|
|
t.Fatalf("plugin go vet: %v\n%s", err, out)
|
|
}
|
|
}
|
|
|
|
func TestModelsLeaf(t *testing.T) {
|
|
dir := copyHelloApp(t)
|
|
ctx := t.Context()
|
|
pluginDir, err := MakePlugin(ctx, dir, "golem15.demo")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
modPath, err := readModulePath(filepath.Join(pluginDir, "go.mod"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bad := "package models\n\nimport " + strconv.Quote(modPath+"/classes") + "\n"
|
|
if err := os.WriteFile(filepath.Join(pluginDir, "models", "bad.go"), []byte(bad), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var buf bytes.Buffer
|
|
err = App(ctx, dir, &buf)
|
|
if err == nil {
|
|
t.Fatal("want models leaf import error")
|
|
}
|
|
msg := err.Error()
|
|
for _, want := range []string{"golem15.demo", "models/bad.go", modPath + "/classes"} {
|
|
if !strings.Contains(msg, want) {
|
|
t.Fatalf("leaf error %q missing %s", msg, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertScaffoldFiles(t *testing.T, pluginDir string) {
|
|
t.Helper()
|
|
assertWinterPluginShape(t, pluginDir)
|
|
|
|
body, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
"func (p *Plugin) ID()",
|
|
"func (p *Plugin) Requires()",
|
|
"func (p *Plugin) Register",
|
|
"func (p *Plugin) Boot",
|
|
"party.Register(&Plugin{})",
|
|
`return "golem15.demo"`,
|
|
} {
|
|
if !bytes.Contains(body, []byte(want)) {
|
|
t.Fatalf("plugin.go missing %s:\n%s", want, body)
|
|
}
|
|
}
|
|
mod, err := os.ReadFile(filepath.Join(pluginDir, "go.mod"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Contains(mod, []byte("toolchain go1.27.0")) {
|
|
t.Fatalf("plugin go.mod missing toolchain:\n%s", mod)
|
|
}
|
|
if !bytes.Contains(mod, []byte("/plugins/demo")) {
|
|
t.Fatalf("plugin go.mod missing module path:\n%s", mod)
|
|
}
|
|
|
|
cmd := execGo(t, pluginDir, "build", "./...")
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("plugin does not compile: %v\n%s", err, out)
|
|
}
|
|
}
|
|
|
|
func assertWinterPluginShape(t *testing.T, pluginDir string) {
|
|
t.Helper()
|
|
required := []string{
|
|
"go.mod",
|
|
"plugin.go",
|
|
"routes.go",
|
|
"registry.gen.go",
|
|
filepath.Join("config", "config.yaml"),
|
|
filepath.Join("lang", "en", "lang.yaml"),
|
|
filepath.Join("views", "mail", "welcome.htm"),
|
|
}
|
|
for _, leaf := range pluginLeaves {
|
|
required = append(required, filepath.Join(leaf, "doc.go"))
|
|
}
|
|
allowed := map[string]struct{}{
|
|
"go.sum": {},
|
|
}
|
|
for _, rel := range required {
|
|
allowed[rel] = struct{}{}
|
|
if _, err := os.Stat(filepath.Join(pluginDir, rel)); err != nil {
|
|
t.Fatalf("missing %s: %v", rel, err)
|
|
}
|
|
}
|
|
|
|
var extra []string
|
|
if err := filepath.WalkDir(pluginDir, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
rel, err := filepath.Rel(pluginDir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := allowed[rel]; ok {
|
|
return nil
|
|
}
|
|
if strings.HasPrefix(filepath.Base(rel), "registry.gen.go.") {
|
|
extra = append(extra, rel)
|
|
return nil
|
|
}
|
|
extra = append(extra, rel)
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(extra) > 0 {
|
|
t.Fatalf("unexpected scaffold files: %v", extra)
|
|
}
|
|
|
|
for _, asset := range []string{
|
|
filepath.Join("config", "config.yaml"),
|
|
filepath.Join("lang", "en", "lang.yaml"),
|
|
filepath.Join("views", "mail", "welcome.htm"),
|
|
} {
|
|
if strings.HasPrefix(filepath.Base(asset), ".") || strings.HasPrefix(filepath.Base(asset), "_") {
|
|
t.Fatalf("embed-incompatible hidden asset %s", asset)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
lines := strings.Split(string(data), "\n")
|
|
for i, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
switch {
|
|
case strings.HasPrefix(trimmed, "replace "+frameworkModule+" =>"):
|
|
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
|
lines[i] = indent + "replace " + frameworkModule + " => " + framework
|
|
case strings.HasPrefix(trimmed, frameworkModule+" =>"):
|
|
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
|
lines[i] = indent + frameworkModule + " => " + framework
|
|
}
|
|
}
|
|
return []byte(strings.Join(lines, "\n"))
|
|
}
|
|
|
|
func execGo(t *testing.T, dir string, args ...string) *exec.Cmd {
|
|
t.Helper()
|
|
cmd := exec.CommandContext(t.Context(), "go", args...)
|
|
cmd.Dir = dir
|
|
applyGoWork(cmd, dir)
|
|
return cmd
|
|
}
|
|
|
|
func write(t *testing.T, path, body string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|