Files
summercms/modules/kiosk/derive_test.go
Jakub Zych 9109e5b139 test(kiosk): manifest, identity, version and client unit tests
- 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
2026-10-29 21:22:47 +01:00

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