- TestReadIdentity: literal, const, chained, cross-package, alias, other-module and Requires shapes; refusals naming ID()/Requires() - TestDerive, TestManifestFormat, TestCheck, TestVersions, TestRegistryClient (httptest: escaping, 404, envelopes, cap, redirect) - runnable ExampleMarshal and ExampleNewestCompatible - party TestDescribe: copy of the plugin list, publish once per app
253 lines
10 KiB
Go
253 lines
10 KiB
Go
package kiosk
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// derivePlugin writes a literal-ID plugin acme.shop with the given go.mod
|
|
// framework line and README into a temp dir and returns the dir.
|
|
func derivePlugin(t *testing.T, require, readme string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
gomod := "module example.test/sm-shop-plugin\n\ngo 1.27.0\n"
|
|
if require != "" {
|
|
gomod += "\nrequire " + FrameworkModule + " " + require + "\n"
|
|
}
|
|
files := map[string]string{
|
|
"go.mod": gomod,
|
|
"plugin.go": pluginHead + `func (*Plugin) ID() string { return "acme.shop" }`,
|
|
}
|
|
if readme != "" {
|
|
files["README.md"] = readme
|
|
}
|
|
writeTree(t, dir, files)
|
|
return dir
|
|
}
|
|
|
|
// TestDerive covers README extraction, overrides, framework rules and
|
|
// requirement minimums.
|
|
func TestDerive(t *testing.T) {
|
|
requireGo(t)
|
|
ctx := context.Background()
|
|
|
|
t.Run("readme text is flattened", func(t *testing.T) {
|
|
readme := "<!-- badge -->\n\n# The `acme` *Shop*\n\n" +
|
|
"Products, [carts](https://example.test/carts) and **orders**\n" +
|
|
"for `acme` apps, see <https://example.test>.\n\n" +
|
|
"Second paragraph.\n"
|
|
m, warnings, err := Derive(ctx, derivePlugin(t, "v0.6.0", readme), DeriveOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Derive: %v", err)
|
|
}
|
|
if len(warnings) != 0 {
|
|
t.Errorf("warnings = %v", warnings)
|
|
}
|
|
if m.Name != "The acme Shop" {
|
|
t.Errorf("name = %q", m.Name)
|
|
}
|
|
if want := "Products, carts and orders for acme apps, see https://example.test."; m.Description != want {
|
|
t.Errorf("description = %q, want %q", m.Description, want)
|
|
}
|
|
if m.Package != m.Module || m.Module != "example.test/sm-shop-plugin" || m.Framework != "v0.6.0" || m.Schema != SchemaVersion {
|
|
t.Errorf("manifest = %+v", m)
|
|
}
|
|
})
|
|
|
|
t.Run("readme errors", func(t *testing.T) {
|
|
cases := []struct{ name, readme, want string }{
|
|
{"missing", "", "needs a README.md"},
|
|
{"no h1", "## Shop\n\nText.\n", "has no H1"},
|
|
{"empty h1", "#\n\nText.\n", "H1"},
|
|
{"no summary", "# Shop\n\n## Usage\n\nText.\n", "no summary paragraph"},
|
|
{"id as h1", "# acme.shop\n\nText.\n", "--name"},
|
|
}
|
|
for _, c := range cases {
|
|
_, _, err := Derive(ctx, derivePlugin(t, "v0.6.0", c.readme), DeriveOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), c.want) {
|
|
t.Errorf("%s: err = %v, want %q", c.name, err, c.want)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("framework rules", func(t *testing.T) {
|
|
readme := "# Shop\n\nA shop.\n"
|
|
if _, _, err := Derive(ctx, derivePlugin(t, "", readme), DeriveOptions{}); err == nil || !strings.Contains(err.Error(), "does not require "+FrameworkModule) {
|
|
t.Errorf("no framework require: err = %v", err)
|
|
}
|
|
if _, _, err := Derive(ctx, derivePlugin(t, "v0.0.0", readme), DeriveOptions{}); err == nil || !strings.Contains(err.Error(), "not a released version") {
|
|
t.Errorf("framework v0.0.0: err = %v", err)
|
|
}
|
|
pseudo := "v0.6.1-0.20261029101500-abcdefabcdef"
|
|
m, warnings, err := Derive(ctx, derivePlugin(t, pseudo, readme), DeriveOptions{})
|
|
if err != nil {
|
|
t.Fatalf("pseudo framework: %v", err)
|
|
}
|
|
if m.Framework != pseudo || len(warnings) != 1 || !strings.Contains(warnings[0], "pseudo-version") {
|
|
t.Errorf("pseudo framework: framework %q, warnings %v", m.Framework, warnings)
|
|
}
|
|
})
|
|
|
|
t.Run("overrides", func(t *testing.T) {
|
|
dir := derivePlugin(t, "v0.6.0", "# acme.shop\n\nA shop.\n")
|
|
set := func(name, desc *string) (Manifest, error) {
|
|
t.Helper()
|
|
if _, _, err := Write(ctx, dir, DeriveOptions{Name: name, Description: desc}); err != nil {
|
|
return Manifest{}, err
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(dir, FileName))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return Parse(raw)
|
|
}
|
|
str := func(s string) *string { return &s }
|
|
m, err := set(str(" Shop "), str("Products and orders."))
|
|
if err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
if m.Name != "Shop" || m.Description != "Products and orders." || m.Overrides == nil || m.Overrides.Name != "Shop" {
|
|
t.Fatalf("after set = %+v / %+v", m, m.Overrides)
|
|
}
|
|
if m, err = set(nil, nil); err != nil || m.Name != "Shop" || m.Description != "Products and orders." {
|
|
t.Fatalf("keep: %+v, %v", m, err)
|
|
}
|
|
if m, err = set(nil, str("")); err != nil || m.Description != "A shop." || m.Overrides.Description != "" || m.Overrides.Name != "Shop" {
|
|
t.Fatalf("clear description: %+v / %+v, %v", m, m.Overrides, err)
|
|
}
|
|
if _, err = set(str(""), nil); err == nil || !strings.Contains(err.Error(), "--name") {
|
|
t.Fatalf("clearing the name while the H1 is the ID: err = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("corrupt plugin.yaml", func(t *testing.T) {
|
|
dir := derivePlugin(t, "v0.6.0", "# Shop\n\nA shop.\n")
|
|
writeTree(t, dir, map[string]string{FileName: "schema: [\n"})
|
|
if _, _, err := Derive(ctx, dir, DeriveOptions{}); err == nil || !strings.Contains(err.Error(), "delete") {
|
|
t.Errorf("err = %v, want the advice to delete and rerun", err)
|
|
}
|
|
})
|
|
|
|
t.Run("not a plugin directory", func(t *testing.T) {
|
|
if _, _, err := Derive(ctx, filepath.Join(t.TempDir(), "x"), DeriveOptions{}); err == nil || !strings.Contains(err.Error(), "not a plugin directory") {
|
|
t.Errorf("missing dir: err = %v", err)
|
|
}
|
|
dir := t.TempDir()
|
|
writeTree(t, dir, map[string]string{"plugin.go": pluginHead + `func (*Plugin) ID() string { return "acme.x" }`})
|
|
if _, err := moduleRoot(dir); err == nil {
|
|
t.Skip("a go.mod above the temp dir")
|
|
}
|
|
if _, _, err := Derive(ctx, dir, DeriveOptions{}); err == nil || !strings.Contains(err.Error(), "no go.mod") {
|
|
t.Errorf("no go.mod: err = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("minimums", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
fw := "\nrequire " + FrameworkModule + " v0.6.0\n\nreplace " + FrameworkModule + " => ../framework\n"
|
|
writeTree(t, root, map[string]string{
|
|
"framework/go.mod": "module " + FrameworkModule + "\n\ngo 1.27.0\n",
|
|
// A module that requires the framework and holds acme.user.
|
|
"user/go.mod": "module example.test/sm-user-plugin\n\ngo 1.27.0\n" + fw,
|
|
"user/plugin.go": pluginHead + `func (*Plugin) ID() string { return "acme.user" }`,
|
|
"user/README.md": "# User\n\nUsers.\n",
|
|
// A module that does not require the framework: never searched.
|
|
"lib/go.mod": "module example.test/lib\n\ngo 1.27.0\n",
|
|
"lib/plugin.go": pluginHead + `func (*Plugin) ID() string { return "acme.lib" }`,
|
|
"shop/go.mod": "module example.test/sm-shop-plugin\n\ngo 1.27.0\n\nrequire (\n\t" + FrameworkModule + " v0.6.0\n\texample.test/sm-user-plugin v0.1.2\n\texample.test/lib v1.0.0\n)\n\n" +
|
|
"replace " + FrameworkModule + " => ../framework\n\nreplace example.test/sm-user-plugin => ../user\n\nreplace example.test/lib => ../lib\n",
|
|
"shop/plugin.go": pluginHead + `func (*Plugin) ID() string { return "acme.shop" }
|
|
func (*Plugin) Requires() []string { return []string{"acme.user", "acme.shopadmin", "acme.lib", "acme.soft"} }`,
|
|
"shop/README.md": "# Shop\n\nA shop.\n",
|
|
"shop/admin/plugin.go": "package admin\n\ntype Plugin struct{}\n\nfunc (*Plugin) ID() string { return \"acme.shopadmin\" }\n",
|
|
"shop/admin/README.md": "# Shop admin\n\nAdmin.\n",
|
|
"shop/testdata/plugin.go": pluginHead + `func (*Plugin) ID() string { return "acme.soft" }`,
|
|
})
|
|
m, warnings, err := Derive(ctx, filepath.Join(root, "shop"), DeriveOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Derive: %v", err)
|
|
}
|
|
if len(warnings) != 0 {
|
|
t.Errorf("warnings = %v", warnings)
|
|
}
|
|
want := Requires{"acme.user": "v0.1.2", "acme.shopadmin": "", "acme.lib": "", "acme.soft": ""}
|
|
if len(m.Requires) != len(want) {
|
|
t.Fatalf("requires = %v, want %v", m.Requires, want)
|
|
}
|
|
for id, v := range want {
|
|
if got, ok := m.Requires[id]; !ok || got != v {
|
|
t.Errorf("requires[%s] = %q (present %v), want %q", id, got, ok, v)
|
|
}
|
|
}
|
|
|
|
nested, _, err := Derive(ctx, filepath.Join(root, "shop", "admin"), DeriveOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Derive nested: %v", err)
|
|
}
|
|
if nested.Package != "example.test/sm-shop-plugin/admin" || nested.Module != "example.test/sm-shop-plugin" {
|
|
t.Errorf("nested = %+v", nested)
|
|
}
|
|
|
|
// A required plugin module at v0.0.0 is refused, a pseudo-version warns.
|
|
shopMod := filepath.Join(root, "shop", "go.mod")
|
|
orig, _ := os.ReadFile(shopMod)
|
|
for v, want := range map[string]string{"v0.0.0": "not a released version", "v0.1.3-0.20261029101500-abcdefabcdef": ""} {
|
|
data := strings.Replace(string(orig), "example.test/sm-user-plugin v0.1.2", "example.test/sm-user-plugin "+v, 1)
|
|
if err := os.WriteFile(shopMod, []byte(data), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, warnings, err := Derive(ctx, filepath.Join(root, "shop"), DeriveOptions{})
|
|
if want != "" {
|
|
if err == nil || !strings.Contains(err.Error(), want) || !strings.Contains(err.Error(), "acme.user") {
|
|
t.Errorf("%s: err = %v", v, err)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil || m.Requires["acme.user"] != v || len(warnings) != 1 || !strings.Contains(warnings[0], "acme.user") {
|
|
t.Errorf("%s: requires %v, warnings %v, err %v", v, m.Requires, warnings, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPackagePath(t *testing.T) {
|
|
root := filepath.FromSlash("/src/m")
|
|
for dir, want := range map[string]string{
|
|
"/src/m": "example.test/m",
|
|
"/src/m/a": "example.test/m/a",
|
|
"/src/m/a/b/c": "example.test/m/a/b/c",
|
|
} {
|
|
got, err := packagePath("example.test/m", root, filepath.FromSlash(dir))
|
|
if err != nil || got != want {
|
|
t.Errorf("packagePath(%s) = %q, %v; want %q", dir, got, err, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPluginDirs(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeTree(t, root, map[string]string{
|
|
"plugin.go": "package p",
|
|
"sub/plugin.go": "package sub",
|
|
"sub/deeper/plugin.go": "package deeper",
|
|
"testdata/plugin.go": "package x",
|
|
"vendor/x/plugin.go": "package x",
|
|
"node_modules/plugin.go": "package x",
|
|
".hidden/plugin.go": "package x",
|
|
"_skip/plugin.go": "package x",
|
|
"nested/go.mod": "module example.test/nested\n",
|
|
"nested/plugin.go": "package nested",
|
|
"other/notplugin.go": "package other",
|
|
})
|
|
got := pluginDirs(root)
|
|
slices.Sort(got)
|
|
want := []string{root, filepath.Join(root, "sub"), filepath.Join(root, "sub", "deeper")}
|
|
if strings.Join(got, "|") != strings.Join(want, "|") {
|
|
t.Errorf("pluginDirs = %v, want %v", got, want)
|
|
}
|
|
}
|