- module installs move to the newest compatible registry release with one go get; submodules fetch tags, check out the release tag and require it in go.mod; go.work overrides and version pins are refreshed - unlisted submodules (git URL installs) update from their own tags and plugin.yaml; --offline restricts to that - without an ID every plugin that resolves is updated; in-app, unlisted and incompatible ones are reported; failure only when a resolved update cannot be carried out - same prompt rule as plugin:install
923 lines
38 KiB
Go
923 lines
38 KiB
Go
package build
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
"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
|
|
}
|
|
|
|
// fullError is the error text plus a bonfire hint's explanation and Try
|
|
// line, where the hints name commands and flags.
|
|
func fullError(err error) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
text := err.Error()
|
|
var h *bonfire.HintError
|
|
if errors.As(err, &h) {
|
|
text += "\n" + h.Explanation + "\n" + h.Try
|
|
}
|
|
return text
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
|
|
translateModule := "example.com/acme/translate"
|
|
depsRegistry := func(t *testing.T) *fakeReg {
|
|
return fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.blog": regPlugin("acme.blog", "https://example.com/acme/blog.git",
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, kiosk.Requires{"acme.translate": "v0.1.0"})),
|
|
"acme.translate": regPlugin("acme.translate", "https://example.com/acme/translate.git",
|
|
regVersion("v0.2.0", "v9.0.0", translateModule, translateModule, nil),
|
|
regVersion("v0.1.1", "v0.5.0", translateModule, translateModule, nil),
|
|
regVersion("v0.1.0", "v0.5.0", translateModule, translateModule, nil),
|
|
regVersion("v0.0.9", "v0.5.0", translateModule, translateModule, nil)),
|
|
})
|
|
}
|
|
stepList := func(p *InstallPlan) string {
|
|
var parts []string
|
|
for _, s := range p.Steps {
|
|
parts = append(parts, s.ID+"@"+s.Version+"/"+s.Mode)
|
|
}
|
|
return strings.Join(parts, ",")
|
|
}
|
|
|
|
t.Run("deps", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
reg := depsRegistry(t)
|
|
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 got := stepList(plan); got != "acme.translate@v0.1.1/module,acme.blog@v0.1.0/module" {
|
|
t.Fatalf("steps = %s", got)
|
|
}
|
|
if !strings.Contains(out.String(), "acme.translate@v0.1.1 (module)\n acme.blog@v0.1.0 (module)") {
|
|
t.Fatalf("summary:\n%s", out.String())
|
|
}
|
|
if testing.Short() {
|
|
return
|
|
}
|
|
proxy := t.TempDir()
|
|
writeProxyModule(t, proxy, blogModule, "v0.1.0", fixturePluginFiles(blogModule, "blog", "acme.blog", "v0.5.0", translateModule+" v0.1.0"))
|
|
writeProxyModule(t, proxy, translateModule, "v0.1.0", fixturePluginFiles(translateModule, "translate", "acme.translate", "v0.5.0"))
|
|
writeProxyModule(t, proxy, translateModule, "v0.1.1", fixturePluginFiles(translateModule, "translate", "acme.translate", "v0.5.0"))
|
|
offlineGoEnv(t, proxy)
|
|
if err := ApplyInstall(t.Context(), plan, &out); err != nil {
|
|
t.Fatalf("apply: %v\n%s", err, out.String())
|
|
}
|
|
if n := strings.Count(out.String(), "go get "); n != 1 {
|
|
t.Fatalf("want one go get, got %d:\n%s", n, out.String())
|
|
}
|
|
gomod, _ := os.ReadFile(filepath.Join(app, "go.mod"))
|
|
for _, want := range []string{blogModule + " v0.1.0", translateModule + " v0.1.1"} {
|
|
if !bytes.Contains(gomod, []byte(want)) {
|
|
t.Fatalf("go.mod lacks %s:\n%s", want, gomod)
|
|
}
|
|
}
|
|
ids := manifestIDs(t, app)
|
|
if got := strings.Join(ids[len(ids)-2:], ","); got != "acme.translate="+translateModule+",acme.blog="+blogModule {
|
|
t.Fatalf("summer.yaml order = %v", ids)
|
|
}
|
|
})
|
|
|
|
t.Run("installed_old", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
if err := appendManifestPlugins(filepath.Join(app, manifestName), []Plugin{{ID: "acme.translate", Module: translateModule}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runGo(t.Context(), app, "mod", "edit", "-require="+translateModule+"@v0.0.9"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reg := depsRegistry(t)
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "v0.0.9") || !strings.Contains(fullError(err), "summer plugin:update acme.translate") {
|
|
t.Fatalf("err = %v (%s), want installed v0.0.9 too old and a plugin:update hint", err, fullError(err))
|
|
}
|
|
// At the minimum it is fine, and only blog is planned.
|
|
if err := runGo(t.Context(), app, "mod", "edit", "-require="+translateModule+"@v0.1.0"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Registry: reg.Client})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := stepList(plan); got != "acme.blog@v0.1.0/module" {
|
|
t.Fatalf("steps = %s", got)
|
|
}
|
|
})
|
|
|
|
t.Run("cycle", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
reg := fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.one": regPlugin("acme.one", "", regVersion("v0.1.0", "v0.5.0", "example.com/acme/one", "example.com/acme/one", kiosk.Requires{"acme.two": "v0.1.0"})),
|
|
"acme.two": regPlugin("acme.two", "", regVersion("v0.1.0", "v0.5.0", "example.com/acme/two", "example.com/acme/two", kiosk.Requires{"acme.one": "v0.1.0"})),
|
|
})
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.one", Yes: true, Registry: reg.Client})
|
|
done <- err
|
|
}()
|
|
err := <-done
|
|
if err == nil || !strings.Contains(err.Error(), "cycle") || !strings.Contains(err.Error(), "acme.one -> acme.two -> acme.one") {
|
|
t.Fatalf("err = %v, want a reported cycle", err)
|
|
}
|
|
})
|
|
|
|
t.Run("pin", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
reg := depsRegistry(t)
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.translate@v0.1.0", Yes: true, Registry: reg.Client})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := stepList(plan); got != "acme.translate@v0.1.0/module" {
|
|
t.Fatalf("steps = %s", got)
|
|
}
|
|
_, err = PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.translate@v0.7.0", Yes: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "v0.7.0") || !strings.Contains(err.Error(), "v0.2.0, v0.1.1, v0.1.0, v0.0.9") {
|
|
t.Fatalf("unknown pin: err = %v, want the available versions listed", err)
|
|
}
|
|
for _, bad := range []string{"acme.translate@latest", "acme.translate@v1.0", "Acme.Translate", "acme.translate@-v1.0.0"} {
|
|
if _, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: bad, Yes: true, Registry: reg.Client}); err == nil {
|
|
t.Fatalf("%s: want an error", bad)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("unknown", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
reg := depsRegistry(t)
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.nope", Yes: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "not in the registry") || !strings.Contains(err.Error(), reg.URL) {
|
|
t.Fatalf("err = %v, want not in the registry at %s", err, reg.URL)
|
|
}
|
|
})
|
|
|
|
t.Run("prompt", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
reg := depsRegistry(t)
|
|
before, _ := os.ReadFile(filepath.Join(app, manifestName))
|
|
var out bytes.Buffer
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Registry: reg.Client, Out: &out})
|
|
if err == nil || !strings.Contains(err.Error(), "--yes") {
|
|
t.Fatalf("non-interactive without --yes: err = %v", err)
|
|
}
|
|
if !strings.Contains(out.String(), "acme.blog@v0.1.0 (module)") {
|
|
t.Fatalf("plan not printed before the error:\n%s", out.String())
|
|
}
|
|
asked := ""
|
|
_, err = PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Registry: reg.Client, Interactive: true,
|
|
Confirm: func(q string) (bool, error) { asked = q; return false, nil }})
|
|
if !errors.Is(err, ErrCancelled) || asked == "" {
|
|
t.Fatalf("declined prompt: err = %v, asked %q", err, asked)
|
|
}
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Registry: reg.Client, Interactive: true,
|
|
Confirm: func(string) (bool, error) { return true, nil }})
|
|
if err != nil || len(plan.Steps) != 2 {
|
|
t.Fatalf("accepted prompt: plan %+v, err %v", plan, err)
|
|
}
|
|
after, _ := os.ReadFile(filepath.Join(app, manifestName))
|
|
if !bytes.Equal(before, after) {
|
|
t.Fatal("planning changed summer.yaml")
|
|
}
|
|
})
|
|
|
|
t.Run("offline", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
reg := depsRegistry(t)
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Offline: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "--offline") || !strings.Contains(fullError(err), "git") {
|
|
t.Fatalf("offline: err = %v (%s)", err, fullError(err))
|
|
}
|
|
if n := len(reg.Requests()); n != 0 {
|
|
t.Fatalf("offline made %d registry requests", n)
|
|
}
|
|
_, err = PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true})
|
|
if err == nil || !strings.Contains(err.Error(), "plugins.registry_url") || !strings.Contains(fullError(err), "git") {
|
|
t.Fatalf("disabled registry: err = %v (%s)", err, fullError(err))
|
|
}
|
|
})
|
|
|
|
t.Run("invalid", func(t *testing.T) {
|
|
app := helloCopy(t)
|
|
for name, v := range map[string]kiosk.RegistryVersion{
|
|
"dash module": regVersion("v0.1.0", "v0.5.0", "-modfile=/tmp/x", "-modfile=/tmp/x", nil),
|
|
"bad version": regVersion("v0.1", "v0.5.0", blogModule, blogModule, nil),
|
|
"outside": regVersion("v0.1.0", "v0.5.0", blogModule, "example.com/other/pkg", nil),
|
|
"bad requireid": regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, kiosk.Requires{"-x": "v0.1.0"}),
|
|
} {
|
|
reg := fakeRegistry(t, map[string]fakePlugin{"acme.blog": regPlugin("acme.blog", "", v)})
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Registry: reg.Client})
|
|
if err == nil {
|
|
t.Fatalf("%s: accepted, plan %+v", name, plan.Steps)
|
|
}
|
|
if name == "bad version" {
|
|
if !strings.Contains(err.Error(), "no release") && !strings.Contains(err.Error(), "refused") {
|
|
t.Fatalf("%s: err = %v", name, err)
|
|
}
|
|
continue
|
|
}
|
|
if !strings.Contains(err.Error(), "refused") {
|
|
t.Fatalf("%s: err = %v, want refused", name, err)
|
|
}
|
|
}
|
|
// ApplyInstall checks again, so a hand-built plan cannot reach go.
|
|
bad := &InstallPlan{AppDir: app, Steps: []InstallStep{{ID: "acme.blog", Version: "v0.1.0", Module: "-x", Package: "-x", Mode: ModeModule}}}
|
|
if err := ApplyInstall(t.Context(), bad, nil); err == nil || !strings.Contains(err.Error(), "refused") {
|
|
t.Fatalf("apply: err = %v", err)
|
|
}
|
|
})
|
|
|
|
// Source installs: fixture repositories over file://, submodules in a
|
|
// git copy of examples/hello, builds with GOPROXY=off.
|
|
translateReq := map[string][2]string{"acme.translate": {translateModule, "v0.1.0"}}
|
|
sourceRegistry := func(t *testing.T, blogRepo, translateRepo string) *fakeReg {
|
|
return fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.blog": regPlugin("acme.blog", blogRepo,
|
|
regVersion("v0.2.0", "v9.0.0", blogModule, blogModule, kiosk.Requires{"acme.translate": "v0.1.0"}),
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, kiosk.Requires{"acme.translate": "v0.1.0"})),
|
|
"acme.translate": regPlugin("acme.translate", translateRepo,
|
|
regVersion("v0.1.0", "v0.5.0", translateModule, translateModule, nil)),
|
|
})
|
|
}
|
|
assertSubmodule := func(t *testing.T, app, rel, tag string) {
|
|
t.Helper()
|
|
gm, err := os.ReadFile(filepath.Join(app, ".gitmodules"))
|
|
if err != nil || !bytes.Contains(gm, []byte("path = "+rel)) {
|
|
t.Fatalf(".gitmodules lacks %s: %v\n%s", rel, err, gm)
|
|
}
|
|
if status := gitOut(t, app, "submodule", "status", "--", rel); !strings.Contains(status, "("+tag+")") {
|
|
t.Fatalf("submodule status = %q, want %s", status, tag)
|
|
}
|
|
if st, err := os.Stat(filepath.Join(app, rel, ".git")); err != nil || st.IsDir() {
|
|
t.Fatalf("%s/.git should be a file after absorbgitdirs: %v", rel, err)
|
|
}
|
|
}
|
|
buildApp := func(t *testing.T, app string) {
|
|
t.Helper()
|
|
var log bytes.Buffer
|
|
if err := App(t.Context(), app, &log); err != nil {
|
|
work, _ := os.ReadFile(filepath.Join(app, "go.work"))
|
|
t.Fatalf("build: %v\n%s\ngo.work:\n%s", err, log.String(), work)
|
|
}
|
|
}
|
|
|
|
t.Run("source", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories and builds the app")
|
|
}
|
|
gitTestEnv(t)
|
|
blogRepo := pluginRepo(t, "acme.blog", blogModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0"}, {Tag: "v0.2.0", Framework: "v9.0.0"}}, nil)
|
|
app := gitApp(t)
|
|
plantComment(t, app)
|
|
reg := fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.blog": regPlugin("acme.blog", blogRepo,
|
|
regVersion("v0.2.0", "v9.0.0", blogModule, blogModule, nil),
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)),
|
|
})
|
|
var out bytes.Buffer
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Source: true, Yes: true, Registry: reg.Client, Out: &out})
|
|
if err != nil {
|
|
t.Fatalf("plan: %v\n%s", err, out.String())
|
|
}
|
|
if got := stepList(plan); got != "acme.blog@v0.1.0/submodule" {
|
|
t.Fatalf("steps = %s", got)
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, &out); err != nil {
|
|
t.Fatalf("apply: %v\n%s", err, out.String())
|
|
}
|
|
assertSubmodule(t, app, "plugins/acme/blog", "v0.1.0")
|
|
gomod, _ := os.ReadFile(filepath.Join(app, "go.mod"))
|
|
if !bytes.Contains(gomod, []byte(blogModule+" v0.1.0")) || !bytes.Contains(gomod, []byte(blogModule+" => ./plugins/acme/blog")) {
|
|
t.Fatalf("go.mod lacks the require at the tag or the local replace:\n%s", gomod)
|
|
}
|
|
work, _ := os.ReadFile(filepath.Join(app, "go.work"))
|
|
if !bytes.Contains(work, []byte("./plugins/acme/blog")) || !bytes.Contains(work, []byte("replace "+frameworkModule)) {
|
|
t.Fatalf("go.work lacks the use or the framework override:\n%s", work)
|
|
}
|
|
assertManifestKeepsComment(t, app)
|
|
if len(installTemps(t, app)) != 0 {
|
|
t.Fatalf("leftover checkouts: %v", installTemps(t, app))
|
|
}
|
|
buildApp(t, app)
|
|
})
|
|
|
|
t.Run("source_deps", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories and builds the app")
|
|
}
|
|
gitTestEnv(t)
|
|
translateRepo := pluginRepo(t, "acme.translate", translateModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0"}}, nil)
|
|
blogRepo := pluginRepo(t, "acme.blog", blogModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0", Requires: translateReq}}, nil)
|
|
app := gitApp(t)
|
|
reg := sourceRegistry(t, blogRepo, translateRepo)
|
|
var out bytes.Buffer
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Source: true, Yes: true, Registry: reg.Client, Out: &out})
|
|
if err != nil {
|
|
t.Fatalf("plan: %v\n%s", err, out.String())
|
|
}
|
|
if got := stepList(plan); got != "acme.translate@v0.1.0/submodule,acme.blog@v0.1.0/submodule" {
|
|
t.Fatalf("steps = %s", got)
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, &out); err != nil {
|
|
t.Fatalf("apply: %v\n%s", err, out.String())
|
|
}
|
|
assertSubmodule(t, app, "plugins/acme/translate", "v0.1.0")
|
|
assertSubmodule(t, app, "plugins/acme/blog", "v0.1.0")
|
|
work, _ := os.ReadFile(filepath.Join(app, "go.work"))
|
|
if !bytes.Contains(work, []byte(translateModule+" v0.1.0 => ./plugins/acme/translate")) {
|
|
t.Fatalf("go.work lacks the versioned override for the workspace module:\n%s", work)
|
|
}
|
|
buildApp(t, app)
|
|
})
|
|
|
|
t.Run("giturl", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories and builds the app")
|
|
}
|
|
gitTestEnv(t)
|
|
translateRepo := pluginRepo(t, "acme.translate", translateModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0"}}, nil)
|
|
blogRepo := pluginRepo(t, "acme.blog", blogModule, []fixtureRelease{
|
|
{Tag: "v0.1.0", Framework: "v0.5.0", Requires: translateReq},
|
|
{Tag: "v0.2.0", Framework: "v9.0.0", Requires: translateReq},
|
|
}, nil)
|
|
app := gitApp(t)
|
|
reg := fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.translate": regPlugin("acme.translate", translateRepo, regVersion("v0.1.0", "v0.5.0", translateModule, translateModule, nil)),
|
|
})
|
|
var out bytes.Buffer
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: blogRepo, Yes: true, Registry: reg.Client, Out: &out})
|
|
if err != nil {
|
|
t.Fatalf("plan: %v\n%s", err, out.String())
|
|
}
|
|
if got := stepList(plan); got != "acme.translate@v0.1.0/submodule,acme.blog@v0.1.0/submodule" {
|
|
t.Fatalf("steps = %s", got)
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, &out); err != nil {
|
|
t.Fatalf("apply: %v\n%s", err, out.String())
|
|
}
|
|
for _, r := range reg.Requests() {
|
|
if strings.Contains(r, "acme.blog") {
|
|
t.Fatalf("the git URL plugin was looked up in the registry: %v", reg.Requests())
|
|
}
|
|
}
|
|
assertSubmodule(t, app, "plugins/acme/blog", "v0.1.0")
|
|
buildApp(t, app)
|
|
|
|
// No tag fits: the default branch head is planned with a warning,
|
|
// and a declined prompt leaves no checkout behind.
|
|
headRepo := pluginRepo(t, "acme.news", "example.com/acme/news", []fixtureRelease{{Tag: "v0.1.0", Framework: "v9.0.0"}}, &fixtureRelease{Tag: "head", Framework: "v9.0.0"})
|
|
out.Reset()
|
|
_, err = PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: headRepo, Registry: reg.Client, Out: &out, Interactive: true,
|
|
Confirm: func(string) (bool, error) { return false, nil }})
|
|
if !errors.Is(err, ErrCancelled) {
|
|
t.Fatalf("head plan: err = %v\n%s", err, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "warning: no release tag fits framework") || !strings.Contains(out.String(), "acme.news@default branch head (submodule)") {
|
|
t.Fatalf("head plan output:\n%s", out.String())
|
|
}
|
|
if left := installTemps(t, app); len(left) != 0 {
|
|
t.Fatalf("declined plan left checkouts: %v", left)
|
|
}
|
|
})
|
|
|
|
t.Run("giturl_offline", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories")
|
|
}
|
|
gitTestEnv(t)
|
|
blogRepo := pluginRepo(t, "acme.blog", blogModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0", Requires: translateReq}}, nil)
|
|
app := gitApp(t)
|
|
reg := fakeRegistry(t, map[string]fakePlugin{})
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: blogRepo, Yes: true, Offline: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "acme.translate") || !strings.Contains(err.Error(), "--offline") {
|
|
t.Fatalf("missing requirement offline: err = %v", err)
|
|
}
|
|
if left := installTemps(t, app); len(left) != 0 {
|
|
t.Fatalf("failed plan left checkouts: %v", left)
|
|
}
|
|
if err := appendManifestPlugins(filepath.Join(app, manifestName), []Plugin{{ID: "acme.translate", Module: translateModule}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runGo(t.Context(), app, "mod", "edit", "-require="+translateModule+"@v0.1.0"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out bytes.Buffer
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: blogRepo, Yes: true, Offline: true, Registry: reg.Client, Out: &out})
|
|
if err != nil {
|
|
t.Fatalf("plan: %v\n%s", err, out.String())
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, &out); err != nil {
|
|
t.Fatalf("apply: %v\n%s", err, out.String())
|
|
}
|
|
if n := len(reg.Requests()); n != 0 {
|
|
t.Fatalf("offline install made %d registry requests: %v", n, reg.Requests())
|
|
}
|
|
work, _ := os.ReadFile(filepath.Join(app, "go.work"))
|
|
if !bytes.Contains(work, []byte(translateModule+" => "+translateModule+" v0.1.0")) {
|
|
t.Fatalf("go.work lacks the version pin for the replaced requirement:\n%s", work)
|
|
}
|
|
})
|
|
|
|
t.Run("nogit", func(t *testing.T) {
|
|
gitTestEnv(t)
|
|
app := helloCopy(t)
|
|
reg := depsRegistry(t)
|
|
_, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Source: true, Yes: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "not a git repository") || !strings.Contains(err.Error(), "submodule") {
|
|
t.Fatalf("--source outside git: err = %v", err)
|
|
}
|
|
_, err = PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "https://git.example.com/acme/sm-blog-plugin.git", Yes: true, Registry: reg.Client})
|
|
if err == nil || !strings.Contains(err.Error(), "not a git repository") {
|
|
t.Fatalf("git URL outside git: err = %v", err)
|
|
}
|
|
if left := installTemps(t, app); len(left) != 0 {
|
|
t.Fatalf("refused install left checkouts: %v", left)
|
|
}
|
|
})
|
|
|
|
t.Run("urls", func(t *testing.T) {
|
|
accept := []string{
|
|
"https://git.example.com/acme/sm-blog-plugin.git",
|
|
"ssh://git@git.example.com/acme/blog.git",
|
|
"git@git.example.com:acme/blog.git",
|
|
}
|
|
refuse := []string{
|
|
"-uhttps://git.example.com/x.git",
|
|
"--upload-pack=touch /tmp/x",
|
|
"ext::sh -c touch% /tmp/pwned",
|
|
"file::/tmp/x",
|
|
"file:///tmp/repo",
|
|
"http://git.example.com/acme/blog.git",
|
|
"git://git.example.com/acme/blog.git",
|
|
"https://user:secret@git.example.com/acme/blog.git",
|
|
"https://git.example.com/-x",
|
|
"git@git.example.com:-oProxyCommand=x",
|
|
"https://git.example.com/a b.git",
|
|
}
|
|
for _, u := range accept {
|
|
if !isGitURL(u) {
|
|
t.Errorf("isGitURL(%q) = false", u)
|
|
}
|
|
if err := validateGitURL(u); err != nil {
|
|
t.Errorf("validateGitURL(%q) = %v", u, err)
|
|
}
|
|
}
|
|
for _, u := range refuse {
|
|
if err := validateGitURL(u); err == nil {
|
|
t.Errorf("validateGitURL(%q) accepted", u)
|
|
}
|
|
}
|
|
for _, id := range []string{"acme.blog", "acme.blog@v0.1.0"} {
|
|
if isGitURL(id) {
|
|
t.Errorf("isGitURL(%q) = true", id)
|
|
}
|
|
}
|
|
if err := validateRepositoryURL("acme.blog", "ssh://git@git.example.com/acme/blog.git"); err == nil {
|
|
t.Error("registry repository over ssh accepted")
|
|
}
|
|
if err := validateRepositoryURL("acme.blog", "https://git.example.com/acme/blog.git"); err != nil {
|
|
t.Errorf("registry https repository: %v", err)
|
|
}
|
|
old := cliGitProtocols
|
|
cliGitProtocols = "https:ssh:file"
|
|
defer func() { cliGitProtocols = old }()
|
|
if err := validateGitURL("file:///tmp/repo"); err != nil {
|
|
t.Errorf("file:// with the test protocols: %v", err)
|
|
}
|
|
if err := validateGitURL("file::/tmp/repo"); err == nil {
|
|
t.Error("file:: accepted with the test protocols")
|
|
}
|
|
})
|
|
|
|
// plugin:update. tagRelease adds a tagged release to a fixture repo.
|
|
tagRelease := func(t *testing.T, repoURL, id, module string, rel fixtureRelease) {
|
|
t.Helper()
|
|
dir := strings.TrimPrefix(repoURL, "file://")
|
|
for name, body := range pluginRepoFiles(t, id, module, rel) {
|
|
write(t, filepath.Join(dir, name), body)
|
|
}
|
|
write(t, filepath.Join(dir, "CHANGES"), "release "+rel.Tag+"\n")
|
|
runTestGit(t, dir, "add", "-A")
|
|
runTestGit(t, dir, "commit", "--quiet", "-m", "release "+rel.Tag)
|
|
runTestGit(t, dir, "tag", rel.Tag)
|
|
}
|
|
requireOf := func(t *testing.T, app, module string) string {
|
|
t.Helper()
|
|
gomod, err := editJSON(t.Context(), app, "mod", "edit", "-json", filepath.Join(app, "go.mod"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, r := range gomod.Require {
|
|
if r.Path == module {
|
|
return r.Version
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
// sourceApp is a git app with acme.blog v0.1.0 installed with --source.
|
|
sourceApp := func(t *testing.T) (app, blogRepo string, reg *fakeReg) {
|
|
t.Helper()
|
|
blogRepo = pluginRepo(t, "acme.blog", blogModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0"}}, nil)
|
|
app = gitApp(t)
|
|
reg = fakeRegistry(t, map[string]fakePlugin{
|
|
"acme.blog": regPlugin("acme.blog", blogRepo, regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)),
|
|
})
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Source: true, Yes: true, Registry: reg.Client})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return app, blogRepo, reg
|
|
}
|
|
|
|
t.Run("update", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("installs from a file:// module proxy")
|
|
}
|
|
app := helloCopy(t)
|
|
proxy := t.TempDir()
|
|
for _, v := range []string{"v0.1.0", "v0.1.1"} {
|
|
writeProxyModule(t, proxy, blogModule, v, fixturePluginFiles(blogModule, "blog", "acme.blog", "v0.5.0"))
|
|
}
|
|
offlineGoEnv(t, proxy)
|
|
reg := fakeRegistry(t, map[string]fakePlugin{"acme.blog": regPlugin("acme.blog", "", blogV1)})
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: "acme.blog", Yes: true, Registry: reg.Client})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reg.set("acme.blog", regPlugin("acme.blog", "",
|
|
regVersion("v0.3.0", "v9.0.0", blogModule, blogModule, nil),
|
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil),
|
|
blogV1))
|
|
var out bytes.Buffer
|
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Yes: true, Registry: reg.Client, Out: &out}); err != nil {
|
|
t.Fatalf("update: %v\n%s", err, out.String())
|
|
}
|
|
if got := requireOf(t, app, blogModule); got != "v0.1.1" {
|
|
t.Fatalf("go.mod requires %s %s, want v0.1.1\n%s", blogModule, got, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "acme.blog v0.1.0 -> v0.1.1 (module)") {
|
|
t.Fatalf("update output:\n%s", out.String())
|
|
}
|
|
out.Reset()
|
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Yes: true, Registry: reg.Client, Out: &out}); err != nil {
|
|
t.Fatalf("second update: %v\n%s", err, out.String())
|
|
}
|
|
if got := requireOf(t, app, blogModule); got != "v0.1.1" {
|
|
t.Fatalf("second update moved to %s", got)
|
|
}
|
|
if !strings.Contains(out.String(), "v0.3.0 needs framework v9.0.0") {
|
|
t.Fatalf("second update does not report the incompatible release:\n%s", out.String())
|
|
}
|
|
})
|
|
|
|
t.Run("update_submodule", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories and builds the app")
|
|
}
|
|
gitTestEnv(t)
|
|
app, blogRepo, reg := sourceApp(t)
|
|
tagRelease(t, blogRepo, "acme.blog", blogModule, fixtureRelease{Tag: "v0.1.1", Framework: "v0.5.0"})
|
|
reg.set("acme.blog", regPlugin("acme.blog", blogRepo,
|
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil),
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)))
|
|
var out bytes.Buffer
|
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Yes: true, Registry: reg.Client, Out: &out}); err != nil {
|
|
t.Fatalf("update: %v\n%s", err, out.String())
|
|
}
|
|
if got := gitOut(t, filepath.Join(app, "plugins", "acme", "blog"), "describe", "--tags"); got != "v0.1.1" {
|
|
t.Fatalf("submodule at %s, want v0.1.1\n%s", got, out.String())
|
|
}
|
|
if got := requireOf(t, app, blogModule); got != "v0.1.1" {
|
|
t.Fatalf("go.mod requires %s, want v0.1.1", got)
|
|
}
|
|
if !strings.Contains(out.String(), "acme.blog v0.1.0 -> v0.1.1 (submodule)") {
|
|
t.Fatalf("update output:\n%s", out.String())
|
|
}
|
|
buildApp(t, app)
|
|
|
|
// A go.work pin of a module to one of its own versions follows the
|
|
// app's require when that moves.
|
|
if err := runGo(t.Context(), app, "work", "edit", "-replace="+translateModule+"="+translateModule+"@v0.1.0"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runGo(t.Context(), app, "mod", "edit", "-require="+translateModule+"@v0.2.0"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := refreshWorkOverrides(t.Context(), app, &out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
work, _ := os.ReadFile(filepath.Join(app, "go.work"))
|
|
if !bytes.Contains(work, []byte(translateModule+" => "+translateModule+" v0.2.0")) {
|
|
t.Fatalf("pin not refreshed:\n%s", work)
|
|
}
|
|
})
|
|
|
|
t.Run("update_all", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories")
|
|
}
|
|
gitTestEnv(t)
|
|
app, blogRepo, reg := sourceApp(t)
|
|
// acme.translate from a git URL: not in the registry, updated from
|
|
// its own tags.
|
|
translateRepo := pluginRepo(t, "acme.translate", translateModule, []fixtureRelease{{Tag: "v0.1.0", Framework: "v0.5.0"}}, nil)
|
|
plan, err := PlanInstall(t.Context(), InstallOptions{StartDir: app, Arg: translateRepo, Yes: true, Registry: reg.Client})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ApplyInstall(t.Context(), plan, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// acme.ghost: a module install the registry does not list.
|
|
if err := appendManifestPlugins(filepath.Join(app, manifestName), []Plugin{{ID: "acme.ghost", Module: "example.com/acme/ghost"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runGo(t.Context(), app, "mod", "edit", "-require=example.com/acme/ghost@v0.1.0"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tagRelease(t, blogRepo, "acme.blog", blogModule, fixtureRelease{Tag: "v0.1.1", Framework: "v0.5.0"})
|
|
tagRelease(t, translateRepo, "acme.translate", translateModule, fixtureRelease{Tag: "v0.1.1", Framework: "v0.5.0"})
|
|
tagRelease(t, translateRepo, "acme.translate", translateModule, fixtureRelease{Tag: "v0.2.0", Framework: "v9.0.0"})
|
|
reg.set("acme.blog", regPlugin("acme.blog", blogRepo,
|
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil),
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)))
|
|
|
|
var out bytes.Buffer
|
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, Yes: true, Registry: reg.Client, Out: &out}); err != nil {
|
|
t.Fatalf("update all: %v\n%s", err, out.String())
|
|
}
|
|
for _, want := range []string{
|
|
"acme.blog v0.1.0 -> v0.1.1 (submodule)",
|
|
"acme.translate v0.1.0 -> v0.1.1 (submodule)",
|
|
"golem15.hello: ",
|
|
"acme.ghost: ",
|
|
"v0.2.0 needs framework v9.0.0",
|
|
} {
|
|
if !strings.Contains(out.String(), want) {
|
|
t.Fatalf("update all output lacks %q:\n%s", want, out.String())
|
|
}
|
|
}
|
|
if got := gitOut(t, filepath.Join(app, "plugins", "acme", "translate"), "describe", "--tags"); got != "v0.1.1" {
|
|
t.Fatalf("translate at %s, want v0.1.1", got)
|
|
}
|
|
|
|
// A release the registry lists but the repository lacks makes the
|
|
// run fail, naming the plugin; the other plugins are reported.
|
|
reg.set("acme.blog", regPlugin("acme.blog", blogRepo,
|
|
regVersion("v0.1.2", "v0.5.0", blogModule, blogModule, nil),
|
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil)))
|
|
out.Reset()
|
|
err = Update(t.Context(), UpdateOptions{StartDir: app, Yes: true, Registry: reg.Client, Out: &out})
|
|
if err == nil || !strings.Contains(err.Error(), "acme.blog") {
|
|
t.Fatalf("failed update: err = %v\n%s", err, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "acme.ghost: ") {
|
|
t.Fatalf("skipped plugins not reported:\n%s", out.String())
|
|
}
|
|
})
|
|
|
|
t.Run("update_prompt", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("clones fixture repositories")
|
|
}
|
|
gitTestEnv(t)
|
|
app, blogRepo, reg := sourceApp(t)
|
|
tagRelease(t, blogRepo, "acme.blog", blogModule, fixtureRelease{Tag: "v0.1.1", Framework: "v0.5.0"})
|
|
reg.set("acme.blog", regPlugin("acme.blog", blogRepo,
|
|
regVersion("v0.1.1", "v0.5.0", blogModule, blogModule, nil),
|
|
regVersion("v0.1.0", "v0.5.0", blogModule, blogModule, nil)))
|
|
var out bytes.Buffer
|
|
err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Registry: reg.Client, Out: &out})
|
|
if err == nil || !strings.Contains(err.Error(), "--yes") || !strings.Contains(out.String(), "acme.blog v0.1.0 -> v0.1.1") {
|
|
t.Fatalf("non-interactive update: err = %v\n%s", err, out.String())
|
|
}
|
|
err = Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.blog", Registry: reg.Client, Interactive: true,
|
|
Confirm: func(string) (bool, error) { return false, nil }})
|
|
if !errors.Is(err, ErrCancelled) {
|
|
t.Fatalf("declined update: err = %v", err)
|
|
}
|
|
if got := gitOut(t, filepath.Join(app, "plugins", "acme", "blog"), "describe", "--tags"); got != "v0.1.0" {
|
|
t.Fatalf("declined update moved the submodule to %s", got)
|
|
}
|
|
if got := requireOf(t, app, blogModule); got != "v0.1.0" {
|
|
t.Fatalf("declined update changed go.mod to %s", got)
|
|
}
|
|
if err := Update(t.Context(), UpdateOptions{StartDir: app, ID: "acme.nope", Yes: true, Registry: reg.Client}); err == nil || !strings.Contains(err.Error(), "summer.yaml") {
|
|
t.Fatalf("unknown ID: err = %v", err)
|
|
}
|
|
})
|
|
|
|
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)
|
|
}
|
|
}
|
|
})
|
|
}
|