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 "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 "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 end end