package build import ( "os" "path/filepath" "strings" "testing" ) // TestAppendManifestPlugins pins the in-place summer.yaml edit of // plugin:add and plugin:install: comments, blank lines and key order stay, // new items take the existing indentation, a missing plugins block is // appended, and an edit that does not parse back leaves the file as it was. func TestAppendManifestPlugins(t *testing.T) { add := []Plugin{{ID: "acme.shop", Module: "example.test/sm-shop-plugin"}, {ID: "acme.pgsubs", Module: "example.test/pg/subs"}} cases := []struct { name, src, want string }{ { name: "comments, blank lines and later keys stay", src: "# The application.\nmodule: example.test/app # app module\nbinary: app\n\nplugins:\n" + " # Core plugins.\n - id: acme.user\n module: example.test/sm-user-plugin # pinned\n\n" + " - id: acme.blog\n module: example.test/sm-blog-plugin\n\n# Build tags.\ntags:\n - summercms_mcp\n", want: "# The application.\nmodule: example.test/app # app module\nbinary: app\n\nplugins:\n" + " # Core plugins.\n - id: acme.user\n module: example.test/sm-user-plugin # pinned\n\n" + " - id: acme.blog\n module: example.test/sm-blog-plugin\n" + " - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n" + "\n# Build tags.\ntags:\n - summercms_mcp\n", }, { name: "indentation is matched", src: "module: example.test/app\nbinary: app\nplugins:\n- id: acme.user\n module: example.test/sm-user-plugin\n", want: "module: example.test/app\nbinary: app\nplugins:\n- id: acme.user\n module: example.test/sm-user-plugin\n" + "- id: acme.shop\n module: example.test/sm-shop-plugin\n- id: acme.pgsubs\n module: example.test/pg/subs\n", }, { name: "four-space indentation", src: "module: example.test/app\nbinary: app\nplugins:\n - id: acme.user\n module: example.test/sm-user-plugin\n", want: "module: example.test/app\nbinary: app\nplugins:\n - id: acme.user\n module: example.test/sm-user-plugin\n" + " - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n", }, { name: "dash alone on its line", src: "module: example.test/app\nbinary: app\nplugins:\n -\n id: acme.user\n module: example.test/sm-user-plugin\n", want: "module: example.test/app\nbinary: app\nplugins:\n -\n id: acme.user\n module: example.test/sm-user-plugin\n" + " - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n", }, { name: "missing plugins block is appended", src: "# App.\nmodule: example.test/app\nbinary: app", want: "# App.\nmodule: example.test/app\nbinary: app\nplugins:\n" + " - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n", }, { name: "inline list becomes a block with the new items", src: "# App.\nmodule: example.test/app\nbinary: app\nplugins: [{id: acme.user, module: example.test/u}, {id: acme.blog, module: \"example.test/b\"}] # core\n# Tags.\ntags: []\n", want: "# App.\nmodule: example.test/app\nbinary: app\nplugins:\n" + " - id: acme.user\n module: example.test/u\n - id: acme.blog\n module: example.test/b\n" + " - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n" + "# Tags.\ntags: []\n", }, { name: "inline list over several lines", src: "module: example.test/app\nbinary: app\nplugins: [\n {id: acme.user, module: example.test/u}, # first\n {id: acme.blog,\n module: example.test/b},\n]\ntags: []\n", want: "module: example.test/app\nbinary: app\nplugins:\n" + " - id: acme.user\n module: example.test/u\n - id: acme.blog\n module: example.test/b\n" + " - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n" + "tags: []\n", }, { name: "empty inline list becomes a block", src: "module: example.test/app\nbinary: app\nplugins: [] # none yet\ntags: []\n", want: "module: example.test/app\nbinary: app\nplugins:\n" + " - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n" + "tags: []\n", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { path := filepath.Join(t.TempDir(), manifestName) write(t, path, c.src) if _, err := appendManifestPlugins(path, add); err != nil { t.Fatalf("appendManifestPlugins: %v", err) } got, _ := os.ReadFile(path) if string(got) != c.want { t.Errorf("summer.yaml =\n%s\nwant\n%s", got, c.want) } m, err := LoadManifest(path) if err != nil { t.Fatal(err) } n := len(m.Plugins) if n < 2 || m.Plugins[n-2] != add[0] || m.Plugins[n-1] != add[1] { t.Errorf("plugins = %+v", m.Plugins) } }) } t.Run("refusals leave the file unchanged", func(t *testing.T) { refusals := []struct { name, src string plugins []Plugin want string }{ {"bad id", "module: example.test/app\nbinary: app\n", []Plugin{{ID: "Acme", Module: "example.test/x"}}, "plugins[0]"}, {"bad module", "module: example.test/app\nbinary: app\n", []Plugin{{ID: "acme.x", Module: "-x"}}, "module"}, {"broken file", "module: [\n", add, ""}, // A second YAML document after the plugins block: the items // land in it, the first document does not gain them, and the // original bytes are restored. {"edit does not parse back", "module: example.test/app\nbinary: app\nplugins:\n- id: acme.user\n module: example.test/sm-user-plugin\n---\nother: 1\n", add, "left unchanged"}, } for _, r := range refusals { path := filepath.Join(t.TempDir(), manifestName) write(t, path, r.src) _, err := appendManifestPlugins(path, r.plugins) if err == nil || !strings.Contains(err.Error(), r.want) { t.Errorf("%s: err = %v, want %q", r.name, err, r.want) } if got, _ := os.ReadFile(path); string(got) != r.src { t.Errorf("%s: file changed:\n%s", r.name, got) } } }) t.Run("an inline list it cannot edit is written again", func(t *testing.T) { // A tagged flow list: the text edit gives up and the manifest is // written again, as plugin:add did before it kept comments. path := filepath.Join(t.TempDir(), manifestName) write(t, path, "# gone\nmodule: example.test/app\nbinary: app\nplugins: !!seq [{id: acme.user, module: example.test/u}]\n") if _, err := LoadManifest(path); err != nil { t.Fatalf("fixture does not parse: %v", err) } rewrote, err := appendManifestPlugins(path, add) if err != nil || !rewrote { t.Fatalf("rewrote %v, err %v", rewrote, err) } m, err := LoadManifest(path) if err != nil || len(m.Plugins) != 3 || m.Plugins[0].ID != "acme.user" || m.Plugins[2] != add[1] { t.Errorf("plugins = %+v, %v", m.Plugins, err) } }) t.Run("nothing to add", func(t *testing.T) { path := filepath.Join(t.TempDir(), manifestName) src := "module: example.test/app\nbinary: app\n" write(t, path, src) if _, err := appendManifestPlugins(path, nil); err != nil { t.Fatal(err) } if got, _ := os.ReadFile(path); string(got) != src { t.Errorf("file changed:\n%s", got) } }) t.Run("missing file", func(t *testing.T) { if _, err := appendManifestPlugins(filepath.Join(t.TempDir(), manifestName), add); err == nil { t.Error("appendManifestPlugins on a missing file succeeded") } }) }