Commit 1709cb7 deleted the pinned hello-world post and ten tests
silently rotted with it - invisible until now because the suite could
not run in this clone (incomplete deps, then the broken frontmatter).
The slug, banner, og-meta and tag tests now derive slug and title from
the compiled posts at runtime (newest live post, newest draft), matching
the existing latest_post_with_links pattern, so content churn no longer
breaks them.
276 lines
10 KiB
Elixir
276 lines
10 KiB
Elixir
# Firehose blog controller tests
|
|
|
|
defmodule FirehoseWeb.BlogTest do
|
|
use FirehoseWeb.ConnCase
|
|
|
|
defp visit_blog_page(conn, blog_id, suffix \\ "") do
|
|
path = "/blog/#{blog_id}" <> suffix
|
|
body = conn |> get(path) |> html_response(200)
|
|
body
|
|
end
|
|
|
|
# Content churns; the behavior under test does not. The newest live post is
|
|
# a deterministic stand-in - the pinned "hello-world" post was deleted in
|
|
# 1709cb7 and the slug tests silently rotted with it.
|
|
defp a_live_post do
|
|
Enum.find(Firehose.EngineeringBlog.all_posts(), &(Blogex.Post.visibility(&1) == :live)) ||
|
|
raise "no live engineering post to exercise the post pages"
|
|
end
|
|
|
|
describe "engineering blog (HTML)" do
|
|
test "GET /blog/engineering returns HTML index with layout", %{conn: conn} do
|
|
body = visit_blog_page(conn, "engineering")
|
|
assert body =~ "Engineering Blog"
|
|
assert body =~ "firehose"
|
|
end
|
|
|
|
test "GET /blog/engineering/:slug returns HTML post with layout", %{conn: conn} do
|
|
post = a_live_post()
|
|
body = visit_blog_page(conn, "engineering", "/#{post.id}")
|
|
assert body =~ post.title
|
|
end
|
|
|
|
test "GET /blog/engineering?page=2 shows the next page of older posts", %{conn: conn} do
|
|
# Page size is 10 (`Blogex.Blog.paginate/2`, blogex/lib/blogex/blog.ex:121)
|
|
# and the test env lists drafts (`show_drafts: true`,
|
|
# app/config/config.exs), so the 34 visible engineering posts (the
|
|
# 2099-dated future post is excluded by `all_posts/0`) fill 4 pages.
|
|
# Page 2, date-descending, runs 2026-07-20 .. 2026-05-14.
|
|
body = visit_blog_page(conn, "engineering", "?page=2")
|
|
|
|
# The pager reports the page the reader asked for, not page 1
|
|
# (markup: blogex/lib/blogex/components.ex).
|
|
assert body =~ ~r/Page 2 of \d+/
|
|
|
|
# "Synthetic TDD" (2026-07-13) sits mid-page-2: five posts of headroom
|
|
# before new publications would push it to page 3. Matched as an anchor
|
|
# because the description of the page-2 post above it also says
|
|
# "Synthetic TDD".
|
|
assert body =~ ~s|>Synthetic TDD</a>|
|
|
|
|
# The newest post is unambiguously on page 1, so page 2 is a genuinely
|
|
# different set of posts rather than page 1 relabelled.
|
|
refute body =~ "I reduced the attack surface"
|
|
end
|
|
end
|
|
|
|
describe "release notes blog (HTML)" do
|
|
test "GET /blog/releases returns HTML index", %{conn: conn} do
|
|
body = visit_blog_page(conn, "releases")
|
|
assert body =~ "Release Notes"
|
|
assert body =~ "v0.1.0 Released"
|
|
end
|
|
|
|
test "GET /blog/releases/:slug returns HTML post", %{conn: conn} do
|
|
body = visit_blog_page(conn, "releases", "/v0-1-0")
|
|
assert body =~ "v0.1.0 Released"
|
|
end
|
|
end
|
|
|
|
describe "external links in compiled posts" do
|
|
# The marker markup is applied at compile time by Blogex.ExternalLinks
|
|
# (blogex/test/blogex/external_links_test.exs covers the rewrite itself);
|
|
# these tests verify the real compiled posts make it to the page.
|
|
defp latest_post_with_links,
|
|
do: Firehose.EngineeringBlog.get_post!("delayed-gratification-in-ai-summer")
|
|
|
|
test "wikipedia link carries target, rel and marker class in compiled body" do
|
|
post = latest_post_with_links()
|
|
|
|
assert post.body =~
|
|
~s(<a href="https://en.wikipedia.org/wiki/Attention_Is_All_You_Need" target="_blank" rel="noopener" class="blogex-external">)
|
|
end
|
|
|
|
test "every external anchor in compiled body is marked up" do
|
|
post = latest_post_with_links()
|
|
|
|
anchors = Regex.scan(~r/<a\s[^>]*>/, post.body) |> List.flatten()
|
|
|
|
external_anchors =
|
|
Enum.filter(anchors, &(&1 =~ ~r<href="https?://>))
|
|
|
|
assert length(external_anchors) > 1
|
|
|
|
assert Enum.all?(external_anchors, fn anchor ->
|
|
anchor =~ ~s(target="_blank") and
|
|
anchor =~ ~s(rel="noopener") and
|
|
anchor =~ ~s(class="blogex-external")
|
|
end)
|
|
end
|
|
|
|
test "internal links in compiled body are untouched" do
|
|
post = latest_post_with_links()
|
|
|
|
assert post.body =~
|
|
~s(<a href="/blog/engineering/swim-the-test">Swim the Test</a>)
|
|
end
|
|
|
|
test "rendered post page shows external links opening in a new tab", %{conn: conn} do
|
|
body =
|
|
conn |> get("/blog/engineering/delayed-gratification-in-ai-summer") |> html_response(200)
|
|
|
|
assert body =~ "target=\"_blank\" rel=\"noopener\" class=\"blogex-external\""
|
|
end
|
|
|
|
test "rendered post page carries the screen-reader hint for external links", %{conn: conn} do
|
|
body =
|
|
conn |> get("/blog/engineering/delayed-gratification-in-ai-summer") |> html_response(200)
|
|
|
|
assert body =~ ~s|<span class="blogex-external-hint"> (opens in a new tab)</span>|
|
|
end
|
|
end
|
|
|
|
describe "internal links in compiled posts" do
|
|
# `Blogex.Blog._validate_links/2` validates `post.body` after
|
|
# NimblePublisher rendered it to HTML. If extraction cannot read that HTML,
|
|
# every post looks link-free and compile-time validation is vacuous - which
|
|
# is the failure the real posts below pin against.
|
|
defp posts_with_internal_links do
|
|
Enum.filter(Firehose.EngineeringBlog.all_posts(), fn post ->
|
|
Blogex.LinkValidator.extract_links(post.body) != []
|
|
end)
|
|
end
|
|
|
|
test "the validator sees internal links in real compiled posts" do
|
|
refute posts_with_internal_links() == []
|
|
end
|
|
|
|
test "the post that links to swim-the-test exposes that link" do
|
|
post =
|
|
Enum.find(posts_with_internal_links(), &(&1.id == "delayed-gratification-in-ai-summer"))
|
|
|
|
assert post, "expected the compiled post to be visible with internal links extracted"
|
|
|
|
assert "/blog/engineering/swim-the-test" in Blogex.LinkValidator.extract_links(post.body)
|
|
end
|
|
|
|
test "no internal link in a real compiled post is invisible to extraction" do
|
|
# Deliberately permissive scan (any <a> whose href appears in any
|
|
# position, quoted or not): every /blog/ link it finds must also come out
|
|
# of extract_links/1, or a bad one could never be reported.
|
|
links_in_body = fn body ->
|
|
~r/<a\s[^>]*?href=["']?(\/blog\/[^"'\s>]+)/
|
|
|> Regex.scan(body)
|
|
|> Enum.map(fn [_, link] -> link end)
|
|
|> Enum.uniq()
|
|
end
|
|
|
|
for post <- Firehose.EngineeringBlog.all_posts(),
|
|
link <- links_in_body.(post.body) do
|
|
assert link in Blogex.LinkValidator.extract_links(post.body),
|
|
"#{post.id}: #{link} is in the compiled body but not extracted"
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "input validation" do
|
|
test "GET /blog/nonexistent returns 404", %{conn: conn} do
|
|
assert conn |> get("/blog/nonexistent") |> html_response(404)
|
|
end
|
|
|
|
test "GET /blog/engineering?page=abc falls back to page 1", %{conn: conn} do
|
|
assert conn |> get("/blog/engineering?page=abc") |> html_response(200) =~ "Engineering Blog"
|
|
end
|
|
|
|
test "GET /blog/engineering?page=-1 falls back to page 1", %{conn: conn} do
|
|
assert conn |> get("/blog/engineering?page=-1") |> html_response(200) =~ "Engineering Blog"
|
|
end
|
|
|
|
test "GET /blog/engineering?page=0 falls back to page 1", %{conn: conn} do
|
|
assert conn |> get("/blog/engineering?page=0") |> html_response(200) =~ "Engineering Blog"
|
|
end
|
|
|
|
test "GET /blog/engineering/nonexistent-post returns 404", %{conn: conn} do
|
|
assert_raise Blogex.NotFoundError, fn ->
|
|
get(conn, "/blog/engineering/nonexistent-post")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "engineering blog (JSON API)" do
|
|
test "GET /api/blog/engineering returns post index", %{conn: conn} do
|
|
assert %{"blog" => "engineering", "posts" => posts} =
|
|
conn
|
|
|> put_req_header("accept", "application/json")
|
|
|> get("/api/blog/engineering")
|
|
|> json_response(200)
|
|
|
|
assert is_list(posts)
|
|
refute Enum.empty?(posts)
|
|
end
|
|
|
|
test "GET /api/blog/engineering/:slug returns a post", %{conn: conn} do
|
|
post = a_live_post()
|
|
|
|
assert %{"id" => id, "title" => title} =
|
|
conn
|
|
|> put_req_header("accept", "application/json")
|
|
|> get("/api/blog/engineering/#{post.id}")
|
|
|> json_response(200)
|
|
|
|
assert id == post.id
|
|
assert title == post.title
|
|
end
|
|
|
|
test "GET /api/blog/engineering/:slug returns 404 for missing post", %{conn: conn} do
|
|
assert conn
|
|
|> put_req_header("accept", "application/json")
|
|
|> get("/api/blog/engineering/nonexistent")
|
|
|> response(404)
|
|
end
|
|
|
|
test "GET /api/blog/engineering/feed.xml returns RSS", %{conn: conn} do
|
|
response = conn |> get("/api/blog/engineering/feed.xml")
|
|
assert response(response, 200) =~ "<rss"
|
|
assert response_content_type(response, :xml)
|
|
end
|
|
|
|
test "GET /api/blog/engineering/tag/:tag returns JSON with posts", %{conn: conn} do
|
|
assert %{"blog" => "engineering", "tag" => "elixir", "posts" => posts} =
|
|
conn
|
|
|> put_req_header("accept", "application/json")
|
|
|> get("/api/blog/engineering/tag/elixir")
|
|
|> json_response(200)
|
|
|
|
assert is_list(posts)
|
|
end
|
|
end
|
|
|
|
describe "release notes blog (JSON API)" do
|
|
test "GET /api/blog/releases returns post index", %{conn: conn} do
|
|
assert %{"blog" => "release_notes", "posts" => posts} =
|
|
conn
|
|
|> put_req_header("accept", "application/json")
|
|
|> get("/api/blog/releases")
|
|
|> json_response(200)
|
|
|
|
assert is_list(posts)
|
|
refute Enum.empty?(posts)
|
|
end
|
|
|
|
test "GET /api/blog/releases/:slug returns a post", %{conn: conn} do
|
|
assert %{"id" => "v0-1-0", "title" => "v0.1.0 Released"} =
|
|
conn
|
|
|> put_req_header("accept", "application/json")
|
|
|> get("/api/blog/releases/v0-1-0")
|
|
|> json_response(200)
|
|
end
|
|
|
|
test "GET /api/blog/releases/feed.xml returns RSS", %{conn: conn} do
|
|
response = conn |> get("/api/blog/releases/feed.xml")
|
|
assert response(response, 200) =~ "<rss"
|
|
assert response_content_type(response, :xml)
|
|
end
|
|
|
|
test "GET /api/blog/releases/tag/:tag returns JSON with posts", %{conn: conn} do
|
|
assert %{"blog" => "release_notes", "tag" => "elixir", "posts" => posts} =
|
|
conn
|
|
|> put_req_header("accept", "application/json")
|
|
|> get("/api/blog/releases/tag/elixir")
|
|
|> json_response(200)
|
|
|
|
assert is_list(posts)
|
|
end
|
|
end
|
|
end
|