package main import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync/atomic" "testing" "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/kiosk" ) // runTool runs the summer CLI with args through bonfire.Execute and // returns the exit status and what went to the root's writer. func runTool(t *testing.T, args ...string) (int, string) { t.Helper() var buf bytes.Buffer root, err := bonfire.NewRoot("summer", toolCommands(), &buf) if err != nil { t.Fatal(err) } root.SetErr(&buf) code := bonfire.Execute(context.Background(), root, args) return code, buf.String() } // writeFiles writes files (slash paths relative to root). func writeFiles(t *testing.T, root string, files map[string]string) { t.Helper() for rel, body := range files { path := filepath.Join(root, filepath.FromSlash(rel)) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(body), 0o644); err != nil { t.Fatal(err) } } } // TestPluginCommands covers the CLI rules of plugin:install and // plugin:manifest that do not need a build: the --yes rule off a terminal, // --offline ID installs, and --check's exit status and output. func TestPluginCommands(t *testing.T) { var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) if r.URL.Path == kiosk.RegistryAPIPath+"/plugins/acme.shop/versions" { _ = json.NewEncoder(w).Encode(kiosk.VersionsResponse{Data: []kiosk.RegistryVersion{{ Version: "v0.1.0", Framework: "v0.6.0", Module: "example.test/sm-shop-plugin", Package: "example.test/sm-shop-plugin", }}}) return } http.NotFound(w, r) })) t.Cleanup(srv.Close) app := t.TempDir() writeFiles(t, app, map[string]string{ "summer.yaml": "module: example.test/app\nbinary: app\n", "go.mod": "module example.test/app\n\ngo 1.27.0\n\nrequire git.golem15.com/golem15/summercms v0.6.0\n", "config/.gitkeep": "", }) t.Chdir(app) t.Setenv("SUMMER_PLUGINS__REGISTRY_URL", srv.URL) t.Run("install needs --yes off a terminal", func(t *testing.T) { code, out := runTool(t, "plugin:install", "acme.shop") if code == bonfire.ExitOK || !strings.Contains(out, "--yes") { t.Fatalf("exit %d, output:\n%s", code, out) } if !strings.Contains(out, "acme.shop@v0.1.0 (module)") { t.Errorf("the plan was not printed before the refusal:\n%s", out) } before, _ := os.ReadFile(filepath.Join(app, "summer.yaml")) if string(before) != "module: example.test/app\nbinary: app\n" { t.Errorf("summer.yaml changed:\n%s", before) } }) t.Run("offline ID install", func(t *testing.T) { calls.Store(0) code, out := runTool(t, "plugin:install", "acme.shop", "--offline", "--yes") if code == bonfire.ExitOK || !strings.Contains(out, "--offline") { t.Fatalf("exit %d, output:\n%s", code, out) } if n := calls.Load(); n != 0 { t.Errorf("--offline made %d registry calls", n) } }) t.Run("disabled registry", func(t *testing.T) { t.Setenv("SUMMER_PLUGINS__REGISTRY_URL", "") calls.Store(0) code, out := runTool(t, "plugin:install", "acme.shop", "--yes") if code == bonfire.ExitOK || !strings.Contains(out, "plugins.registry_url") || calls.Load() != 0 { t.Fatalf("exit %d, calls %d, output:\n%s", code, calls.Load(), out) } }) t.Run("install outside an application", func(t *testing.T) { t.Chdir(t.TempDir()) if code, _ := runTool(t, "plugin:install", "acme.shop", "--yes"); code == bonfire.ExitOK { t.Error("plugin:install outside an application succeeded") } }) t.Run("manifest --check", func(t *testing.T) { plugin := t.TempDir() writeFiles(t, plugin, map[string]string{ "go.mod": "module example.test/sm-shop-plugin\n\ngo 1.27.0\n\nrequire " + kiosk.FrameworkModule + " v0.6.0\n", "plugin.go": "package shop\n\ntype Plugin struct{}\n\nfunc (*Plugin) ID() string { return \"acme.shop\" }\n", "README.md": "# Shop\n\nProducts and orders.\n", }) path := filepath.Join(plugin, kiosk.FileName) code, out := runTool(t, "plugin:manifest", "--check", plugin) if code != bonfire.ExitFailure || !strings.Contains(out, "Expected "+path+":") || !strings.Contains(out, "id: acme.shop") || !strings.Contains(out, "is missing") { t.Fatalf("missing: exit %d, output:\n%s", code, out) } if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatalf("--check wrote %s", path) } if code, out := runTool(t, "plugin:manifest", plugin); code != bonfire.ExitOK || !strings.Contains(out, "Wrote "+path) { t.Fatalf("write: exit %d, output:\n%s", code, out) } if code, out := runTool(t, "plugin:manifest", "--check", plugin); code != bonfire.ExitOK || !strings.Contains(out, "is up to date") { t.Fatalf("fresh: exit %d, output:\n%s", code, out) } writeFiles(t, plugin, map[string]string{"README.md": "# Shop\n\nProducts, carts and orders.\n"}) written, _ := os.ReadFile(path) code, out = runTool(t, "plugin:manifest", "--check", plugin) if code != bonfire.ExitFailure || !strings.Contains(out, "is stale") || !strings.Contains(out, "description: Products, carts and orders.") { t.Fatalf("stale: exit %d, output:\n%s", code, out) } if after, _ := os.ReadFile(path); !bytes.Equal(after, written) { t.Error("--check rewrote a stale file") } code, out = runTool(t, "plugin:manifest", "--check", "--name", "Shop", plugin) if code == bonfire.ExitOK || !strings.Contains(out, "takes no --name or --description") { t.Fatalf("--check --name: exit %d, output:\n%s", code, out) } code, out = runTool(t, "plugin:manifest", "--pretend", plugin) if code != bonfire.ExitOK || !strings.Contains(out, "description: Products, carts and orders.") { t.Fatalf("--pretend: exit %d, output:\n%s", code, out) } if after, _ := os.ReadFile(path); !bytes.Equal(after, written) { t.Error("--pretend wrote the file") } }) }