package plugin import ( "fmt" "strings" "testing" ) 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_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" [[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 || m.Compatibility.BlockCore != ">=1.5 <2.0" { t.Errorf("Compat = %+v", m.Compatibility) } 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 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) } }