Files
summercms/modules/kiosk/identity_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

257 lines
9.2 KiB
Go

package kiosk
import (
"context"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"testing"
)
// writeTree writes files (slash paths relative to root) under root.
func writeTree(t *testing.T, root string, files map[string]string) {
t.Helper()
for rel, body := range files {
path := filepath.Join(root, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
}
// requireGo skips a test that runs the go command when it is missing.
func requireGo(t *testing.T) {
t.Helper()
if _, err := exec.LookPath("go"); err != nil {
t.Skip("go command not found")
}
}
const pluginHead = "package p\n\ntype Plugin struct{}\n\n"
// TestReadIdentity reads ID() and Requires() from fixtures of every
// supported shape and refuses the rest with the function named.
func TestReadIdentity(t *testing.T) {
requireGo(t)
ctx := context.Background()
gomod := "module example.test/p\n\ngo 1.27.0\n"
accepted := []struct {
name string
files map[string]string
want Identity
}{
{"literal", map[string]string{
"plugin.go": pluginHead + `func (*Plugin) ID() string { return "acme.a" }
func (*Plugin) Requires() []string { return []string{"acme.b", "acme.c"} }`,
}, Identity{ID: "acme.a", Requires: []string{"acme.b", "acme.c"}}},
{"parenthesized literal", map[string]string{
"plugin.go": pluginHead + `func (Plugin) ID() string { return ("acme.a") }`,
}, Identity{ID: "acme.a"}},
{"same-file const", map[string]string{
"plugin.go": pluginHead + `const id = "acme.a"
func (*Plugin) ID() string { return id }
func (*Plugin) Requires() []string { return nil }`,
}, Identity{ID: "acme.a"}},
{"const in another file", map[string]string{
"plugin.go": pluginHead + `func (*Plugin) ID() string { return PluginID }`,
"ids.go": "package p\n\nconst PluginID = \"acme.a\"\n",
}, Identity{ID: "acme.a"}},
{"chained and concatenated consts", map[string]string{
"plugin.go": pluginHead + `const (
vendor = "acme"
name = base
base = "a"
full = vendor + "." + name
)
func (*Plugin) ID() string { return full }`,
}, Identity{ID: "acme.a"}},
{"cross-package same module", map[string]string{
"go.mod": gomod,
"ids/ids.go": "package ids\n\nconst A = \"acme.a\"\nconst B = \"acme.b\"\n",
"plugin.go": pluginHead[:len("package p\n\n")] + `import "example.test/p/ids"
type Plugin struct{}
func (*Plugin) ID() string { return ids.A }
func (*Plugin) Requires() []string { return []string{ids.B} }`,
}, Identity{ID: "acme.a", Requires: []string{"acme.b"}}},
{"named import alias", map[string]string{
"go.mod": gomod,
"names/ids.go": "package names\n\nconst A = \"acme.a\"\n",
"plugin.go": `package p
import x "example.test/p/names"
type Plugin struct{}
func (*Plugin) ID() string { return x.A }`,
}, Identity{ID: "acme.a"}},
{"package name differs from path", map[string]string{
"go.mod": gomod,
"go-idents/ids.go": "package idents\n\nconst A = \"acme.a\"\n",
"plugin.go": `package p
import "example.test/p/go-idents"
type Plugin struct{}
func (*Plugin) ID() string { return idents.A }`,
}, Identity{ID: "acme.a"}},
{"requires empty slice", map[string]string{
"plugin.go": pluginHead + `func (*Plugin) ID() string { return "acme.a" }
func (*Plugin) Requires() []string { return []string{} }`,
}, Identity{ID: "acme.a", Requires: []string{}}},
{"requires mixed literals and consts", map[string]string{
"plugin.go": pluginHead + `const user = "acme.user"
func (*Plugin) ID() string { return "acme.a" }
func (*Plugin) Requires() []string { return []string{user, "acme.translate"} }`,
}, Identity{ID: "acme.a", Requires: []string{"acme.user", "acme.translate"}}},
{"no Requires method", map[string]string{
"plugin.go": pluginHead + `func (*Plugin) ID() string { return "acme.a" }`,
}, Identity{ID: "acme.a"}},
{"plugin.go wins over a model's ID", map[string]string{
"a_model.go": "package p\n\ntype Post struct{}\n\nfunc (Post) ID() string { return \"post\" }\nfunc (Post) Requires() []string { return []string{\"x.y\"} }\n",
"plugin.go": pluginHead + `func (*Plugin) ID() string { return "acme.a" }
func (*Plugin) Requires() []string { return []string{"acme.b"} }`,
}, Identity{ID: "acme.a", Requires: []string{"acme.b"}}},
{"test files are ignored", map[string]string{
"plugin.go": pluginHead + `func (*Plugin) ID() string { return PluginID }`,
"ids.go": "package p\n\nconst PluginID = \"acme.a\"\n",
"plugin_test.go": "package p\n\nconst PluginID = \"acme.test\"\n",
}, Identity{ID: "acme.a"}},
}
for _, tc := range accepted {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
files := map[string]string{"go.mod": gomod}
for k, v := range tc.files {
files[k] = v
}
writeTree(t, dir, files)
got, err := ReadIdentity(ctx, dir)
if err != nil {
t.Fatalf("ReadIdentity: %v", err)
}
if got.ID != tc.want.ID || !slices.Equal(got.Requires, tc.want.Requires) || (got.Requires == nil) != (tc.want.Requires == nil) {
t.Errorf("identity = %#v, want %#v", got, tc.want)
}
})
}
t.Run("other module through a replace", func(t *testing.T) {
root := t.TempDir()
writeTree(t, root, map[string]string{
"dep/go.mod": "module example.test/dep\n\ngo 1.27.0\n",
"dep/consts/ids.go": "package consts\n\nconst Shop = \"acme.shop\"\nconst User = \"acme.user\"\n",
"plugin/go.mod": "module example.test/p\n\ngo 1.27.0\n\nrequire example.test/dep v0.3.0\n\nreplace example.test/dep => ../dep\n",
"plugin/plugin.go": `package p
import "example.test/dep/consts"
type Plugin struct{}
func (*Plugin) ID() string { return consts.Shop }
func (*Plugin) Requires() []string { return []string{consts.User} }`,
})
got, err := ReadIdentity(ctx, filepath.Join(root, "plugin"))
if err != nil {
t.Fatalf("ReadIdentity: %v", err)
}
if got.ID != "acme.shop" || !slices.Equal(got.Requires, []string{"acme.user"}) {
t.Errorf("identity = %#v", got)
}
})
refused := []struct {
name, src, want string
extra map[string]string
keepsID bool
}{
{"function call", `func (*Plugin) ID() string { return id() }
func id() string { return "acme.x" }`, "ID()", nil, false},
{"var", `var pluginID = "acme.x"
func (*Plugin) ID() string { return pluginID }`, "ID()", nil, false},
{"concatenation with a call", `func (*Plugin) ID() string { return "acme." + suffix() }
func suffix() string { return "x" }`, "ID()", nil, false},
{"empty ID", `func (*Plugin) ID() string { return "" }`, "empty string", nil, false},
{"no return", `func (*Plugin) ID() string { panic("x") }`, "ID()", nil, false},
{"self-referencing const", `const a = b
const b = a
func (*Plugin) ID() string { return a }`, "refers to itself", nil, false},
{"const without value", `const (
a = "acme.a"
b
)
func (*Plugin) ID() string { return b }`, "no explicit value", nil, false},
{"unknown import", `func (*Plugin) ID() string { return ids.A }`, "no import provides package ids", nil, false},
{"import outside the module", "", "no import provides package ids", map[string]string{"plugin.go": `package p
import "example.test/elsewhere/ids"
type Plugin struct{}
func (*Plugin) ID() string { return ids.A }`}, false},
{"missing ID method", `func (*Plugin) Name() string { return "acme.x" }`, "no ID() method", nil, false},
{"requires var", `var deps = []string{"acme.y"}
func (*Plugin) ID() string { return "acme.x" }
func (*Plugin) Requires() []string { return deps }`, "Requires()", nil, true},
{"requires array", `func (*Plugin) ID() string { return "acme.x" }
func (*Plugin) Requires() []string { s := [1]string{"acme.y"}; return s[:] }`, "Requires()", nil, true},
{"requires element var", `var dep = "acme.y"
func (*Plugin) ID() string { return "acme.x" }
func (*Plugin) Requires() []string { return []string{dep} }`, "Requires()", nil, true},
}
for _, tc := range refused {
t.Run("refuse "+tc.name, func(t *testing.T) {
dir := t.TempDir()
files := map[string]string{"go.mod": gomod}
if tc.src != "" {
files["plugin.go"] = pluginHead + tc.src + "\n"
}
for k, v := range tc.extra {
files[k] = v
}
writeTree(t, dir, files)
got, err := ReadIdentity(ctx, dir)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("err = %v, want one naming %q", err, tc.want)
}
if tc.keepsID != (got.ID == "acme.x") {
t.Errorf("identity = %#v; the ID is kept only when Requires() fails", got)
}
if tc.name != "missing ID method" && !strings.Contains(err.Error(), dir) {
t.Errorf("err = %v, want the plugin directory named", err)
}
})
}
t.Run("not a directory", func(t *testing.T) {
if _, err := ReadIdentity(ctx, filepath.Join(t.TempDir(), "missing")); err == nil {
t.Error("ReadIdentity of a missing directory succeeded")
}
empty := t.TempDir()
if _, err := ReadIdentity(ctx, empty); err == nil || !strings.Contains(err.Error(), "no Go files") {
t.Errorf("empty directory: err = %v", err)
}
})
}
func TestLastElem(t *testing.T) {
for in, want := range map[string]string{
"example.test/a/b": "b",
"example.test/a/v2": "a",
"example.test/go-yaml": "yaml",
"v2": "v2",
} {
if got := lastElem(in); got != want {
t.Errorf("lastElem(%q) = %q, want %q", in, got, want)
}
}
}