# frozen_string_literal: true require "test_helper" require "volumen/server" require "rack/test" require "tmpdir" require "fileutils" require "json" class ServerTest < Minitest::Test include Rack::Test::Methods def app Volumen::Server.configured(config: @config, store: @store) end def setup @dir = Dir.mktmpdir File.write(File.join(@dir, "hello.md"), <<~POST) +++ title = "Hello" tags = ["intro"] +++ Hello **world**. POST File.write(File.join(@dir, "draft.md"), <<~POST) +++ title = "Draft" draft = true +++ Secret. POST @config = Volumen::Config.load(path: "/nonexistent", overrides: { content: @dir }) @store = Volumen::Store.new(@dir, default_lang: "en") end def teardown FileUtils.remove_entry(@dir) end def test_site_endpoint get "/api/volumen/site" assert_predicate last_response, :ok? assert_equal "My Blog", JSON.parse(last_response.body)["title"] end def test_posts_excludes_drafts get "/api/volumen/posts" data = JSON.parse(last_response.body) assert_equal 1, data["total"] assert_equal "hello", data["posts"].first["slug"] end def test_post_detail_renders_html get "/api/volumen/posts/hello" assert_predicate last_response, :ok? assert_includes JSON.parse(last_response.body)["html"], "world" end def test_post_not_found get "/api/volumen/posts/missing" assert_equal 404, last_response.status assert_equal "not_found", JSON.parse(last_response.body)["error"] end def test_tags_endpoint get "/api/volumen/tags" assert_equal [{ "name" => "intro", "count" => 1 }], JSON.parse(last_response.body)["tags"] end def test_cors_header_present get "/api/volumen/site" header = last_response.headers["Access-Control-Allow-Origin"] || last_response.headers["access-control-allow-origin"] assert_equal "*", header end def test_all_langs_post_appears_in_other_languages File.write(File.join(@dir, "global.md"), <<~POST) +++ title = "Global" lang = "en" all_langs = true +++ Everywhere. POST get "/api/volumen/posts?lang=cs" slugs = JSON.parse(last_response.body)["posts"].map { |post| post["slug"] } assert_includes slugs, "global" refute_includes slugs, "hello" end def test_feed_endpoint get "/api/volumen/feed.xml" assert_predicate last_response, :ok? assert_equal "application/rss+xml", last_response["Content-Type"]&.split(";")&.first body = last_response.body assert_includes body, "Hello" refute_includes body, "Draft" assert_includes body, "en" end def test_draft_post_detail_is_not_found get "/api/volumen/posts/draft" assert_equal 404, last_response.status assert_equal "not_found", JSON.parse(last_response.body)["error"] end def test_sitemap_endpoint get "/api/volumen/sitemap.xml" assert_predicate last_response, :ok? assert_equal "application/xml", last_response["Content-Type"]&.split(";")&.first body = last_response.body assert_includes body, "" assert_includes body, "https://example.com/hello" refute_includes body, "draft" end def test_cors_preflight options "/api/volumen/site" assert_predicate last_response, :ok? assert_equal "*", last_response.headers["Access-Control-Allow-Origin"] assert_equal "GET, OPTIONS", last_response.headers["Access-Control-Allow-Methods"] end def test_scheduled_post_is_hidden future = (Date.today + 30).strftime("%Y-%m-%d") File.write(File.join(@dir, "later.md"), <<~POST) +++ title = "Later" publish_at = #{future} +++ Not yet. POST get "/api/volumen/posts" slugs = JSON.parse(last_response.body)["posts"].map { |post| post["slug"] } refute_includes slugs, "later" end def test_json_feed_endpoint get "/api/volumen/feed.json" assert_predicate last_response, :ok? data = JSON.parse(last_response.body) assert_equal "https://jsonfeed.org/version/1.1", data["version"] assert_equal "My Blog", data["title"] assert_equal 1, data["items"].length assert_equal "Hello", data["items"].first["title"] assert_includes data["items"].first["content_html"], "world" 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, "@rss@example.com" 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, "" 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) assert_equal 1, data["total"] assert_equal "hello", data["posts"].first["slug"] end def test_search_by_body get "/api/volumen/posts?q=world" data = JSON.parse(last_response.body) assert_equal 1, data["total"] end def test_search_no_match get "/api/volumen/posts?q=nonexistent" data = JSON.parse(last_response.body) assert_equal 0, data["total"] end def test_search_is_case_insensitive get "/api/volumen/posts?q=HELLO" data = JSON.parse(last_response.body) assert_equal 1, data["total"] end def test_pagination_has_next_and_prev # Create enough posts to paginate 5.times do |i| File.write(File.join(@dir, "post-#{i}.md"), <<~POST) +++ title = "Post #{i}" +++ Body #{i}. POST end get "/api/volumen/posts?page=1&limit=2" data = JSON.parse(last_response.body) assert_equal true, data["has_next"] assert_equal false, data["has_prev"] get "/api/volumen/posts?page=2&limit=2" data = JSON.parse(last_response.body) assert_equal true, data["has_next"] assert_equal true, data["has_prev"] get "/api/volumen/posts?page=3&limit=2" data = JSON.parse(last_response.body) assert_equal false, data["has_next"] assert_equal true, data["has_prev"] end end