package bnp
import (
"context"
"os"
"path/filepath"
"testing"
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
)
// writeFixture lays out a minimal codeless plugin repo.
func writeFixture(t *testing.T, dir string, files map[string]string) {
t.Helper()
for rel, content := range files {
path := filepath.Join(dir, rel)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
}
func codelessFixture(t *testing.T) string {
t.Helper()
dir := t.TempDir()
writeFixture(t, dir, map[string]string{
"plugin.mod": "[plugin]\nname = \"fixture-theme\"\nversion = \"0.1.0\"\nkind = \"theme\"\n",
"manifest.yaml": "theme_presets: presets.json\nrequired_icon_packs: [lucide]\n" +
"master_pages: master_pages.json\n" +
"system_templates:\n - key: fixture\n title: Fixture Theme\n" +
"page_templates:\n - system: fixture\n key: landing\n title: Landing\n slots: [main]\n" +
"template_overrides:\n - template: fixture\n block: heading\n" +
"email_wrappers: [fixture]\n",
"templates/fixture/landing.ninjatpl": `{{ content }}`,
"templates/overrides/fixture/heading.ninjatpl": `
{{ text }}
`,
"templates/email/fixture.ninjatpl": ``,
"presets.json": `{"presets":[{"key":"default"}]}`,
"master_pages.json": `[{"key":"landing","title":"Landing","blocks":[{"block_key":"html","title":"Hero","content":{"x":1},"slot":"main","sort_order":1}]}]`,
"blocks/blocks.yaml": "blocks:\n - key: hero\n title: Hero\n category: content\n" +
" schema: hero.schema.json\n template: hero.ninjatpl\n providers: [site]\n",
"blocks/hero.schema.json": `{"type":"object"}`,
"blocks/hero.ninjatpl": `{{ title }}
`,
"seed/seed.json": `{"settings":{"ensure":{"welcome":true}},` +
`"pages":[{"slug":"/","title":"Home","template_key":"landing"}],` +
`"menu_items":[{"menu":"main","label":"Home","page_slug":"/","sort_order":1}]}`,
"assets/logo.svg": ``,
})
return dir
}
func TestCodelessBuildAndVerifyRoundTrip(t *testing.T) {
dir := codelessFixture(t)
isCodeless, err := IsCodelessRepo(dir)
if err != nil || !isCodeless {
t.Fatalf("IsCodelessRepo = %v, %v; want true", isCodeless, err)
}
out := filepath.Join(t.TempDir(), "fixture-theme-0.1.0.bnp")
res, err := BuildCodeless(context.Background(), BuildOptions{Dir: dir, Output: out})
if err != nil {
t.Fatalf("BuildCodeless: %v", err)
}
if !res.Codeless || res.BlockCount != 1 {
t.Errorf("result = %+v; want codeless with 1 block", res)
}
v, err := Verify(out)
if err != nil {
t.Fatalf("Verify: %v", err)
}
if !v.Codeless || !v.HasBlocks || !v.HasSeed || !v.HasAssets || v.Name != "fixture-theme" {
t.Errorf("verify = %+v", v)
}
m, err := ReadManifest(out)
if err != nil {
t.Fatal(err)
}
if len(m.GetSystemTemplates()) != 1 || len(m.GetPageTemplates()) != 1 || m.GetPageTemplates()[0].GetSystemKey() != "fixture" {
t.Errorf("templates = %v / %v", m.GetSystemTemplates(), m.GetPageTemplates())
}
if !m.GetCodeless() || len(m.GetMasterPages()) != 1 || len(m.GetThemePresets()) == 0 {
t.Errorf("manifest = codeless:%v masters:%d presets:%dB",
m.GetCodeless(), len(m.GetMasterPages()), len(m.GetThemePresets()))
}
if len(m.GetRequiredIconPacks()) != 1 || m.GetRequiredIconPacks()[0] != "lucide" {
t.Errorf("icon packs = %v", m.GetRequiredIconPacks())
}
if len(m.GetBlockTemplateOverrides()) != 1 ||
m.GetBlockTemplateOverrides()[0].GetTemplateKey() != "fixture" ||
m.GetBlockTemplateOverrides()[0].GetBlockKey() != "heading" {
t.Errorf("template overrides = %v", m.GetBlockTemplateOverrides())
}
if len(m.GetEmailWrapperSystemKeys()) != 1 || m.GetEmailWrapperSystemKeys()[0] != "fixture" {
t.Errorf("email wrappers = %v", m.GetEmailWrapperSystemKeys())
}
}
func TestIsCodelessRepoFalseWithGoSource(t *testing.T) {
dir := t.TempDir()
writeFixture(t, dir, map[string]string{
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\n",
"main.go": "package main\nfunc main() {}\n",
})
isCodeless, err := IsCodelessRepo(dir)
if err != nil || isCodeless {
t.Fatalf("IsCodelessRepo = %v, %v; want false", isCodeless, err)
}
}
func TestCodelessBuildRejectsBadDeclarations(t *testing.T) {
cases := []struct {
name string
files map[string]string
}{
{"bad blocks yaml", map[string]string{
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\n",
"blocks/blocks.yaml": "blocks:\n - key: hero\n", // missing title/schema/template
}},
{"missing template file", map[string]string{
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\n",
"blocks/blocks.yaml": "blocks:\n - key: hero\n title: Hero\n" +
" schema: hero.schema.json\n template: missing.ninjatpl\n",
"blocks/hero.schema.json": `{}`,
}},
{"seed media without id", map[string]string{
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\n",
"seed/seed.json": `{"media":[{"file":"a.jpg"}]}`,
"seed/a.jpg": "x",
}},
{"data_dir grant", map[string]string{
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\ndata_dir = true\n",
}},
{"public_routes claim", map[string]string{
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\n" +
"public_routes = [{ path = \"/area\", prefix = true }]\n",
}},
{"sitemap flag", map[string]string{
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\nsitemap = true\n",
}},
{"override missing template file", map[string]string{
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\n",
"manifest.yaml": "template_overrides:\n - template: x\n block: heading\n",
}},
{"override missing block key", map[string]string{
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\n",
"manifest.yaml": "template_overrides:\n - template: x\n",
"templates/overrides/x/heading.ninjatpl": `{{ text }}
`,
}},
{"email wrapper missing template file", map[string]string{
"plugin.mod": "[plugin]\nname = \"x\"\nversion = \"0.1.0\"\n",
"manifest.yaml": "email_wrappers: [x]\n",
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
writeFixture(t, dir, tc.files)
_, err := BuildCodeless(context.Background(), BuildOptions{
Dir: dir, Output: filepath.Join(t.TempDir(), "x.bnp"),
})
if err == nil {
t.Fatal("BuildCodeless succeeded; want error")
}
})
}
}
func TestCodelessHookViolation(t *testing.T) {
ok := &abiv1.PluginManifest{Codeless: true, Name: "x"}
if err := CodelessHookViolation(ok); err != nil {
t.Fatalf("clean manifest flagged: %v", err)
}
bad := []*abiv1.PluginManifest{
{Codeless: true, HasHttpHandler: true},
{Codeless: true, JobTypes: []string{"j"}},
{Codeless: true, DeclaredTags: []string{"t"}},
{Codeless: true, Blocks: []*abiv1.BlockMeta{{Key: "k"}}},
{Codeless: true, DataDir: true},
}
for i, m := range bad {
if err := CodelessHookViolation(m); err == nil {
t.Errorf("case %d not flagged", i)
}
}
}