diff --git a/lib/admin/accounts/account.ex b/lib/admin/accounts/account.ex index 42458f718..c46a7d72f 100644 --- a/lib/admin/accounts/account.ex +++ b/lib/admin/accounts/account.ex @@ -21,7 +21,7 @@ defmodule Admin.Accounts.Account do |> cast(attrs, [:name, :email, :type, :extra]) |> validate_required([:name, :email, :type]) |> validate_email() - |> maybe_validate_lang(:extra) + |> validate_change(:extra, fn _, value -> validate_lang(value) end) end defp validate_email(changeset) do @@ -33,37 +33,18 @@ defmodule Admin.Accounts.Account do end # Validates `lang` only if present; permits nil or empty maps. - defp maybe_validate_lang(changeset, field) when is_atom(field) do - map = get_field(changeset, field) + defp validate_lang(map) when is_map(map) and map == %{}, do: [] - cond do - # Skip validation if nil or empty map - is_nil(map) or (is_map(map) and map == %{}) -> - changeset + defp validate_lang(map) when is_map(map) do + case Map.fetch(map, "lang") do + :error -> + [] - # If provided but not a map, type error - not is_map(map) -> - add_error(changeset, field, "must be a map") + {:ok, lang} when is_binary(lang) and lang != "" -> + [] - true -> - map_contains_string(changeset, field, map, :lang) - end - end - - defp map_contains_string(changeset, field, map, key) do - case Map.get(map, key) do - nil -> - # Key absent is OK - changeset - - v when is_binary(v) and v != "" -> - changeset - - v when is_binary(v) -> - add_error(changeset, field, "\"lang\" must be a non-empty string") - - _ -> - add_error(changeset, field, "\"lang\" must be a string") + {:ok, _} -> + [extra: {"must be a non-empty string", []}] end end end diff --git a/test/admin/blog_test.exs b/test/admin/blog_test.exs new file mode 100644 index 000000000..5910fcfc3 --- /dev/null +++ b/test/admin/blog_test.exs @@ -0,0 +1,139 @@ +defmodule Admin.BlogTest do + use ExUnit.Case, async: true + + alias Admin.Blog + alias Admin.Blog.Parser + alias Admin.Blog.Post + + describe "blog parser" do + test "parses yaml frontmatter" do + content = """ + --- + title: hello + other: + - hey + - you + --- + + This is the blog content + """ + + assert {%{"other" => ["hey", "you"], "title" => "hello"}, "\nThis is the blog content\n"} = + Parser.parse("somepath", content) + end + + test "raises if missing the delimiter" do + content = """ + title: hello + other: + - hey + - you + + This is the blog content + """ + + assert_raise MatchError, fn -> + Parser.parse("somepath", content) + end + end + + test "raises if not yaml frontmatter the delimiter" do + content = """ + %{ + title: "hello" + other: ["hey", "you"] + } + --- + + This is the blog content + """ + + assert_raise MatchError, fn -> + Parser.parse("somepath", content) + end + end + + test "no content is ok" do + content = """ + title: hello + other: + - hey + - you + --- + """ + + assert {%{"other" => ["hey", "you"], "title" => "hello"}, ""} = + Parser.parse("somepath", content) + end + end + + describe "blog posts" do + test "can get all posts" do + assert Blog.all_posts() + end + + test "can get a post by id" do + assert Blog.get_post_by_id!("2026-01-19-production-release") + end + + test "post that does not exist raises" do + assert_raise AdminWeb.NotFoundError, fn -> + Blog.get_post_by_id!("nonexistent-post-id") + end + end + + test "posts by year" do + assert posts = Blog.posts_by_year() + assert posts |> Enum.find(fn {year, posts_for_year} -> year == 2026 end) + end + end + + describe "blog post" do + test "can build a blog post struct" do + assert %Post{ + id: "2026-01-01-my-post-id", + title: "hello", + date: ~D[2026-01-01], + tags: [], + authors: ["hey", "you"], + body: "Content" + } = + Post.build( + "2026/01-01-my-post-id", + %{ + "title" => "hello", + "authors" => ["hey", "you"] + }, + "Content" + ) + end + + test "parses the description from the content" do + content = """ + This is the description of the post. + + + + This is the rest of the post. + """ + + assert %Post{ + id: "2026-01-01-my-post-id", + title: "hello", + date: ~D[2026-01-01], + tags: [], + description: "This is the description of the post.\n\n", + authors: ["hey", "you"], + body: ^content + } = + Post.build( + "2026/01-01-my-post-id", + %{ + "title" => "hello", + "authors" => ["hey", "you"] + }, + content + ) + end + end +end diff --git a/test/admin/members_test.exs b/test/admin/members_test.exs new file mode 100644 index 000000000..0b0a1da91 --- /dev/null +++ b/test/admin/members_test.exs @@ -0,0 +1,66 @@ +defmodule Admin.MembersTest do + use Admin.DataCase + + alias Admin.Accounts + + describe "member extra" do + test "no extra is ok" do + assert {:ok, member} = + Accounts.create_member(%{ + name: "Someone", + email: "unknown@example.com", + type: "individual" + }) + + assert member.extra == nil + end + + test "no lang in extra is ok" do + assert {:ok, member} = + Accounts.create_member(%{ + name: "Someone", + email: "unknown@example.com", + type: "individual", + extra: %{} + }) + + assert member.extra == %{} + end + + test "fails if extra is not a map" do + assert {:error, %Ecto.Changeset{} = changeset} = + Accounts.create_member(%{ + name: "Someone", + email: "unknown@example.com", + type: "individual", + extra: ["not a map"] + }) + + assert changeset.errors[:extra] == {"is invalid", [{:type, :map}, {:validation, :cast}]} + end + + test "fails if lang in extra is empty" do + assert {:error, %Ecto.Changeset{} = changeset} = + Accounts.create_member(%{ + name: "Someone", + email: "unknown@example.com", + type: "individual", + extra: %{"lang" => ""} + }) + + assert changeset.errors[:extra] == {"must be a non-empty string", []} + end + + test "has a valid lang in extra" do + assert {:ok, member} = + Accounts.create_member(%{ + name: "Someone", + email: "unknown@example.com", + type: "individual", + extra: %{"lang" => "en"} + }) + + assert member.extra == %{"lang" => "en"} + end + end +end diff --git a/test/admin/static_pages_test.exs b/test/admin/static_pages_test.exs new file mode 100644 index 000000000..9d5cc0fe4 --- /dev/null +++ b/test/admin/static_pages_test.exs @@ -0,0 +1,45 @@ +defmodule Admin.StaticPagesTest do + use ExUnit.Case, async: true + + alias Admin.StaticPages + alias Admin.StaticPages.Page + + describe "static pages" do + test "can get required pages" do + assert ["disclaimer", "privacy", "terms"] = StaticPages.get_unique_page_ids() + end + + test "get pages in en" do + assert StaticPages.get_static_page!("en", "disclaimer") + assert StaticPages.get_static_page!("en", "terms") + assert StaticPages.get_static_page!("en", "privacy") + + assert_raise AdminWeb.NotFoundError, fn -> + StaticPages.get_static_page!("en", "nonexistent") + end + end + + test "page exists" do + assert StaticPages.exists?("en", "disclaimer") + refute StaticPages.exists?("en", "nonexistent") + end + end + + describe "page struct" do + test "can build a page from a file" do + assert %Page{ + id: "disclaimer", + locale: "en", + title: "Disclaimer", + body: "Content" + } = + Page.build( + "en/disclaimer.md", + %{ + title: "Disclaimer" + }, + "Content" + ) + end + end +end