add custom page data model with split changesets and CRUD context
Stage 1 of custom CMS pages. Adds type/published/meta/nav fields to pages schema, splits changeset into system vs custom (with slug format validation and reserved path exclusion), adds create/update/delete functions with auto-redirect on slug change, and warms custom pages in ETS cache. 62 pages tests, 1426 total. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,11 +46,24 @@ defmodule Berrypod.PagesTest do
|
||||
assert is_map(block["settings"]), "block missing settings"
|
||||
end
|
||||
end
|
||||
|
||||
test "returns nil for unknown custom slug" do
|
||||
assert Pages.get_page("does-not-exist") == nil
|
||||
end
|
||||
|
||||
test "returns page data for created custom page" do
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "my-page", title: "My page"})
|
||||
page = Pages.get_page("my-page")
|
||||
|
||||
assert page.slug == "my-page"
|
||||
assert page.title == "My page"
|
||||
assert page.type == "custom"
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_pages/0" do
|
||||
describe "list_system_pages/0" do
|
||||
test "returns all 14 pages" do
|
||||
pages = Pages.list_pages()
|
||||
pages = Pages.list_system_pages()
|
||||
|
||||
assert length(pages) == 14
|
||||
slugs = Enum.map(pages, & &1.slug)
|
||||
@@ -60,6 +73,43 @@ defmodule Berrypod.PagesTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_custom_pages/0" do
|
||||
test "returns empty list when no custom pages exist" do
|
||||
assert Pages.list_custom_pages() == []
|
||||
end
|
||||
|
||||
test "returns custom pages ordered by title" do
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "zebra", title: "Zebra"})
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "apple", title: "Apple"})
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "mango", title: "Mango"})
|
||||
|
||||
pages = Pages.list_custom_pages()
|
||||
titles = Enum.map(pages, & &1.title)
|
||||
assert titles == ["Apple", "Mango", "Zebra"]
|
||||
end
|
||||
|
||||
test "does not include system pages" do
|
||||
{:ok, _} = Pages.save_page("home", %{title: "Home page", blocks: []})
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "custom-one", title: "Custom"})
|
||||
|
||||
pages = Pages.list_custom_pages()
|
||||
slugs = Enum.map(pages, & &1.slug)
|
||||
assert "custom-one" in slugs
|
||||
refute "home" in slugs
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_all_pages/0" do
|
||||
test "includes both system and custom pages" do
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "faq", title: "FAQ"})
|
||||
|
||||
pages = Pages.list_all_pages()
|
||||
slugs = Enum.map(pages, & &1.slug)
|
||||
assert "home" in slugs
|
||||
assert "faq" in slugs
|
||||
end
|
||||
end
|
||||
|
||||
describe "save_page/2" do
|
||||
test "creates a new page in the DB" do
|
||||
blocks = [%{"id" => "blk_1", "type" => "hero", "settings" => %{"title" => "Hello"}}]
|
||||
@@ -91,14 +141,144 @@ defmodule Berrypod.PagesTest do
|
||||
assert page.title == "New home"
|
||||
end
|
||||
|
||||
test "rejects invalid slug" do
|
||||
test "rejects invalid slug format" do
|
||||
assert {:error, changeset} =
|
||||
Pages.save_page("nonexistent", %{title: "Bad", blocks: []})
|
||||
Pages.save_page("UPPER CASE", %{title: "Bad", blocks: []})
|
||||
|
||||
assert errors_on(changeset).slug
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_custom_page/1" do
|
||||
test "creates a custom page with valid slug" do
|
||||
assert {:ok, page} = Pages.create_custom_page(%{slug: "our-story", title: "Our story"})
|
||||
|
||||
assert page.slug == "our-story"
|
||||
assert page.title == "Our story"
|
||||
assert page.type == "custom"
|
||||
assert page.blocks == []
|
||||
assert page.published == true
|
||||
end
|
||||
|
||||
test "rejects duplicate slug" do
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "faq", title: "FAQ"})
|
||||
assert {:error, changeset} = Pages.create_custom_page(%{slug: "faq", title: "FAQ 2"})
|
||||
assert errors_on(changeset).slug
|
||||
end
|
||||
|
||||
test "rejects system slug" do
|
||||
assert {:error, changeset} = Pages.create_custom_page(%{slug: "home", title: "Home"})
|
||||
assert errors_on(changeset).slug
|
||||
end
|
||||
|
||||
test "rejects reserved path" do
|
||||
assert {:error, changeset} = Pages.create_custom_page(%{slug: "admin", title: "Admin"})
|
||||
assert errors_on(changeset).slug
|
||||
end
|
||||
|
||||
test "rejects invalid slug format" do
|
||||
assert {:error, changeset} =
|
||||
Pages.create_custom_page(%{slug: "My Page", title: "My page"})
|
||||
|
||||
assert errors_on(changeset).slug
|
||||
end
|
||||
|
||||
test "rejects slug with leading hyphen" do
|
||||
assert {:error, changeset} = Pages.create_custom_page(%{slug: "-bad", title: "Bad"})
|
||||
assert errors_on(changeset).slug
|
||||
end
|
||||
|
||||
test "rejects slug with trailing hyphen" do
|
||||
assert {:error, changeset} = Pages.create_custom_page(%{slug: "bad-", title: "Bad"})
|
||||
assert errors_on(changeset).slug
|
||||
end
|
||||
|
||||
test "accepts single-character slug" do
|
||||
assert {:ok, page} = Pages.create_custom_page(%{slug: "x", title: "X"})
|
||||
assert page.slug == "x"
|
||||
end
|
||||
|
||||
test "populates cache on create" do
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "cached-page", title: "Cached"})
|
||||
assert {:ok, data} = PageCache.get("cached-page")
|
||||
assert data.title == "Cached"
|
||||
assert data.type == "custom"
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete_custom_page/1" do
|
||||
test "deletes a custom page" do
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "temp", title: "Temp"})
|
||||
page_struct = Pages.get_page_struct("temp")
|
||||
assert {:ok, _} = Pages.delete_custom_page(page_struct)
|
||||
assert Pages.get_page("temp") == nil
|
||||
end
|
||||
|
||||
test "refuses to delete a system page" do
|
||||
{:ok, page} = Pages.save_page("home", %{title: "Home page", blocks: []})
|
||||
assert {:error, :system_page} = Pages.delete_custom_page(page)
|
||||
end
|
||||
|
||||
test "invalidates cache on delete" do
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "bye", title: "Bye"})
|
||||
page_struct = Pages.get_page_struct("bye")
|
||||
Pages.delete_custom_page(page_struct)
|
||||
assert :miss = PageCache.get("bye")
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_custom_page/2" do
|
||||
test "updates title" do
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "updates", title: "V1"})
|
||||
page = Pages.get_page_struct("updates")
|
||||
assert {:ok, updated} = Pages.update_custom_page(page, %{title: "V2"})
|
||||
assert updated.title == "V2"
|
||||
end
|
||||
|
||||
test "slug change creates redirect" do
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "old-name", title: "Page"})
|
||||
page = Pages.get_page_struct("old-name")
|
||||
assert {:ok, updated} = Pages.update_custom_page(page, %{slug: "new-name"})
|
||||
assert updated.slug == "new-name"
|
||||
|
||||
assert {:ok, redirect} = Berrypod.Redirects.lookup("/old-name")
|
||||
assert redirect.to_path == "/new-name"
|
||||
end
|
||||
|
||||
test "slug change invalidates old cache and populates new" do
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "rename-me", title: "Page"})
|
||||
page = Pages.get_page_struct("rename-me")
|
||||
{:ok, _} = Pages.update_custom_page(page, %{slug: "renamed"})
|
||||
|
||||
assert :miss = PageCache.get("rename-me")
|
||||
assert {:ok, cached} = PageCache.get("renamed")
|
||||
assert cached.slug == "renamed"
|
||||
end
|
||||
|
||||
test "updates meta and nav fields" do
|
||||
{:ok, _} = Pages.create_custom_page(%{slug: "meta-test", title: "Meta"})
|
||||
page = Pages.get_page_struct("meta-test")
|
||||
|
||||
{:ok, updated} =
|
||||
Pages.update_custom_page(page, %{
|
||||
meta_description: "A test page",
|
||||
show_in_nav: true,
|
||||
nav_label: "Test",
|
||||
nav_position: 5
|
||||
})
|
||||
|
||||
assert updated.meta_description == "A test page"
|
||||
assert updated.show_in_nav == true
|
||||
assert updated.nav_label == "Test"
|
||||
assert updated.nav_position == 5
|
||||
end
|
||||
|
||||
test "refuses to update a system page" do
|
||||
{:ok, page} = Pages.save_page("home", %{title: "Home page", blocks: []})
|
||||
assert {:error, :system_page} = Pages.update_custom_page(page, %{title: "Nope"})
|
||||
end
|
||||
end
|
||||
|
||||
describe "reset_page/1" do
|
||||
test "restores defaults after customisation" do
|
||||
{:ok, _} =
|
||||
@@ -220,6 +400,20 @@ defmodule Berrypod.PagesTest do
|
||||
refute Map.has_key?(cart_types, "product_hero")
|
||||
end
|
||||
|
||||
test "allowed_for/1 returns only portable blocks for custom pages" do
|
||||
custom_types = BlockTypes.allowed_for("my-custom-page")
|
||||
|
||||
# Portable blocks are included
|
||||
assert Map.has_key?(custom_types, "hero")
|
||||
assert Map.has_key?(custom_types, "featured_products")
|
||||
assert Map.has_key?(custom_types, "image_text")
|
||||
|
||||
# Page-specific blocks are excluded
|
||||
refute Map.has_key?(custom_types, "cart_items")
|
||||
refute Map.has_key?(custom_types, "product_hero")
|
||||
refute Map.has_key?(custom_types, "contact_form")
|
||||
end
|
||||
|
||||
test "every block type has required fields" do
|
||||
for {type, def} <- BlockTypes.all() do
|
||||
assert is_binary(def.name), "#{type} missing name"
|
||||
@@ -313,6 +507,14 @@ defmodule Berrypod.PagesTest do
|
||||
]
|
||||
end
|
||||
|
||||
test "unknown slug returns humanised title and empty blocks" do
|
||||
page = Defaults.for_slug("our-story")
|
||||
|
||||
assert page.slug == "our-story"
|
||||
assert page.title == "Our story"
|
||||
assert page.blocks == []
|
||||
end
|
||||
|
||||
test "generate_block_id/0 produces unique prefixed IDs" do
|
||||
id1 = Defaults.generate_block_id()
|
||||
id2 = Defaults.generate_block_id()
|
||||
@@ -344,4 +546,22 @@ defmodule Berrypod.PagesTest do
|
||||
assert :miss = PageCache.get(slug)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Page schema" do
|
||||
test "system_slugs/0 returns 14 slugs" do
|
||||
assert length(Page.system_slugs()) == 14
|
||||
end
|
||||
|
||||
test "system_slug?/1 returns true for system slugs" do
|
||||
assert Page.system_slug?("home")
|
||||
assert Page.system_slug?("cart")
|
||||
refute Page.system_slug?("my-page")
|
||||
end
|
||||
|
||||
test "reserved_path?/1 returns true for reserved paths" do
|
||||
assert Page.reserved_path?("admin")
|
||||
assert Page.reserved_path?("collections")
|
||||
refute Page.reserved_path?("faq")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user