176 lines
6.1 KiB
Python
176 lines
6.1 KiB
Python
"""Docs Gatekeeper API tests."""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from app.api.v1 import docs as docs_api
|
|
from app.main import app
|
|
from app.models.user import User
|
|
from app.services.docs_gatekeeper import DOCS_METADATA
|
|
|
|
|
|
def make_user(role: str = "viewer", groups: list[str] | None = None) -> User:
|
|
user = User(
|
|
id=1,
|
|
username="docs-user",
|
|
email="docs@example.com",
|
|
password_hash="x",
|
|
role=role,
|
|
is_active=True,
|
|
)
|
|
user.gatekeeper_groups = groups or []
|
|
return user
|
|
|
|
|
|
async def get_json(path: str, user: User | None = None):
|
|
if user is not None:
|
|
async def override_user():
|
|
return user
|
|
|
|
app.dependency_overrides[docs_api.get_optional_current_user] = override_user
|
|
|
|
transport = ASGITransport(app=app)
|
|
try:
|
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
return await client.get(path)
|
|
finally:
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_public_catalog_only_for_anonymous_user():
|
|
response = await get_json("/api/v1/docs/catalog")
|
|
|
|
assert response.status_code == 200
|
|
items = response.json()["items"]
|
|
assert {item["access"] for item in items} == {"public"}
|
|
zh_items = [item for item in items if item["lang"] == "zh"]
|
|
assert [item["slug"] for item in zh_items] == [
|
|
"overview",
|
|
"manual",
|
|
"quickstart",
|
|
"faq",
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_developer_catalog_includes_architecture_and_frontend_reference_docs():
|
|
response = await get_json(
|
|
"/api/v1/docs/catalog",
|
|
make_user(role="viewer", groups=["docs_developer"]),
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
zh_slugs = {item["slug"] for item in response.json()["items"] if item["lang"] == "zh"}
|
|
zh_items = [item for item in response.json()["items"] if item["lang"] == "zh"]
|
|
assert [item["group"] for item in zh_items[:4]] == ["Overview", "Manual", "Manual", "Manual"]
|
|
assert zh_items[4]["group"] == "Architecture"
|
|
assert "platform-data-flows" in zh_slugs
|
|
assert "naming-glossary" in zh_slugs
|
|
assert "tactile-ui-components" in zh_slugs
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_anonymous_can_read_public_doc():
|
|
response = await get_json("/api/v1/docs/zh/quickstart")
|
|
manual_response = await get_json("/api/v1/docs/zh/manual")
|
|
overview_response = await get_json("/api/v1/docs/zh/overview")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["access"] == "public"
|
|
assert "快速开始" in response.json()["markdown"]
|
|
assert manual_response.status_code == 200
|
|
assert manual_response.json()["access"] == "public"
|
|
assert overview_response.status_code == 200
|
|
assert overview_response.json()["access"] == "public"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_anonymous_protected_doc_requires_authentication():
|
|
response = await get_json("/api/v1/docs/zh/backend-collectors")
|
|
|
|
assert response.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_viewer_without_group_cannot_read_developer_doc():
|
|
response = await get_json(
|
|
"/api/v1/docs/zh/backend-collectors",
|
|
make_user(role="viewer"),
|
|
)
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_developer_group_can_read_developer_but_not_admin_doc():
|
|
user = make_user(role="viewer", groups=["docs_developer"])
|
|
|
|
developer_response = await get_json("/api/v1/docs/zh/backend-collectors", user)
|
|
tactile_response = await get_json("/api/v1/docs/zh/tactile-ui-components", user)
|
|
glossary_response = await get_json("/api/v1/docs/zh/naming-glossary", user)
|
|
admin_response = await get_json("/api/v1/docs/zh/backend-system-service-control", user)
|
|
|
|
assert developer_response.status_code == 200
|
|
assert developer_response.json()["access"] == "docs_developer"
|
|
assert tactile_response.status_code == 200
|
|
assert tactile_response.json()["access"] == "docs_developer"
|
|
assert glossary_response.status_code == 200
|
|
assert glossary_response.json()["access"] == "docs_developer"
|
|
assert admin_response.status_code == 403
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_and_super_admin_can_read_admin_docs():
|
|
admin_response = await get_json(
|
|
"/api/v1/docs/zh/backend-system-service-control",
|
|
make_user(role="admin"),
|
|
)
|
|
super_admin_response = await get_json(
|
|
"/api/v1/docs/zh/backend-system-service-control",
|
|
make_user(role="super_admin"),
|
|
)
|
|
|
|
assert admin_response.status_code == 200
|
|
assert super_admin_response.status_code == 200
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unknown_language_slug_and_path_traversal_do_not_read_files():
|
|
bad_lang = await get_json("/api/v1/docs/fr/quickstart")
|
|
bad_slug = await get_json("/api/v1/docs/zh/not-a-doc")
|
|
traversal = await get_json("/api/v1/docs/zh/..%2Fmanual")
|
|
|
|
assert bad_lang.status_code == 404
|
|
assert bad_slug.status_code == 404
|
|
assert traversal.status_code == 404
|
|
|
|
|
|
def test_public_docs_markdown_links_do_not_create_missing_docs_routes():
|
|
repo_root = Path(__file__).resolve().parents[2]
|
|
technical_root = repo_root / "docs" / "technical"
|
|
registered_filenames = {entry.filename for entry in DOCS_METADATA}
|
|
problems: list[str] = []
|
|
|
|
for markdown_path in sorted(technical_root.glob("*/*.md")):
|
|
markdown = markdown_path.read_text(encoding="utf-8")
|
|
for match in re.finditer(r"\[([^\]]+)]\(([^)]+\.md(?:#[^)]+)?)\)", markdown):
|
|
label, href = match.group(1), match.group(2)
|
|
if href.startswith(("http://", "https://", "mailto:")):
|
|
continue
|
|
|
|
href_without_hash = href.split("#", 1)[0].replace("\\", "/")
|
|
filename = Path(href_without_hash).name
|
|
if "/docs/technical/" in href_without_hash:
|
|
if filename not in registered_filenames:
|
|
problems.append(f"{markdown_path.relative_to(repo_root)} links unregistered public doc {href!r} ({label})")
|
|
continue
|
|
|
|
if href_without_hash.endswith(".md"):
|
|
problems.append(f"{markdown_path.relative_to(repo_root)} links non-public markdown {href!r} ({label})")
|
|
|
|
assert problems == []
|