# 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| # 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() end test "every external anchor in compiled body is marked up" do post = latest_post_with_links() anchors = Regex.scan(~r/]*>/, post.body) |> List.flatten() external_anchors = Enum.filter(anchors, &(&1 =~ ~r)) 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(Swim the Test) 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| (opens in a new tab)| 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 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/]*?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) =~ " "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) =~ " "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