diff --git a/cmd/summer/plugin_commands_test.go b/cmd/summer/plugin_commands_test.go new file mode 100644 index 00000000..31ea3766 --- /dev/null +++ b/cmd/summer/plugin_commands_test.go @@ -0,0 +1,161 @@ +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") + } + }) +} diff --git a/scripts/check-phase34.sh b/scripts/check-phase34.sh index 26448b0c..d4ea1d7b 100755 --- a/scripts/check-phase34.sh +++ b/scripts/check-phase34.sh @@ -91,6 +91,15 @@ GO_TESTS=( ./modules/kiosk:TestManifestSmoke ./modules/kiosk:TestPluginManifest ./internal/build:TestPluginInfoSmoke ./internal/build:TestAddPluginConstID ./modules/cabana:TestPluginsScreenSmoke + # 34-06 unit tests. + ./modules/kiosk:TestReadIdentity ./modules/kiosk:TestDerive ./modules/kiosk:TestManifestFormat + ./modules/kiosk:TestCheck ./modules/kiosk:TestVersions ./modules/kiosk:TestRegistryClient + ./modules/kiosk:TestPackagePath ./modules/kiosk:TestPluginDirs ./modules/kiosk:TestLastElem + ./modules/kiosk:ExampleMarshal ./modules/kiosk:ExampleNewestCompatible + ./modules/party:TestDescribe + ./internal/build:TestPluginDirs ./internal/build:TestEffectiveFramework ./internal/build:TestResolveInstall + ./internal/build:TestAskToProceed ./internal/build:TestAppendManifestPlugins + ./cmd/summer:TestPluginCommands ) # docker stage: the non-short framework tests (file:// proxies, git