"""Tests for /wp + /wp/{path:.*} static + SPA fallback behavior."""
import pytest
pytestmark = pytest.mark.asyncio
@pytest.fixture
def stub_web_dir(tmp_path, monkeypatch):
"""Provide a temporary `web/` directory with a minimal index.html.
The real WEB_DIR points at the build output (`web/index.html` + js
bundle) which lives next to `wp_api/`. CI runs without a frontend
build, so the directory doesn't exist — every static-serve test
would hit a FileNotFoundError. Monkeypatch `WEB_DIR` to a tmp path
that we populate with a stub index so the SPA-fallback handler has
something to serve.
"""
from wp_api import spa as spa_mod
web_dir = tmp_path / "web"
web_dir.mkdir()
(web_dir / "index.html").write_text(
"
Wildcard Pipeline"
"",
encoding="utf-8",
)
monkeypatch.setattr(spa_mod, "WEB_DIR", web_dir)
return web_dir
async def test_root_serves_index_html(wp_client, stub_web_dir):
resp = await wp_client.get("/wp")
assert resp.status == 200
body = await resp.text()
assert "" in body.lower()
assert "wildcard pipeline" in body.lower()
async def test_unknown_path_falls_back_to_index_html(wp_client, stub_web_dir):
resp = await wp_client.get("/wp/modules/some-future-route")
assert resp.status == 200
body = await resp.text()
assert "" in body.lower()
async def test_static_asset_served_when_present(wp_client, stub_web_dir):
"""Static files in WEB_DIR are served as-is, not as the SPA shell."""
asset = stub_web_dir / "favicon.txt"
asset.write_text("hello", encoding="utf-8")
resp = await wp_client.get("/wp/favicon.txt")
assert resp.status == 200
assert (await resp.text()) == "hello"
async def test_path_traversal_blocked(wp_client):
"""Requests with .. in the path must not escape WEB_DIR."""
resp = await wp_client.get("/wp/../pyproject.toml")
# aiohttp's router normalizes path traversal before reaching the handler;
# the request resolves to /pyproject.toml which is not registered → 404.
# If it ever reaches our handler, the resolve check returns 403.
assert resp.status in (403, 404)
async def test_api_routes_not_shadowed(wp_client):
"""The catch-all /wp/{path:.*} must not shadow specific /wp/api/* routes."""
resp = await wp_client.get("/wp/api/modules")
assert resp.status == 200
body = await resp.json()
assert "items" in body
async def test_unknown_api_path_returns_json_404(wp_client):
"""Unknown `/wp/api/...` paths must NOT fall through to the SPA shell.
Pre-fix `GET /wp/api/this/does/not/exist` returned 200 text/html
(the SPA index), making client-side debugging hostile — the
JSON.parse failure masked the real "no such route" error.
"""
resp = await wp_client.get("/wp/api/this/path/does/not/exist")
assert resp.status == 404
assert resp.headers.get("Content-Type", "").startswith("application/json")
body = await resp.json()
assert "error" in body