test(summer): plugin command prompts

- TestPluginCommands: plugin:install needs --yes off a terminal, --offline
  and a disabled registry refuse ID installs without registry calls,
  plugin:manifest --check exit status and output on missing and stale files
- check-phase34.sh: the 34-06 unit tests are required names of --go
This commit is contained in:
Jakub Zych
2026-10-29 21:22:47 +01:00
parent 7de774b0b3
commit a231309721
2 changed files with 170 additions and 0 deletions

View File

@@ -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")
}
})
}

View File

@@ -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