yak(behavior-tests-blogex-public-api-and-router-plumbing-q1fb): behavior tests: blogex public API and router plumbing
This commit is contained in:
@@ -232,10 +232,10 @@ defmodule Blogex.LinkValidator do
|
||||
@doc false
|
||||
@spec slug_end(String.t()) :: integer()
|
||||
defp slug_end(str) do
|
||||
case String.split(str, ["?", "#"], parts: 2) do
|
||||
[slug | _] -> String.length(slug)
|
||||
_ -> String.length(str)
|
||||
end
|
||||
# `String.split/3` always returns a non-empty list whose head is a binary,
|
||||
# so there is no empty/other case to handle here.
|
||||
[slug | _] = String.split(str, ["?", "#"], parts: 2)
|
||||
String.length(slug)
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
||||
+11
-7
@@ -56,18 +56,22 @@ defmodule Blogex.MixProject do
|
||||
# in blogex/). Rule: the threshold is a FLOOR. Raise it as behavior-test yaks
|
||||
# land; never lower it. A drop means the change (or an exclusion) is wrong.
|
||||
#
|
||||
# Measured 2026-10-26, with the test scaffolding below excluded: 74.53% — the
|
||||
# Measured 2026-10-28, with the test scaffolding below excluded: 78.59% — the
|
||||
# same via `mix test --cover` and via `mix test --cover --export-coverage
|
||||
# default` + `mix test.coverage`. Floor is the integer below that.
|
||||
#
|
||||
# Excluded: the test scaffolding (Blogex.Test.*) only. Fixtures measure the
|
||||
# tests, not the library. Everything in lib/blogex counts, and today's gaps
|
||||
# are real: Blogex (the delegating facade), Blogex.Components and
|
||||
# Blogex.Layout are at 0% because the router tests only exercise the JSON
|
||||
# responses — moving this floor up is the behavior-test work.
|
||||
# Excluded: the test scaffolding in `ignore_modules` below. The two registry
|
||||
# blog doubles (Blogex.Test.AlphaBlog/BetaBlog) are deliberately not listed
|
||||
# there: they are fully exercised, and excluding them measures 78.06% — the
|
||||
# 78 floor holds either way.
|
||||
#
|
||||
# Everything in lib/blogex counts, and today's gaps are real:
|
||||
# Blogex.Components and Blogex.Layout are at 0% because the router tests only
|
||||
# exercise the JSON responses — covering the rendered HTML is its own yak.
|
||||
# Blogex.Router (72.06%) still has its HTML arms and `match _` uncovered.
|
||||
defp test_coverage do
|
||||
[
|
||||
summary: [threshold: 74],
|
||||
summary: [threshold: 78],
|
||||
ignore_modules: [
|
||||
Blogex.Test.FakeBlog,
|
||||
Blogex.Test.PostBuilder,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
%{
|
||||
title: "Draft Post (test fixture)",
|
||||
author: "Test Author",
|
||||
description: "A draft used by the show_drafts? tests",
|
||||
published: false
|
||||
}
|
||||
---
|
||||
This post is a draft and is hidden unless :show_drafts is on.
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user