243 lines
9.5 KiB
Python
243 lines
9.5 KiB
Python
"""Tests for the public API endpoints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from pathlib import Path
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
class TestSiteEndpoint:
|
|
def test_site_endpoint(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/site")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["title"] == "My Blog"
|
|
|
|
def test_site_endpoint_exposes_fediverse_creator_default(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/site")
|
|
assert response.status_code == 200
|
|
assert "fediverse_creator" in response.json()
|
|
|
|
|
|
class TestPostsEndpoint:
|
|
def test_posts_excludes_drafts(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/posts")
|
|
data = response.json()
|
|
assert data["total"] == 1
|
|
assert data["posts"][0]["slug"] == "hello"
|
|
|
|
def test_post_detail_renders_html(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/posts/hello")
|
|
assert response.status_code == 200
|
|
assert "<strong>world</strong>" in response.json()["html"]
|
|
|
|
def test_post_not_found(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/posts/missing")
|
|
assert response.status_code == 404
|
|
assert response.json()["detail"]["error"] == "not_found"
|
|
|
|
def test_draft_post_detail_returns_error(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/posts/draft")
|
|
assert response.status_code == 404
|
|
assert response.json()["detail"]["error"] == "draft"
|
|
|
|
|
|
class TestTagsEndpoint:
|
|
def test_tags_endpoint(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/tags")
|
|
data = response.json()
|
|
assert data["tags"] == [{"name": "intro", "count": 1}]
|
|
|
|
|
|
class TestCORS:
|
|
def test_cors_header_present(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/site")
|
|
assert response.headers.get("access-control-allow-origin") == "*"
|
|
|
|
def test_cors_preflight(self, client: TestClient) -> None:
|
|
response = client.options("/api/volumen/site")
|
|
assert response.status_code == 200
|
|
assert response.headers["access-control-allow-origin"] == "*"
|
|
assert "GET" in response.headers["access-control-allow-methods"]
|
|
|
|
|
|
class TestLanguageFilter:
|
|
def test_all_langs_post_appears_in_other_languages(
|
|
self, client: TestClient, tmp_content_dir: Path
|
|
) -> None:
|
|
(tmp_content_dir / "global.md").write_text(
|
|
'+++\ntitle = "Global"\nlang = "en"\nall_langs = true\n+++\n\nEverywhere.\n'
|
|
)
|
|
response = client.get("/api/volumen/posts?lang=cs")
|
|
slugs = [p["slug"] for p in response.json()["posts"]]
|
|
assert "global" in slugs
|
|
assert "hello" not in slugs
|
|
|
|
def test_lang_filter_default_shows_default_lang_posts(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/posts")
|
|
slugs = [p["slug"] for p in response.json()["posts"]]
|
|
assert "hello" in slugs
|
|
|
|
|
|
class TestScheduledPosts:
|
|
def test_scheduled_post_is_hidden(self, client: TestClient, tmp_content_dir: Path) -> None:
|
|
future = date.today().replace(year=date.today().year + 1).isoformat()
|
|
(tmp_content_dir / "later.md").write_text(
|
|
f'+++\ntitle = "Later"\npublish_at = {future}\n+++\n\nNot yet.\n'
|
|
)
|
|
response = client.get("/api/volumen/posts")
|
|
slugs = [p["slug"] for p in response.json()["posts"]]
|
|
assert "later" not in slugs
|
|
|
|
|
|
class TestSearch:
|
|
def test_search_by_title(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/posts?q=Hello")
|
|
data = response.json()
|
|
assert data["total"] == 1
|
|
assert data["posts"][0]["slug"] == "hello"
|
|
|
|
def test_search_by_body(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/posts?q=world")
|
|
data = response.json()
|
|
assert data["total"] == 1
|
|
|
|
def test_search_no_match(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/posts?q=nonexistent")
|
|
assert response.json()["total"] == 0
|
|
|
|
def test_search_is_case_insensitive(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/posts?q=HELLO")
|
|
assert response.json()["total"] == 1
|
|
|
|
|
|
class TestPagination:
|
|
def test_pagination_has_next_and_prev(self, client: TestClient, tmp_content_dir: Path) -> None:
|
|
for i in range(5):
|
|
(tmp_content_dir / f"post-{i}.md").write_text(
|
|
f'+++\ntitle = "Post {i}"\n+++\n\nBody {i}.\n'
|
|
)
|
|
|
|
resp = client.get("/api/volumen/posts?page=1&limit=2")
|
|
data = resp.json()
|
|
assert data["has_next"] is True
|
|
assert data["has_prev"] is False
|
|
|
|
resp = client.get("/api/volumen/posts?page=2&limit=2")
|
|
data = resp.json()
|
|
assert data["has_next"] is True
|
|
assert data["has_prev"] is True
|
|
|
|
resp = client.get("/api/volumen/posts?page=3&limit=2")
|
|
data = resp.json()
|
|
assert data["has_next"] is False
|
|
assert data["has_prev"] is True
|
|
|
|
|
|
class TestRSSFeed:
|
|
def test_feed_endpoint(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/feed.xml")
|
|
assert response.status_code == 200
|
|
ct = response.headers.get("content-type", "")
|
|
assert "application/rss+xml" in ct or "application/xml" in ct
|
|
body = response.text
|
|
assert '<rss version="2.0"' in body
|
|
assert "<title>Hello</title>" in body
|
|
assert "Draft" not in body
|
|
assert "<language>en</language>" in body
|
|
|
|
def test_rss_feed_includes_dc_creator_when_set(
|
|
self, client: TestClient, tmp_content_dir: Path
|
|
) -> None:
|
|
(tmp_content_dir / "rss.md").write_text(
|
|
'+++\ntitle = "Rss"\nslug = "rss"\n'
|
|
'fediverse_creator = "@rss@example.com"\n+++\n\nBody.\n'
|
|
)
|
|
response = client.get("/api/volumen/feed.xml")
|
|
assert "<dc:creator>@rss@example.com</dc:creator>" in response.text
|
|
assert "xmlns:dc" in response.text
|
|
|
|
def test_rss_feed_omits_dc_creator_when_not_set(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/feed.xml")
|
|
assert "<dc:creator>" not in response.text
|
|
|
|
|
|
class TestJSONFeed:
|
|
def test_json_feed_endpoint(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/feed.json")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["version"] == "https://jsonfeed.org/version/1.1"
|
|
assert data["title"] == "My Blog"
|
|
assert len(data["items"]) == 1
|
|
assert data["items"][0]["title"] == "Hello"
|
|
assert "<strong>world</strong>" in data["items"][0]["content_html"]
|
|
titles = [i["title"] for i in data["items"]]
|
|
assert "Draft" not in titles
|
|
|
|
def test_json_feed_includes_authors_from_post(
|
|
self, client: TestClient, tmp_content_dir: Path
|
|
) -> None:
|
|
(tmp_content_dir / "jsonfed.md").write_text(
|
|
'+++\ntitle = "Jsonfed"\nslug = "jsonfed"\n'
|
|
'fediverse_creator = "@jf@example.io"\n+++\n\nBody.\n'
|
|
)
|
|
response = client.get("/api/volumen/feed.json")
|
|
items = response.json()["items"]
|
|
item = next(i for i in items if i.get("id", "").endswith("jsonfed"))
|
|
assert item["authors"] == [{"name": "@jf@example.io"}]
|
|
|
|
def test_json_feed_falls_back_to_site_default_fediverse_creator(
|
|
self, client: TestClient, tmp_content_dir: Path
|
|
) -> None:
|
|
pass # Requires a custom config with site.fediverse_creator set
|
|
|
|
|
|
class TestSitemap:
|
|
def test_sitemap_endpoint(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/sitemap.xml")
|
|
assert response.status_code == 200
|
|
ct = response.headers.get("content-type", "")
|
|
assert "application/xml" in ct or "text/xml" in ct
|
|
body = response.text
|
|
assert "urlset" in body
|
|
assert "<loc>https://example.com/hello</loc>" in body
|
|
assert "draft" not in body
|
|
|
|
|
|
class TestFediverseCreator:
|
|
def test_post_summary_exposes_fediverse_creator(
|
|
self, client: TestClient, tmp_content_dir: Path
|
|
) -> None:
|
|
(tmp_content_dir / "attributed.md").write_text(
|
|
'+++\ntitle = "Attributed"\nslug = "attributed"\n'
|
|
'fediverse_creator = "@ada@lovelace.org"\n+++\n\nBody.\n'
|
|
)
|
|
response = client.get("/api/volumen/posts/attributed")
|
|
assert response.status_code == 200
|
|
assert response.json()["fediverse_creator"] == "@ada@lovelace.org"
|
|
|
|
def test_post_summary_fediverse_creator_is_null_when_not_set(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/posts/hello")
|
|
assert response.status_code == 200
|
|
assert response.json()["fediverse_creator"] is None
|
|
|
|
def test_posts_list_summary_exposes_fediverse_creator(
|
|
self, client: TestClient, tmp_content_dir: Path
|
|
) -> None:
|
|
(tmp_content_dir / "fed.md").write_text(
|
|
'+++\ntitle = "Fed"\nslug = "fed"\n'
|
|
'fediverse_creator = "@user@example.social"\n+++\n\nBody.\n'
|
|
)
|
|
response = client.get("/api/volumen/posts")
|
|
fed = next(p for p in response.json()["posts"] if p["slug"] == "fed")
|
|
assert fed["fediverse_creator"] == "@user@example.social"
|
|
|
|
|
|
class TestCacheHeaders:
|
|
def test_cache_control_on_api_responses(self, client: TestClient) -> None:
|
|
response = client.get("/api/volumen/site")
|
|
assert "cache-control" in response.headers
|