feat: add fediverse_creator handle for Mastodon attribution
Expose a per-post fediverse_creator frontmatter field plus a site-wide default in config.toml ([site].fediverse_creator). The handle is returned in the post and site payloads, rendered as <dc:creator> in the RSS feed and as authors[].name in the JSON feed, so a front-end can drop it into <meta name="fediverse:creator"> on rendered pages. Per-post value overrides the site default. Validated against a simple @user@host regex. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -77,4 +77,61 @@ class PostTest < Minitest::Test
|
||||
post = Volumen::Post.new(metadata: {})
|
||||
refute_predicate post, :scheduled?
|
||||
end
|
||||
|
||||
def test_fediverse_creator_returns_metadata_value
|
||||
post = Volumen::Post.new(
|
||||
metadata: { "slug" => "x", "fediverse_creator" => "@user@example.social" }
|
||||
)
|
||||
assert_equal "@user@example.social", post.fediverse_creator
|
||||
end
|
||||
|
||||
def test_fediverse_creator_is_nil_when_missing
|
||||
post = Volumen::Post.new(metadata: { "slug" => "x" })
|
||||
assert_nil post.fediverse_creator
|
||||
end
|
||||
|
||||
def test_fediverse_creator_is_nil_when_blank
|
||||
post = Volumen::Post.new(metadata: { "slug" => "x", "fediverse_creator" => " " })
|
||||
assert_nil post.fediverse_creator
|
||||
end
|
||||
|
||||
def test_fediverse_creator_validation_accepts_well_formed_handles
|
||||
post = Volumen::Post.new(
|
||||
metadata: { "slug" => "x", "fediverse_creator" => "@ada@lovelace.org" }
|
||||
)
|
||||
assert_predicate post, :valid_fediverse_creator?
|
||||
end
|
||||
|
||||
def test_fediverse_creator_validation_rejects_malformed_handles
|
||||
[
|
||||
"ada@example.com",
|
||||
"@only-one-at",
|
||||
"@user@example.com extra",
|
||||
"@user@",
|
||||
"@@example.com"
|
||||
].each do |value|
|
||||
post = Volumen::Post.new(metadata: { "slug" => "x", "fediverse_creator" => value })
|
||||
refute_predicate post, :valid_fediverse_creator?, "expected #{value.inspect} to be invalid"
|
||||
end
|
||||
end
|
||||
|
||||
def test_fediverse_creator_validation_accepts_padded_value
|
||||
post = Volumen::Post.new(
|
||||
metadata: { "slug" => "x", "fediverse_creator" => " @user@example.com " }
|
||||
)
|
||||
assert_predicate post, :valid_fediverse_creator?
|
||||
end
|
||||
|
||||
def test_summary_includes_fediverse_creator
|
||||
post = Volumen::Post.new(
|
||||
metadata: { "slug" => "x", "fediverse_creator" => "@user@example.social" },
|
||||
body: "body"
|
||||
)
|
||||
assert_equal "@user@example.social", post.summary["fediverse_creator"]
|
||||
end
|
||||
|
||||
def test_summary_omits_fediverse_creator_when_absent
|
||||
post = Volumen::Post.new(metadata: { "slug" => "x" }, body: "body")
|
||||
assert_nil post.summary["fediverse_creator"]
|
||||
end
|
||||
end
|
||||
|
||||
+97
-1
@@ -98,7 +98,7 @@ class ServerTest < Minitest::Test
|
||||
assert_predicate last_response, :ok?
|
||||
assert_equal "application/rss+xml", last_response["Content-Type"]&.split(";")&.first
|
||||
body = last_response.body
|
||||
assert_includes body, "<rss version=\"2.0\">"
|
||||
assert_includes body, "<rss version=\"2.0\""
|
||||
assert_includes body, "<title>Hello</title>"
|
||||
refute_includes body, "Draft"
|
||||
assert_includes body, "<language>en</language>"
|
||||
@@ -154,6 +154,102 @@ class ServerTest < Minitest::Test
|
||||
refute_includes data["items"].map { |i| i["title"] }, "Draft"
|
||||
end
|
||||
|
||||
def test_post_summary_exposes_fediverse_creator
|
||||
File.write(File.join(@dir, "attributed.md"), <<~POST)
|
||||
+++
|
||||
title = "Attributed"
|
||||
slug = "attributed"
|
||||
fediverse_creator = "@ada@lovelace.org"
|
||||
+++
|
||||
|
||||
Body.
|
||||
POST
|
||||
get "/api/volumen/posts/attributed"
|
||||
assert_predicate last_response, :ok?
|
||||
data = JSON.parse(last_response.body)
|
||||
assert_equal "@ada@lovelace.org", data["fediverse_creator"]
|
||||
end
|
||||
|
||||
def test_post_summary_fediverse_creator_is_null_when_not_set
|
||||
get "/api/volumen/posts/hello"
|
||||
assert_predicate last_response, :ok?
|
||||
assert_nil JSON.parse(last_response.body)["fediverse_creator"]
|
||||
end
|
||||
|
||||
def test_posts_list_summary_exposes_fediverse_creator
|
||||
File.write(File.join(@dir, "fed.md"), <<~POST)
|
||||
+++
|
||||
title = "Fed"
|
||||
slug = "fed"
|
||||
fediverse_creator = "@user@example.social"
|
||||
+++
|
||||
|
||||
Body.
|
||||
POST
|
||||
get "/api/volumen/posts"
|
||||
data = JSON.parse(last_response.body)
|
||||
fed = data["posts"].find { |post| post["slug"] == "fed" }
|
||||
assert_equal "@user@example.social", fed["fediverse_creator"]
|
||||
end
|
||||
|
||||
def test_site_endpoint_exposes_fediverse_creator_default
|
||||
get "/api/volumen/site"
|
||||
assert_predicate last_response, :ok?
|
||||
assert JSON.parse(last_response.body).key?("fediverse_creator")
|
||||
end
|
||||
|
||||
def test_rss_feed_includes_dc_creator_when_set
|
||||
File.write(File.join(@dir, "rss.md"), <<~POST)
|
||||
+++
|
||||
title = "Rss"
|
||||
slug = "rss"
|
||||
fediverse_creator = "@rss@example.com"
|
||||
+++
|
||||
|
||||
Body.
|
||||
POST
|
||||
get "/api/volumen/feed.xml"
|
||||
assert_includes last_response.body, "<dc:creator>@rss@example.com</dc:creator>"
|
||||
assert_includes last_response.body, "xmlns:dc=\"http://purl.org/dc/elements/1.1/\""
|
||||
end
|
||||
|
||||
def test_rss_feed_omits_dc_creator_when_not_set
|
||||
get "/api/volumen/feed.xml"
|
||||
refute_includes last_response.body, "<dc:creator>"
|
||||
end
|
||||
|
||||
def test_json_feed_includes_authors_from_post
|
||||
File.write(File.join(@dir, "jsonfed.md"), <<~POST)
|
||||
+++
|
||||
title = "Jsonfed"
|
||||
slug = "jsonfed"
|
||||
fediverse_creator = "@jf@example.io"
|
||||
+++
|
||||
|
||||
Body.
|
||||
POST
|
||||
get "/api/volumen/feed.json"
|
||||
item = JSON.parse(last_response.body)["items"].find { |i| i["id"] == "https://example.com/jsonfed" }
|
||||
assert_equal [{ "name" => "@jf@example.io" }], item["authors"]
|
||||
end
|
||||
|
||||
def test_json_feed_falls_back_to_site_default_fediverse_creator
|
||||
Dir.chdir(@dir) do
|
||||
File.write("config.toml", <<~TOML)
|
||||
[site]
|
||||
fediverse_creator = "@owner@example.com"
|
||||
TOML
|
||||
end
|
||||
@config = Volumen::Config.load(
|
||||
path: File.join(@dir, "config.toml"),
|
||||
overrides: { content: @dir }
|
||||
)
|
||||
get "/api/volumen/feed.json"
|
||||
items = JSON.parse(last_response.body)["items"]
|
||||
expected = [{ "name" => "@owner@example.com" }]
|
||||
assert(items.all? { |i| i["authors"] == expected })
|
||||
end
|
||||
|
||||
def test_search_by_title
|
||||
get "/api/volumen/posts?q=Hello"
|
||||
data = JSON.parse(last_response.body)
|
||||
|
||||
Reference in New Issue
Block a user