Files
summercms/internal/build/install_smoke_test.go
Jakub Zych 9b23e877a6 feat(build): summer plugin:install for registry plugins as Go modules
- kiosk.Client reads the registry API v1 over fetchguard (trusted mode,
  4 MiB, 10 s, no redirects); kiosk.RegistryURL reads plugins.registry_url
  (default https://summercms.io, empty disables)
- build.PlanInstall/ApplyInstall pick the newest release the effective
  framework satisfies (or a pin), validate registry strings, go get it and
  append it to summer.yaml
- summer.yaml edits (plugin:add and plugin:install) keep comments and
  blank lines
- TestInstallSmoke/module installs from a file:// GOPROXY and builds
2026-10-29 18:22:01 +01:00

214 lines
7.5 KiB
Go

package build
import (
"bytes"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"git.golem15.com/golem15/summercms/modules/kiosk"
)
const (
blogModule = "example.com/acme/blog"
// plantedComment is written into summer.yaml before an install; the
// install must keep it.
plantedComment = "# planted: plugin:install keeps this comment"
)
// plantComment puts a comment line and a blank line above the plugins
// list of the app's summer.yaml.
func plantComment(t *testing.T, appDir string) {
t.Helper()
path := filepath.Join(appDir, manifestName)
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
s := strings.Replace(string(raw), "plugins:\n", "\n"+plantedComment+"\nplugins:\n", 1)
write(t, path, s)
}
func assertManifestKeepsComment(t *testing.T, appDir string) {
t.Helper()
raw, err := os.ReadFile(filepath.Join(appDir, manifestName))
if err != nil {
t.Fatal(err)
}
if !bytes.Contains(raw, []byte(plantedComment)) {
t.Fatalf("summer.yaml lost the planted comment:\n%s", raw)
}
}
func manifestIDs(t *testing.T, appDir string) []string {
t.Helper()
m, err := LoadManifest(filepath.Join(appDir, manifestName))
if err != nil {
t.Fatal(err)
}
ids := make([]string, len(m.Plugins))
for i, p := range m.Plugins {
ids[i] = p.ID + "=" + p.Module
}
return ids
}
func TestInstallSmoke(t *testing.T) {
blogV1 := regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)
blogV2 := regVersion("v0.2.0", "v9.0.0", blogModule, blogModule, nil)
t.Run("module", func(t *testing.T) {
if testing.Short() {
t.Skip("builds an app against a file:// module proxy")
}
app := helloCopy(t)
plantComment(t, app)
proxy := t.TempDir()
writeProxyModule(t, proxy, blogModule, "v0.1.0", fixturePluginFiles(blogModule, "blog", "acme.blog", "v0.5.0"))
offlineGoEnv(t, proxy)
reg := fakeRegistry(t, map[string]fakePlugin{
"acme.blog": regPlugin("acme.blog", "https://example.com/acme/blog.git", blogV2, blogV1),
})
var out bytes.Buffer
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Registry: reg.Client, Out: &out})
if err != nil {
t.Fatalf("plan: %v\n%s", err, out.String())
}
if len(plan.Steps) != 1 || plan.Steps[0].Version != "v0.1.0" || plan.Steps[0].Mode != ModeModule {
t.Fatalf("plan steps = %+v", plan.Steps)
}
if !strings.Contains(out.String(), "acme.blog@v0.1.0 (module)") {
t.Fatalf("plan output lacks the step:\n%s", out.String())
}
if err := ApplyInstall(t.Context(), plan, &out); err != nil {
t.Fatalf("apply: %v\n%s", err, out.String())
}
if !strings.Contains(out.String(), "run summer build") {
t.Fatalf("apply output:\n%s", out.String())
}
gomod, err := os.ReadFile(filepath.Join(app, "go.mod"))
if err != nil {
t.Fatal(err)
}
if !bytes.Contains(gomod, []byte(blogModule+" v0.1.0")) {
t.Fatalf("go.mod does not require %s v0.1.0:\n%s", blogModule, gomod)
}
ids := manifestIDs(t, app)
if last := ids[len(ids)-1]; last != "acme.blog="+blogModule {
t.Fatalf("summer.yaml plugins = %v", ids)
}
assertManifestKeepsComment(t, app)
var log bytes.Buffer
if err := App(t.Context(), app, &log); err != nil {
t.Fatalf("build: %v\n%s", err, log.String())
}
gen, err := os.ReadFile(filepath.Join(app, "plugins.gen.go"))
if err != nil {
t.Fatal(err)
}
if !bytes.Contains(gen, []byte(`_ "`+blogModule+`"`)) || !bytes.Contains(gen, []byte(`Version: "v0.1.0"`)) {
t.Fatalf("plugins.gen.go does not import %s at v0.1.0:\n%s", blogModule, gen)
}
})
t.Run("module_pin_framework", func(t *testing.T) {
app := helloCopy(t)
reg := fakeRegistry(t, map[string]fakePlugin{
"acme.blog": regPlugin("acme.blog", "https://example.com/acme/blog.git", blogV2, blogV1),
})
before := manifestIDs(t, app)
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog@v0.2.0", Yes: true, Registry: reg.Client})
if err == nil || !strings.Contains(err.Error(), "v9.0.0") {
t.Fatalf("pin to v0.2.0: err = %v, want one naming framework v9.0.0", err)
}
if got := manifestIDs(t, app); strings.Join(got, ",") != strings.Join(before, ",") {
t.Fatalf("summer.yaml changed: %v", got)
}
})
t.Run("append_manifest", func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, manifestName)
cases := []struct {
name, in, want string
}{
{
name: "indented",
in: "# app\nmodule: example.com/app\nbinary: app\n\n# the plugins\nplugins:\n - id: a.one\n module: example.com/one # first\n\n # trailing note\nbuild:\n tags: [x]\n",
want: "# app\nmodule: example.com/app\nbinary: app\n\n# the plugins\nplugins:\n - id: a.one\n module: example.com/one # first\n - id: acme.blog\n module: example.com/acme/blog\n\n # trailing note\nbuild:\n tags: [x]\n",
},
{
name: "flush",
in: "module: example.com/app\nbinary: app\nplugins:\n- id: a.one\n module: example.com/one\n",
want: "module: example.com/app\nbinary: app\nplugins:\n- id: a.one\n module: example.com/one\n- id: acme.blog\n module: example.com/acme/blog\n",
},
{
name: "empty",
in: "module: example.com/app\nbinary: app\nplugins: []\n# end\n",
want: "module: example.com/app\nbinary: app\nplugins:\n - id: acme.blog\n module: example.com/acme/blog\n# end\n",
},
{
name: "absent",
in: "module: example.com/app\nbinary: app",
want: "module: example.com/app\nbinary: app\nplugins:\n - id: acme.blog\n module: example.com/acme/blog\n",
},
}
for _, c := range cases {
write(t, path, c.in)
if err := appendManifestPlugins(path, []Plugin{{ID: "acme.blog", Module: blogModule}}); err != nil {
t.Fatalf("%s: %v", c.name, err)
}
got, _ := os.ReadFile(path)
if string(got) != c.want {
t.Fatalf("%s:\n got %q\nwant %q", c.name, got, c.want)
}
}
inline := "module: example.com/app\nbinary: app\nplugins: [{id: a.one, module: example.com/one}]\n"
write(t, path, inline)
if err := appendManifestPlugins(path, []Plugin{{ID: "acme.blog", Module: blogModule}}); err == nil {
t.Fatal("inline plugins list: want an error")
}
if got, _ := os.ReadFile(path); string(got) != inline {
t.Fatalf("inline list was changed: %q", got)
}
dup := "module: example.com/app\nbinary: app\nplugins:\n - id: acme.blog\n module: example.com/other\n"
write(t, path, dup)
if err := appendManifestPlugins(path, []Plugin{{ID: "acme.blog", Module: blogModule}}); err == nil {
t.Fatal("duplicate ID: want an error")
}
if got, _ := os.ReadFile(path); string(got) != dup {
t.Fatalf("summer.yaml not restored: %q", got)
}
})
t.Run("registry_client", func(t *testing.T) {
reg := fakeRegistry(t, map[string]fakePlugin{
"acme.blog": regPlugin("acme.blog", "https://example.com/acme/blog.git", blogV2, blogV1),
})
vs, err := reg.Client.Versions(t.Context(), "acme.blog")
if err != nil || len(vs) != 2 || vs[0].Version != "v0.2.0" {
t.Fatalf("versions = %+v, %v", vs, err)
}
_, err = reg.Client.Versions(t.Context(), "acme.nope")
if !errors.Is(err, kiosk.ErrNotFound) || !strings.Contains(err.Error(), reg.URL) {
t.Fatalf("unknown id: err = %v", err)
}
list, err := reg.Client.List(t.Context(), "blog", "")
if err != nil || list.Meta.Total != 1 {
t.Fatalf("list = %+v, %v", list, err)
}
if _, err := kiosk.NewClient("", nil); !errors.Is(err, kiosk.ErrRegistryDisabled) {
t.Fatalf("empty URL: err = %v", err)
}
for _, bad := range []string{"ftp://x", "https://user:pw@x", "https://x/?q=1", "x"} {
if _, err := kiosk.NewClient(bad, nil); err == nil {
t.Fatalf("NewClient(%q) accepted", bad)
}
}
})
}