- 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
264 lines
8.9 KiB
Go
264 lines
8.9 KiB
Go
package kiosk
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// fullManifest is a valid manifest with every field set.
|
|
func fullManifest() Manifest {
|
|
return Manifest{
|
|
Schema: SchemaVersion,
|
|
ID: "acme.shop",
|
|
Module: "example.test/sm-shop-plugin",
|
|
Package: "example.test/sm-shop-plugin/shop",
|
|
Name: "Shop",
|
|
Description: "Products, carts and orders.",
|
|
Framework: "v0.6.0",
|
|
Requires: Requires{"acme.user": "v0.1.2", "acme.payment": "", "acme.audit": "v1.0.0"},
|
|
Overrides: &Overrides{Name: "Shop"},
|
|
}
|
|
}
|
|
|
|
// TestManifestFormat pins the plugin.yaml bytes, the parse rules and the
|
|
// validation table.
|
|
func TestManifestFormat(t *testing.T) {
|
|
t.Run("marshal is stable and ordered", func(t *testing.T) {
|
|
want := Header + "\n" +
|
|
"schema: 1\n" +
|
|
"id: acme.shop\n" +
|
|
"module: example.test/sm-shop-plugin\n" +
|
|
"package: example.test/sm-shop-plugin/shop\n" +
|
|
"name: Shop\n" +
|
|
"description: Products, carts and orders.\n" +
|
|
"framework: v0.6.0\n" +
|
|
"requires:\n" +
|
|
" acme.audit: v1.0.0\n" +
|
|
" acme.payment: \"\"\n" +
|
|
" acme.user: v0.1.2\n" +
|
|
"overrides:\n" +
|
|
" name: Shop\n"
|
|
for i := 0; i < 20; i++ {
|
|
got, err := Marshal(fullManifest())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != want {
|
|
t.Fatalf("run %d: Marshal =\n%s\nwant\n%s", i, got, want)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("empty requires and overrides are left out", func(t *testing.T) {
|
|
m := fullManifest()
|
|
m.Requires = Requires{}
|
|
m.Overrides = &Overrides{}
|
|
got, err := Marshal(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(got), "requires") || strings.Contains(string(got), "overrides") {
|
|
t.Errorf("Marshal kept empty blocks:\n%s", got)
|
|
}
|
|
m.Requires = nil
|
|
m.Overrides = nil
|
|
again, err := Marshal(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(again) != string(got) {
|
|
t.Errorf("nil and empty differ:\n%s\n%s", got, again)
|
|
}
|
|
})
|
|
|
|
t.Run("parse round-trips", func(t *testing.T) {
|
|
raw, err := Marshal(fullManifest())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, err := Parse(raw)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
again, err := Marshal(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(again) != string(raw) {
|
|
t.Errorf("round trip changed the bytes:\n%s\n%s", raw, again)
|
|
}
|
|
if v, ok := m.Requires["acme.payment"]; !ok || v != "" {
|
|
t.Errorf("empty minimum lost: %v", m.Requires)
|
|
}
|
|
})
|
|
|
|
t.Run("parse refusals", func(t *testing.T) {
|
|
raw, err := Marshal(fullManifest())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
newer := strings.Replace(string(raw), "schema: 1", "schema: 2", 1)
|
|
if _, err := Parse([]byte(newer)); err == nil || !strings.Contains(err.Error(), "update summer") {
|
|
t.Errorf("newer schema: err = %v, want one saying to update summer", err)
|
|
}
|
|
if _, err := Parse([]byte("schema: [1\n")); err == nil || !strings.Contains(err.Error(), FileName) {
|
|
t.Errorf("broken YAML: err = %v", err)
|
|
}
|
|
bad := strings.Replace(string(raw), "id: acme.shop", "id: Acme.Shop", 1)
|
|
if _, err := Parse([]byte(bad)); err == nil || !strings.Contains(err.Error(), "vendor.plugin") {
|
|
t.Errorf("invalid id: err = %v", err)
|
|
}
|
|
// A field a later schema-1 writer adds is ignored, as documented
|
|
// on Parse: older summer binaries keep reading newer files.
|
|
extra := string(raw) + "homepage: https://example.test\n"
|
|
m, err := Parse([]byte(extra))
|
|
if err != nil {
|
|
t.Fatalf("unknown field: %v", err)
|
|
}
|
|
if m.ID != "acme.shop" {
|
|
t.Errorf("unknown field changed the manifest: %+v", m)
|
|
}
|
|
})
|
|
|
|
t.Run("validate", func(t *testing.T) {
|
|
if err := Validate(fullManifest()); err != nil {
|
|
t.Fatalf("Validate(full) = %v", err)
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
mutate func(*Manifest)
|
|
want string
|
|
}{
|
|
{"schema zero", func(m *Manifest) { m.Schema = 0 }, "schema"},
|
|
{"schema newer", func(m *Manifest) { m.Schema = SchemaVersion + 1 }, "schema"},
|
|
{"id upper", func(m *Manifest) { m.ID = "Acme.shop" }, "id"},
|
|
{"id no vendor", func(m *Manifest) { m.ID = "shop" }, "id"},
|
|
{"id three parts", func(m *Manifest) { m.ID = "acme.shop.x" }, "id"},
|
|
{"id digit first", func(m *Manifest) { m.ID = "1acme.shop" }, "id"},
|
|
{"module empty", func(m *Manifest) { m.Module = "" }, "module"},
|
|
{"module dash", func(m *Manifest) { m.Module = "-x/y"; m.Package = m.Module }, "module"},
|
|
{"module space", func(m *Manifest) { m.Module = "example.test/a b"; m.Package = m.Module }, "module"},
|
|
{"module dotdot", func(m *Manifest) { m.Module = "example.test/../x"; m.Package = m.Module }, "module"},
|
|
{"module version", func(m *Manifest) { m.Module = "example.test/x@v1"; m.Package = m.Module }, "module"},
|
|
{"package shell", func(m *Manifest) { m.Package = "example.test/sm-shop-plugin/$(id)" }, "package"},
|
|
{"package outside", func(m *Manifest) { m.Package = "example.test/sm-shop-plugin-x" }, "not inside"},
|
|
{"name", func(m *Manifest) { m.Name = "\t" }, "name"},
|
|
{"description", func(m *Manifest) { m.Description = "" }, "description"},
|
|
{"framework bare", func(m *Manifest) { m.Framework = "0.6.0" }, "framework"},
|
|
{"framework empty", func(m *Manifest) { m.Framework = "" }, "framework"},
|
|
{"framework describe", func(m *Manifest) { m.Framework = "v0.6.0-3-gabcdef1" }, "framework"},
|
|
{"required id", func(m *Manifest) { m.Requires = Requires{"User": ""} }, "required id"},
|
|
{"required minimum", func(m *Manifest) { m.Requires = Requires{"acme.user": "main"} }, "minimum"},
|
|
}
|
|
for _, tc := range cases {
|
|
m := fullManifest()
|
|
tc.mutate(&m)
|
|
err := Validate(m)
|
|
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Errorf("%s: Validate = %v, want an error naming %q", tc.name, err, tc.want)
|
|
}
|
|
}
|
|
ok := []func(*Manifest){
|
|
func(m *Manifest) { m.Package = m.Module },
|
|
func(m *Manifest) { m.Framework = "v0.6.1-0.20261029101500-abcdefabcdef" },
|
|
func(m *Manifest) { m.Requires = Requires{"acme.user": "v0.1.3-0.20261029101500-abcdefabcdef"} },
|
|
func(m *Manifest) { m.Requires = nil; m.Overrides = nil },
|
|
}
|
|
for i, mutate := range ok {
|
|
m := fullManifest()
|
|
mutate(&m)
|
|
if err := Validate(m); err != nil {
|
|
t.Errorf("accepted case %d: Validate = %v", i, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestCheck covers the drift guard: missing, stale and fresh files, and
|
|
// CheckT's single Fatalf.
|
|
func TestCheck(t *testing.T) {
|
|
ctx := context.Background()
|
|
dir := filepath.Join(copyTestdata(t, "literal"), "literal")
|
|
path := filepath.Join(dir, FileName)
|
|
|
|
if err := Check(ctx, dir); err != nil {
|
|
t.Fatalf("Check on the committed fixture: %v", err)
|
|
}
|
|
var fresh fakeTB
|
|
CheckT(&fresh, dir)
|
|
if len(fresh.fatals) != 0 {
|
|
t.Fatalf("CheckT on a fresh file: %q", fresh.fatals)
|
|
}
|
|
|
|
committed, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stale := strings.Replace(string(committed), "name: Blog", "name: Weblog", 1)
|
|
if err := os.WriteFile(path, []byte(stale), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var se *StaleError
|
|
err = Check(ctx, dir)
|
|
if !errors.As(err, &se) || se.Missing {
|
|
t.Fatalf("Check on an edited file = %v, want a stale *StaleError", err)
|
|
}
|
|
if se.Path != path || string(se.Got) != stale || string(se.Want) != string(committed) {
|
|
t.Errorf("StaleError = path %s got %q want %q", se.Path, se.Got, se.Want)
|
|
}
|
|
line := strings.Count(string(committed)[:strings.Index(string(committed), "name: Blog")], "\n") + 1
|
|
wantMsg := `line ` + strconv.Itoa(line) + ` is "name: Weblog", want "name: Blog"`
|
|
if !strings.Contains(err.Error(), wantMsg) {
|
|
t.Errorf("stale message = %q, want it to carry %q", err.Error(), wantMsg)
|
|
}
|
|
var staleTB fakeTB
|
|
CheckT(&staleTB, dir)
|
|
if len(staleTB.fatals) != 1 || !strings.Contains(staleTB.fatals[0], "summer plugin:manifest "+dir) {
|
|
t.Errorf("CheckT on a stale file: %q", staleTB.fatals)
|
|
}
|
|
|
|
if err := os.Remove(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = Check(ctx, dir)
|
|
if !errors.As(err, &se) || !se.Missing || !strings.Contains(err.Error(), "is missing") {
|
|
t.Fatalf("Check without the file = %v, want Missing", err)
|
|
}
|
|
var missingTB fakeTB
|
|
CheckT(&missingTB, dir)
|
|
if len(missingTB.fatals) != 1 {
|
|
t.Errorf("CheckT on a missing file: %q", missingTB.fatals)
|
|
}
|
|
|
|
var broken fakeTB
|
|
CheckT(&broken, t.TempDir())
|
|
if len(broken.fatals) != 1 {
|
|
t.Errorf("CheckT on a directory without a plugin: %q", broken.fatals)
|
|
}
|
|
|
|
t.Run("stale message", func(t *testing.T) {
|
|
cases := []struct {
|
|
want, got, msg string
|
|
}{
|
|
{"a\nb\n", "a\nc\n", `line 2 is "c", want "b"`},
|
|
{"a\nb\n", "a\nb\nextra\n", `line 3 is "extra", want ""`},
|
|
{"a\nb\nc\n", "a\n", `line 2 is "", want "b"`},
|
|
}
|
|
for _, c := range cases {
|
|
e := &StaleError{Path: "p/plugin.yaml", Want: []byte(c.want), Got: []byte(c.got)}
|
|
if !strings.Contains(e.Error(), c.msg) || !strings.HasPrefix(e.Error(), "kiosk: p/plugin.yaml is stale") {
|
|
t.Errorf("StaleError(%q, %q) = %q, want %q", c.want, c.got, e.Error(), c.msg)
|
|
}
|
|
}
|
|
same := &StaleError{Path: "p", Want: []byte("x"), Got: []byte("x")}
|
|
if same.Error() != "kiosk: p is stale" {
|
|
t.Errorf("equal bytes: %q", same.Error())
|
|
}
|
|
})
|
|
}
|