Clients no longer need to compute page boundaries themselves; the API now returns boolean flags indicating whether more pages exist in each direction. Includes tests for first, middle, and last page.
305 lines
8.6 KiB
Ruby
305 lines
8.6 KiB
Ruby
# 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"], "<strong>world</strong>"
|
|
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, "<rss version=\"2.0\""
|
|
assert_includes body, "<title>Hello</title>"
|
|
refute_includes body, "Draft"
|
|
assert_includes body, "<language>en</language>"
|
|
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, "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">"
|
|
assert_includes body, "<loc>https://example.com/hello</loc>"
|
|
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"], "<strong>world</strong>"
|
|
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)
|
|
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
|