manifest.yaml gains system_templates + page_templates declarations; each page template's .ninjatpl source lives at templates/<system>/<key>.ninjatpl and is validated at pack time. Verify no longer rejects declared system/page templates on codeless manifests (guest template_keys stay rejected) — the cms host registers them source-tracked and renders the files through the same host pongo pipeline powered blocks use. Blog/system/normal page layouts are now fully expressible with zero code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
159 lines
5.4 KiB
Go
159 lines
5.4 KiB
Go
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" +
|
|
"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",
|
|
"templates/fixture/landing.ninjatpl": `<main>{{ content }}</main>`,
|
|
"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": `<h1>{{ title }}</h1>`,
|
|
"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": `<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())
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|