342 lines
11 KiB
Elixir
342 lines
11 KiB
Elixir
defmodule Blogex.LinkValidatorTest do
|
|
use ExUnit.Case
|
|
alias Blogex.LinkValidator
|
|
|
|
describe "extract_links/1" do
|
|
test "extracts internal blog links from markdown body" do
|
|
body =
|
|
"Check out [hello world](/blog/engineering/hello-world) and [release v1](/blog/releases/v1-0-0)."
|
|
|
|
assert LinkValidator.extract_links(body) == [
|
|
"/blog/engineering/hello-world",
|
|
"/blog/releases/v1-0-0"
|
|
]
|
|
end
|
|
|
|
test "extracts internal blog links from HTML body" do
|
|
body =
|
|
~s(<p>Check out <a href="/blog/engineering/hello-world">hello world</a> and <a href="/blog/releases/v1-0-0">release v1</a>.</p>)
|
|
|
|
assert LinkValidator.extract_links(body) == [
|
|
"/blog/engineering/hello-world",
|
|
"/blog/releases/v1-0-0"
|
|
]
|
|
end
|
|
|
|
test "extracts tag page links from HTML body" do
|
|
body = ~s(<p>See <a href="/blog/engineering/tag/pi.dev">pi.dev posts</a>.</p>)
|
|
|
|
assert LinkValidator.extract_links(body) == ["/blog/engineering/tag/pi.dev"]
|
|
end
|
|
|
|
# HTML anchors are not "the href right after <a (" - they are whatever the
|
|
# renderer emits. Pinned with the shapes a hand-rolled scan misses.
|
|
test "extracts internal blog links whatever the anchor's attributes look like" do
|
|
body =
|
|
~s(<p><a class="post-link" href="/blog/engineering/first">first</a>) <>
|
|
~s( <a href=/blog/releases/second>second</a>) <>
|
|
~s( <a data-x="1" aria-label="third" href='/blog/engineering/third'>third</a></p>)
|
|
|
|
assert LinkValidator.extract_links(body) == [
|
|
"/blog/engineering/first",
|
|
"/blog/releases/second",
|
|
"/blog/engineering/third"
|
|
]
|
|
end
|
|
|
|
test "ignores external links" do
|
|
body = "See [GitHub](https://github.com) and [internal](/blog/engineering/post)."
|
|
|
|
assert LinkValidator.extract_links(body) == ["/blog/engineering/post"]
|
|
end
|
|
|
|
test "ignores non-blog internal links" do
|
|
body = "See [/about](/about) and [/blog/engineering/post](/blog/engineering/post)."
|
|
|
|
assert LinkValidator.extract_links(body) == ["/blog/engineering/post"]
|
|
end
|
|
|
|
test "returns empty list when no internal blog links" do
|
|
body = "Just external links: [GitHub](https://github.com)."
|
|
|
|
assert LinkValidator.extract_links(body) == []
|
|
end
|
|
|
|
test "handles multiple links on one line" do
|
|
body = "[a](/blog/engineering/a) [b](/blog/releases/b) [c](/blog/engineering/c)"
|
|
|
|
assert LinkValidator.extract_links(body) == [
|
|
"/blog/engineering/a",
|
|
"/blog/releases/b",
|
|
"/blog/engineering/c"
|
|
]
|
|
end
|
|
|
|
test "handles links with query strings" do
|
|
body = "[link](/blog/engineering/post?foo=bar)"
|
|
|
|
assert LinkValidator.extract_links(body) == ["/blog/engineering/post?foo=bar"]
|
|
end
|
|
|
|
test "handles links with anchor fragments" do
|
|
body = "[link](/blog/engineering/post#section)"
|
|
|
|
assert LinkValidator.extract_links(body) == ["/blog/engineering/post#section"]
|
|
end
|
|
|
|
test "handles empty body" do
|
|
assert LinkValidator.extract_links("") == []
|
|
end
|
|
end
|
|
|
|
describe "validate_link/1" do
|
|
test "validates correct engineering link" do
|
|
assert LinkValidator.validate_link("/blog/engineering/my-post") == :ok
|
|
end
|
|
|
|
test "validates correct releases link" do
|
|
assert LinkValidator.validate_link("/blog/releases/v1-0-0") == :ok
|
|
end
|
|
|
|
test "rejects unknown blog ID" do
|
|
assert LinkValidator.validate_link("/blog/unknown/post") ==
|
|
{:error, "unknown blog ID: unknown"}
|
|
end
|
|
|
|
test "rejects uppercase blog ID" do
|
|
assert LinkValidator.validate_link("/blog/Engineering/post") ==
|
|
{:error, "unknown blog ID: Engineering"}
|
|
end
|
|
|
|
test "rejects empty slug" do
|
|
assert LinkValidator.validate_link("/blog/engineering/") ==
|
|
{:error, "empty slug"}
|
|
end
|
|
|
|
test "rejects slug with uppercase letters" do
|
|
assert LinkValidator.validate_link("/blog/engineering/My-Post") ==
|
|
{:error, "slug must be lowercase alphanumeric with hyphens: My-Post"}
|
|
end
|
|
|
|
test "rejects slug with special characters" do
|
|
assert LinkValidator.validate_link("/blog/engineering/hello@world") ==
|
|
{:error, "slug must be lowercase alphanumeric with hyphens: hello@world"}
|
|
end
|
|
|
|
test "rejects slug with spaces" do
|
|
assert LinkValidator.validate_link("/blog/engineering/hello world") ==
|
|
{:error, "slug must be lowercase alphanumeric with hyphens: hello world"}
|
|
end
|
|
|
|
test "allows single-word slug" do
|
|
assert LinkValidator.validate_link("/blog/engineering/hello") == :ok
|
|
end
|
|
|
|
test "allows hyphenated slug" do
|
|
assert LinkValidator.validate_link("/blog/engineering/my-cool-post") == :ok
|
|
end
|
|
|
|
test "allows slug with numbers" do
|
|
assert LinkValidator.validate_link("/blog/releases/v1-2-3") == :ok
|
|
end
|
|
|
|
test "rejects slug starting with hyphen" do
|
|
assert LinkValidator.validate_link("/blog/engineering/-post") ==
|
|
{:error, "slug must be lowercase alphanumeric with hyphens: -post"}
|
|
end
|
|
|
|
test "rejects slug ending with hyphen" do
|
|
assert LinkValidator.validate_link("/blog/engineering/post-") ==
|
|
{:error, "slug must be lowercase alphanumeric with hyphens: post-"}
|
|
end
|
|
|
|
test "rejects consecutive hyphens" do
|
|
assert LinkValidator.validate_link("/blog/engineering/post--name") ==
|
|
{:error, "slug must be lowercase alphanumeric with hyphens: post--name"}
|
|
end
|
|
|
|
test "returns :ok for link with query string and valid slug" do
|
|
assert LinkValidator.validate_link("/blog/engineering/post?foo=bar") == :ok
|
|
end
|
|
|
|
test "returns :ok for link with anchor fragment and valid slug" do
|
|
assert LinkValidator.validate_link("/blog/engineering/post#section") == :ok
|
|
end
|
|
|
|
test "allows tag page links" do
|
|
assert LinkValidator.validate_link("/blog/engineering/tag/pi.dev") == :ok
|
|
assert LinkValidator.validate_link("/blog/releases/tag/v1") == :ok
|
|
end
|
|
|
|
test "allows tag page links with fragments" do
|
|
assert LinkValidator.validate_link("/blog/engineering/tag/pi.dev#top") == :ok
|
|
end
|
|
|
|
# Tags are user-defined: the documented rule restricts *slugs*, not tags.
|
|
test "accepts tag links with dots and capitals" do
|
|
assert LinkValidator.validate_link("/blog/engineering/tag/pi.dev") == :ok
|
|
assert LinkValidator.validate_link("/blog/engineering/tag/Pi.dev") == :ok
|
|
end
|
|
|
|
test "rejects tag page link with empty tag" do
|
|
assert LinkValidator.validate_link("/blog/engineering/tag/") ==
|
|
{:error, "empty tag in tag link"}
|
|
end
|
|
|
|
test "rejects unknown blog ID in tag page link" do
|
|
assert LinkValidator.validate_link("/blog/unknown/tag/pi.dev") ==
|
|
{:error, "unknown blog ID: unknown"}
|
|
end
|
|
|
|
test "rejects non-blog path" do
|
|
assert LinkValidator.validate_link("/about") ==
|
|
{:error, "not a blog link: /about"}
|
|
end
|
|
|
|
test "rejects malformed link" do
|
|
assert LinkValidator.validate_link("not-a-url") ==
|
|
{:error, "not a blog link: not-a-url"}
|
|
end
|
|
end
|
|
|
|
describe "validate_links/1" do
|
|
test "returns :ok when all links are valid" do
|
|
links = [
|
|
"/blog/engineering/hello-world",
|
|
"/blog/releases/v1-0-0"
|
|
]
|
|
|
|
assert LinkValidator.validate_links(links) == :ok
|
|
end
|
|
|
|
test "returns errors for invalid links" do
|
|
links = [
|
|
"/blog/engineering/hello-world",
|
|
"/blog/unknown/post",
|
|
"/blog/releases/My-Post"
|
|
]
|
|
|
|
assert LinkValidator.validate_links(links) == {
|
|
:error,
|
|
[
|
|
{2, "/blog/unknown/post", "unknown blog ID: unknown"},
|
|
{3, "/blog/releases/My-Post",
|
|
"slug must be lowercase alphanumeric with hyphens: My-Post"}
|
|
]
|
|
}
|
|
end
|
|
|
|
test "returns :ok for empty list" do
|
|
assert LinkValidator.validate_links([]) == :ok
|
|
end
|
|
|
|
test "reports line numbers correctly" do
|
|
links = [
|
|
"/blog/engineering/ok",
|
|
"/blog/bad/slug",
|
|
"/blog/releases/ok"
|
|
]
|
|
|
|
assert LinkValidator.validate_links(links) == {
|
|
:error,
|
|
[{2, "/blog/bad/slug", "unknown blog ID: bad"}]
|
|
}
|
|
end
|
|
end
|
|
|
|
describe "validate_body/2" do
|
|
test "returns :ok when body has no internal blog links" do
|
|
body = "Just text, no links."
|
|
|
|
assert LinkValidator.validate_body(body, :engineering) == :ok
|
|
end
|
|
|
|
test "returns :ok when all links are valid" do
|
|
body = "[link](/blog/engineering/post)"
|
|
|
|
assert LinkValidator.validate_body(body, :engineering) == :ok
|
|
end
|
|
|
|
test "returns errors with post context" do
|
|
body = "[link](/blog/unknown/post)"
|
|
|
|
assert LinkValidator.validate_body(body, :engineering) == {
|
|
:error,
|
|
[
|
|
{
|
|
1,
|
|
"/blog/unknown/post",
|
|
"unknown blog ID: unknown",
|
|
post_id: nil
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
test "includes post_id in error tuples when provided" do
|
|
body = "[link](/blog/unknown/post)"
|
|
|
|
assert LinkValidator.validate_body(body, :engineering, post_id: "test-post") == {
|
|
:error,
|
|
[
|
|
{
|
|
1,
|
|
"/blog/unknown/post",
|
|
"unknown blog ID: unknown",
|
|
post_id: "test-post"
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
# `_validate_links/2` hands this module `post.body`, which NimblePublisher
|
|
# already rendered to HTML, so failure reporting has to work on HTML bodies.
|
|
# These are the two reports the compile-time integration tests depend on.
|
|
test "reports an unknown blog ID in an HTML body" do
|
|
body = ~s(<p><a href="/blog/unknown/broken">bad</a></p>)
|
|
|
|
assert LinkValidator.validate_body(body, :engineering, post_id: "html-post") == {
|
|
:error,
|
|
[
|
|
{
|
|
1,
|
|
"/blog/unknown/broken",
|
|
"unknown blog ID: unknown",
|
|
post_id: "html-post"
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
test "reports an invalid slug in an HTML body" do
|
|
body = ~s(<p><a href="/blog/engineering/My-Post">bad</a></p>)
|
|
|
|
assert LinkValidator.validate_body(body, :engineering, post_id: "html-post") == {
|
|
:error,
|
|
[
|
|
{
|
|
1,
|
|
"/blog/engineering/My-Post",
|
|
"slug must be lowercase alphanumeric with hyphens: My-Post",
|
|
post_id: "html-post"
|
|
}
|
|
]
|
|
}
|
|
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
|