package bnp
import (
"context"
"os"
"path/filepath"
"testing"
abiv1 "git.dev.alexdunmow.com/block/core/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",
"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 !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())
}
}
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",
}},
}
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)
}
}
}