Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 10 additions & 29 deletions lib/admin/accounts/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
139 changes: 139 additions & 0 deletions test/admin/blog_test.exs
Original file line number Diff line number Diff line change
@@ -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.

<!-- truncate -->

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
66 changes: 66 additions & 0 deletions test/admin/members_test.exs
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions test/admin/static_pages_test.exs
Original file line number Diff line number Diff line change
@@ -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
Loading