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": `
{{ body|safe }}
`, "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) } } }