yak(behavior-tests-blogex-public-api-and-router-plumbing-q1fb): behavior tests: blogex public API and router plumbing

This commit is contained in:
Willem van den Ende
2026-10-28 22:15:02 +00:00
parent c04d8c856c
commit cc03cefe8d
9 changed files with 211 additions and 46 deletions
@@ -127,4 +127,41 @@ defmodule Blogex.BlogIntegrationTest do
File.rm!(tmp_file)
end
end
describe "show_drafts?/0" do
# The only path through `Blogex.show_drafts?/0` (blogex.ex): `use Blogex.Blog`
# compiles a real module over a real draft fixture, so the flag is read by
# the generated `all_posts/0` rather than asserted in isolation.
#
# One test, one compile: compiling the same module name twice would print a
# "redefining module" warning, and warnings are fatal everywhere in this repo.
test "drafts are hidden by default and visible when :show_drafts is on" do
tmp_file = Path.join(System.tmp_dir!(), "test_blog_show_drafts.ex")
File.write!(tmp_file, """
defmodule TestBlogShowDrafts do
use Blogex.Blog,
blog_id: :test,
app: :blogex,
from: "priv/blog/test/2026/01-03-draft-post.md",
title: "Test Blog",
base_path: "/blog/test"
end
""")
[{TestBlogShowDrafts, _bytecode}] = Code.compile_file(tmp_file, __ENV__.file)
on_exit(fn ->
File.rm!(tmp_file)
Application.delete_env(:blogex, :show_drafts)
end)
assert apply(TestBlogShowDrafts, :all_posts, []) == []
Application.put_env(:blogex, :show_drafts, true)
assert [post] = apply(TestBlogShowDrafts, :all_posts, [])
assert post.title == "Draft Post (test fixture)"
end
end
end
@@ -324,4 +324,18 @@ defmodule Blogex.LinkValidatorTest do
}
end
end
describe "parse_blog_link/1" do
# `slug_end/1` decides where the slug stops so `?query` / `#fragment` are
# not validated as part of it. Pinned directly on `parse_blog_link/1`
# (both are `@doc false`) because `validate_link/1` only reaches them via
# "valid slug" cases, and a wrong cut would still validate by luck.
test "strips query strings and fragments from the slug" do
assert LinkValidator.parse_blog_link("/blog/engineering/hello-world?utm=1") ==
{"engineering", "hello-world"}
assert LinkValidator.parse_blog_link("/blog/engineering/hello-world#top") ==
{"engineering", "hello-world"}
end
end
end
+1 -35
View File
@@ -2,41 +2,7 @@ defmodule Blogex.RegistryTest do
use ExUnit.Case
alias Blogex.Registry
defmodule AlphaBlog do
def blog_id, do: :alpha
def all_posts,
do: [Blogex.Test.PostBuilder.build(id: "a1", date: ~D[2026-03-01], blog: :alpha)]
def all_tags, do: ["elixir"]
def unfiltered_posts,
do: [
Blogex.Test.PostBuilder.build(id: "a1", date: ~D[2026-03-01], blog: :alpha),
Blogex.Test.PostBuilder.build(
id: "a-draft",
date: ~D[2026-03-05],
blog: :alpha,
published: false
)
]
end
defmodule BetaBlog do
def blog_id, do: :beta
def all_posts,
do: [Blogex.Test.PostBuilder.build(id: "b1", date: ~D[2026-03-15], blog: :beta)]
def all_tags, do: ["devops"]
def unfiltered_posts,
do: [
Blogex.Test.PostBuilder.build(id: "b1", date: ~D[2026-03-15], blog: :beta),
Blogex.Test.PostBuilder.build(id: "b-future", date: ~D[2099-01-01], blog: :beta)
]
end
alias Blogex.Test.{AlphaBlog, BetaBlog}
setup do
Application.put_env(:blogex, :blogs, [AlphaBlog, BetaBlog])
+20
View File
@@ -153,6 +153,26 @@ defmodule Blogex.RouterTest do
end
end
describe "Helpers.base_url/1" do
# `port_suffix/2` has one clause per (scheme, default port) pair plus the
# catch-all, and the feeds embed this URL. All four shapes are pinned so a
# clause cannot be dropped or reordered without a failure.
test "omits the port for the default scheme ports" do
assert base_url(:http, 80) == "http://www.example.com"
assert base_url(:https, 443) == "https://www.example.com"
end
test "keeps a non-default port" do
assert base_url(:http, 4000) == "http://www.example.com:4000"
assert base_url(:https, 8443) == "https://www.example.com:8443"
end
defp base_url(scheme, port) do
%{Plug.Test.conn(:get, "/") | scheme: scheme, port: port}
|> Blogex.Router.Helpers.base_url()
end
end
defp get_content_type(conn) do
conn
|> Plug.Conn.get_resp_header("content-type")
+67
View File
@@ -0,0 +1,67 @@
defmodule BlogexTest do
@moduledoc """
Behaviour tests for the `Blogex` facade.
The facade is a thin delegating layer over `Blogex.Registry`, but callers
(Phoenix controllers, LiveViews) only ever see the answers, so these tests
assert the answers: the exact module lists, post orderings and tag sets the
fixtures imply. Asserting `Blogex.x() == Blogex.Registry.x()` would pin the
delegation instead of the behaviour and would keep passing if both sides
drifted together.
Non-async: the registry is configured through the process-global `:blogex`
application env.
"""
use ExUnit.Case
alias Blogex.Test.{AlphaBlog, BetaBlog}
setup do
Application.put_env(:blogex, :blogs, [AlphaBlog, BetaBlog])
on_exit(fn -> Application.delete_env(:blogex, :blogs) end)
end
describe "blogs/0" do
test "returns the configured blog modules" do
assert Blogex.blogs() == [AlphaBlog, BetaBlog]
end
end
describe "get_blog!/1 and get_blog/1" do
test "return the module registered for a blog_id" do
assert Blogex.get_blog!(:alpha) == AlphaBlog
assert Blogex.get_blog(:beta) == BetaBlog
end
test "an unknown blog_id returns nil and raises" do
assert Blogex.get_blog(:nope) == nil
assert_raise Blogex.NotFoundError, fn -> Blogex.get_blog!(:nope) end
end
end
describe "all_posts/0" do
# Newest first across both blogs: beta's 2026-03-15 post, then alpha's
# 2026-03-01. Drafts (a-draft) and future-dated posts (b-future) are not
# visible to readers, so neither appears.
test "merges published posts from every blog, newest first" do
assert ids(Blogex.all_posts()) == ["b1", "a1"]
end
end
describe "all_posts_unfiltered/0" do
# Everything, newest first: the 2099 scheduled post, then b1, the alpha
# draft, and alpha's oldest.
test "includes drafts and future-dated posts, newest first" do
assert ids(Blogex.all_posts_unfiltered()) == ["b-future", "b1", "a-draft", "a1"]
end
end
describe "all_tags/0" do
test "returns the unique tags across blogs, sorted" do
assert Blogex.all_tags() == ["devops", "elixir"]
end
end
defp ids(posts), do: Enum.map(posts, & &1.id)
end
+49
View File
@@ -0,0 +1,49 @@
defmodule Blogex.Test.AlphaBlog do
@moduledoc """
A minimal blog module for cross-blog tests.
Implements the interface `Blogex.Registry` queries (and nothing else), so a
test can configure `:blogex, :blogs` with real modules without compiling a
`use Blogex.Blog` module or touching the filesystem.
`all_posts/0`/`unfiltered_posts/0` are hard-coded — the dates are literals so
tests can assert exact orderings rather than re-deriving them.
"""
def blog_id, do: :alpha
def all_posts,
do: [Blogex.Test.PostBuilder.build(id: "a1", date: ~D[2026-03-01], blog: :alpha)]
def all_tags, do: ["elixir"]
def unfiltered_posts,
do: [
Blogex.Test.PostBuilder.build(id: "a1", date: ~D[2026-03-01], blog: :alpha),
Blogex.Test.PostBuilder.build(
id: "a-draft",
date: ~D[2026-03-05],
blog: :alpha,
published: false
)
]
end
defmodule Blogex.Test.BetaBlog do
@moduledoc """
The second blog module for cross-blog tests; see `Blogex.Test.AlphaBlog`.
"""
def blog_id, do: :beta
def all_posts,
do: [Blogex.Test.PostBuilder.build(id: "b1", date: ~D[2026-03-15], blog: :beta)]
def all_tags, do: ["devops"]
def unfiltered_posts,
do: [
Blogex.Test.PostBuilder.build(id: "b1", date: ~D[2026-03-15], blog: :beta),
Blogex.Test.PostBuilder.build(id: "b-future", date: ~D[2099-01-01], blog: :beta)
]
end