pluginsdk/plugin/mod_test.go

476 lines
12 KiB
Go

package plugin
import (
"bytes"
"fmt"
"strings"
"testing"
tomlpkg "github.com/BurntSushi/toml"
)
func TestParseModFull_BasicFields(t *testing.T) {
src := []byte(`
[plugin]
name = "smartblock"
scope = "blockninja"
version = "1.4.2"
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if m.Plugin.Name != "smartblock" {
t.Errorf("Name = %q, want smartblock", m.Plugin.Name)
}
if m.Plugin.Scope != "blockninja" {
t.Errorf("Scope = %q, want blockninja", m.Plugin.Scope)
}
if m.Plugin.Version != "1.4.2" {
t.Errorf("Version = %q, want 1.4.2", m.Plugin.Version)
}
if got := m.Coords(); got != "@blockninja/smartblock@1.4.2" {
t.Errorf("Coords() = %q", got)
}
}
func TestParseModFull_BackCompatNoScope(t *testing.T) {
src := []byte(`
[plugin]
name = "legacy"
version = "0.1.0"
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if m.Plugin.Scope != "" {
t.Errorf("Scope should be empty, got %q", m.Plugin.Scope)
}
if got := m.Coords(); got != "legacy@0.1.0" {
t.Errorf("Coords() = %q, want legacy@0.1.0", got)
}
}
func TestParseModFull_InvalidTOML(t *testing.T) {
_, err := ParseModFull([]byte("not valid toml = ="))
if err == nil {
t.Fatal("expected parse error")
}
}
func TestParseModFull_EmptyInput(t *testing.T) {
m, err := ParseModFull(nil)
if err != nil {
t.Fatalf("nil input err: %v", err)
}
if m.Plugin.Name != "" {
t.Errorf("Name should be empty")
}
}
func TestParseModFull_KindAndCategories(t *testing.T) {
src := []byte(`
[plugin]
name = "analyser"
scope = "blockninja"
version = "0.1.0"
kind = "plugin"
categories = ["analytics", "seo"]
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if m.Plugin.Kind != "plugin" {
t.Errorf("Kind = %q, want plugin", m.Plugin.Kind)
}
if got := m.Plugin.Categories; len(got) != 2 || got[0] != "analytics" || got[1] != "seo" {
t.Errorf("Categories = %v", got)
}
}
func TestParseModFull_KindDefaultsEmpty(t *testing.T) {
src := []byte(`
[plugin]
name = "legacy"
version = "0.1.0"
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if m.Plugin.Kind != "" {
t.Errorf("Kind should be empty for legacy mod, got %q", m.Plugin.Kind)
}
}
func TestCoords_AcceptsScopeWithOrWithoutAt(t *testing.T) {
want := "@themes/foo@1.0.0"
withAt := &ModFile{Plugin: ModPlugin{Name: "foo", Scope: "@themes", Version: "1.0.0"}}
if got := withAt.Coords(); got != want {
t.Errorf("Coords() with leading @ = %q, want %q", got, want)
}
withoutAt := &ModFile{Plugin: ModPlugin{Name: "foo", Scope: "themes", Version: "1.0.0"}}
if got := withoutAt.Coords(); got != want {
t.Errorf("Coords() without leading @ = %q, want %q", got, want)
}
}
func TestParseModFull_PrivateField(t *testing.T) {
src := []byte(`
[plugin]
name = "internal-tool"
version = "0.1.0"
private = true
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if !m.Plugin.Private {
t.Errorf("Private = false, want true")
}
}
func TestParseModFull_DataDirField(t *testing.T) {
src := []byte(`
[plugin]
name = "stateful-plugin"
version = "0.1.0"
data_dir = true
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if !m.Plugin.DataDir {
t.Errorf("DataDir = false, want true")
}
}
func TestParseModFull_DataDirDefaultsFalse(t *testing.T) {
src := []byte(`
[plugin]
name = "stateless-plugin"
version = "0.1.0"
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if m.Plugin.DataDir {
t.Errorf("DataDir = true, want false (absent key)")
}
}
func TestParseModFull_PrivateDefaultsFalse(t *testing.T) {
src := []byte(`
[plugin]
name = "public-thing"
scope = "themes"
version = "0.1.0"
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if m.Plugin.Private {
t.Errorf("Private = true, want false (default)")
}
}
func TestCoords_PrivateOverridesScope(t *testing.T) {
m := &ModFile{Plugin: ModPlugin{
Name: "myplugin",
Scope: "@themes",
Version: "0.1.0",
Private: true,
}}
if got := m.Coords(); got != "@private/myplugin@0.1.0" {
t.Errorf("Coords() = %q, want @private/myplugin@0.1.0", got)
}
}
func TestCoords_PrivateNoScope(t *testing.T) {
m := &ModFile{Plugin: ModPlugin{
Name: "myplugin",
Version: "0.1.0",
Private: true,
}}
if got := m.Coords(); got != "@private/myplugin@0.1.0" {
t.Errorf("Coords() = %q, want @private/myplugin@0.1.0", got)
}
}
func TestParseModFull_RequiredIconPacks(t *testing.T) {
src := []byte(`
[plugin]
name = "neon"
version = "0.1.0"
required_icon_packs = ["tabler", "phosphor"]
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if len(m.Plugin.RequiredIconPacks) != 2 {
t.Fatalf("RequiredIconPacks len = %d, want 2", len(m.Plugin.RequiredIconPacks))
}
if m.Plugin.RequiredIconPacks[0] != "tabler" || m.Plugin.RequiredIconPacks[1] != "phosphor" {
t.Errorf("RequiredIconPacks = %v", m.Plugin.RequiredIconPacks)
}
}
func TestParseModFull_RequiredIconPacksOmitted(t *testing.T) {
src := []byte(`
[plugin]
name = "noicons"
version = "0.1.0"
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if len(m.Plugin.RequiredIconPacks) != 0 {
t.Errorf("RequiredIconPacks should be empty when omitted, got %v", m.Plugin.RequiredIconPacks)
}
}
func TestParseRequiredIconPacks(t *testing.T) {
src := []byte(`
[plugin]
name = "neon"
version = "0.1.0"
required_icon_packs = ["tabler", " phosphor ", ""]
`)
got := ParseRequiredIconPacks(src)
if len(got) != 2 {
t.Fatalf("ParseRequiredIconPacks len = %d (%v), want 2", len(got), got)
}
if got[0] != "tabler" || got[1] != "phosphor" {
t.Errorf("ParseRequiredIconPacks = %v, want [tabler phosphor]", got)
}
}
func TestParseRequiredIconPacks_NilOnAbsent(t *testing.T) {
src := []byte(`
[plugin]
name = "noicons"
version = "0.1.0"
`)
if got := ParseRequiredIconPacks(src); got != nil {
t.Errorf("ParseRequiredIconPacks on absent field = %v, want nil", got)
}
}
func TestParseRequiredIconPacks_NilOnInvalidTOML(t *testing.T) {
if got := ParseRequiredIconPacks([]byte("not valid = = =")); got != nil {
t.Errorf("ParseRequiredIconPacks on invalid TOML = %v, want nil", got)
}
}
func TestParseModFull_RequiresAndCompat(t *testing.T) {
src := []byte(`
[plugin]
name = "symposium"
scope = "blockninja"
version = "0.2.0"
[compatibility]
block_core = ">=1.5 <2.0"
admin_api = ">=0.1.2 <0.2.0"
plugin_sdk = ">=0.3.7 <0.4.0"
[[requires]]
name = "@blockninja/smartblock"
version = ">=1.0 <2.0"
[[requires]]
name = "@blockninja/gotham"
version = ">=1.2"
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if m.Compatibility == nil {
t.Fatal("Compatibility is nil")
}
if m.Compatibility.BlockCore != ">=1.5 <2.0" {
t.Errorf("Compat.BlockCore = %q", m.Compatibility.BlockCore)
}
if m.Compatibility.AdminAPI != ">=0.1.2 <0.2.0" {
t.Errorf("Compat.AdminAPI = %q", m.Compatibility.AdminAPI)
}
if m.Compatibility.PluginSDK != ">=0.3.7 <0.4.0" {
t.Errorf("Compat.PluginSDK = %q", m.Compatibility.PluginSDK)
}
if len(m.Requires) != 2 {
t.Fatalf("Requires len = %d, want 2", len(m.Requires))
}
if m.Requires[0].Name != "@blockninja/smartblock" {
t.Errorf("Requires[0].Name = %q", m.Requires[0].Name)
}
if m.Requires[1].Version != ">=1.2" {
t.Errorf("Requires[1].Version = %q", m.Requires[1].Version)
}
}
func TestModCompatPluginSDKRoundTrip(t *testing.T) {
t.Parallel()
want := &ModFile{
Plugin: ModPlugin{Name: "wiki", Version: "0.2.0"},
Compatibility: &ModCompat{
BlockCore: ">=0.3.4",
AdminAPI: ">=0.1.2",
PluginSDK: ">=0.3.7 <0.4.0",
},
}
var encoded bytes.Buffer
if err := tomlpkg.NewEncoder(&encoded).Encode(want); err != nil {
t.Fatalf("encode plugin.mod: %v", err)
}
if !strings.Contains(encoded.String(), `plugin_sdk = ">=0.3.7 <0.4.0"`) {
t.Fatalf("encoded plugin.mod omitted plugin_sdk:\n%s", encoded.String())
}
got, err := ParseModFull(encoded.Bytes())
if err != nil {
t.Fatalf("ParseModFull(round trip): %v", err)
}
if got.Compatibility == nil || got.Compatibility.PluginSDK != want.Compatibility.PluginSDK {
t.Fatalf("round-trip PluginSDK = %#v, want %q", got.Compatibility, want.Compatibility.PluginSDK)
}
}
func TestNormalizeTags_HappyPath(t *testing.T) {
got, err := NormalizeTags([]string{"dark", "agency", "serif"})
if err != nil {
t.Fatalf("NormalizeTags err: %v", err)
}
want := []string{"dark", "agency", "serif"}
if len(got) != len(want) {
t.Fatalf("len = %d, want %d (%v)", len(got), len(want), got)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("[%d] = %q, want %q", i, got[i], want[i])
}
}
}
func TestNormalizeTags_LowercaseAndTrim(t *testing.T) {
got, err := NormalizeTags([]string{" Dark ", "AGENCY"})
if err != nil {
t.Fatalf("err: %v", err)
}
if len(got) != 2 || got[0] != "dark" || got[1] != "agency" {
t.Errorf("got %v, want [dark agency]", got)
}
}
func TestNormalizeTags_DedupesCaseInsensitive(t *testing.T) {
got, err := NormalizeTags([]string{"dark", "Dark", "DARK", "agency"})
if err != nil {
t.Fatalf("err: %v", err)
}
if len(got) != 2 || got[0] != "dark" || got[1] != "agency" {
t.Errorf("got %v, want [dark agency]", got)
}
}
func TestNormalizeTags_DropsEmpty(t *testing.T) {
got, err := NormalizeTags([]string{"", "dark", " "})
if err != nil {
t.Fatalf("err: %v", err)
}
if len(got) != 1 || got[0] != "dark" {
t.Errorf("got %v, want [dark]", got)
}
}
func TestNormalizeTags_RejectsBadSlugs(t *testing.T) {
_, err := NormalizeTags([]string{"valid", "Has Space", "trailing-", "-leading", "double--hyphen", "a", strings.Repeat("x", 31)})
if err == nil {
t.Fatal("expected error")
}
for _, frag := range []string{"Has Space", "trailing-", "-leading", "double--hyphen", "a", strings.Repeat("x", 31)} {
if !strings.Contains(err.Error(), frag) {
t.Errorf("error %q does not mention %q", err.Error(), frag)
}
}
}
func TestNormalizeTags_AcceptsBounds(t *testing.T) {
got, err := NormalizeTags([]string{"ab", strings.Repeat("a", 30)})
if err != nil {
t.Fatalf("err: %v", err)
}
if len(got) != 2 {
t.Errorf("got %v, want both accepted", got)
}
}
func TestNormalizeTags_CapEnforced(t *testing.T) {
in := make([]string, TagMaxCount+1)
for i := range in {
in[i] = fmt.Sprintf("tag-%d", i)
}
_, err := NormalizeTags(in)
if err == nil {
t.Fatal("expected too-many-tags error")
}
if !strings.Contains(err.Error(), "too many tags") {
t.Errorf("error %q does not mention 'too many tags'", err.Error())
}
}
func TestNormalizeTags_NilInput(t *testing.T) {
got, err := NormalizeTags(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
if len(got) != 0 {
t.Errorf("got %v, want empty", got)
}
}
func TestParseModFull_Tags(t *testing.T) {
src := []byte(`
[plugin]
name = "dark-pro"
scope = "themes"
version = "0.1.0"
kind = "theme"
tags = ["dark", "agency", "serif"]
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if len(m.Plugin.Tags) != 3 {
t.Fatalf("Tags len = %d, want 3 (%v)", len(m.Plugin.Tags), m.Plugin.Tags)
}
if m.Plugin.Tags[0] != "dark" || m.Plugin.Tags[1] != "agency" || m.Plugin.Tags[2] != "serif" {
t.Errorf("Tags = %v", m.Plugin.Tags)
}
}
func TestParseModFull_TagsOmittedIsNil(t *testing.T) {
src := []byte(`
[plugin]
name = "no-tags"
version = "0.1.0"
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("err: %v", err)
}
if m.Plugin.Tags != nil {
t.Errorf("Tags = %v, want nil when omitted", m.Plugin.Tags)
}
}