175 lines
6.7 KiB
Python
175 lines
6.7 KiB
Python
from pathlib import Path
|
|
from typing import Generator
|
|
|
|
import pytest
|
|
from aiohttp import web
|
|
from aiohttp.test_utils import make_mocked_request
|
|
|
|
from app.library.config import Config
|
|
from app.library.router import ROUTES
|
|
from app.routes.api import _static
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_static_routes() -> Generator[None, None, None]:
|
|
Config._reset_singleton()
|
|
ROUTES.clear()
|
|
_static.STATIC_STATE.root = None
|
|
_static.STATIC_STATE.index_file = None
|
|
yield
|
|
_static.STATIC_STATE.root = None
|
|
_static.STATIC_STATE.index_file = None
|
|
ROUTES.clear()
|
|
Config._reset_singleton()
|
|
|
|
|
|
def _make_request(path: str, *, accept: str = "text/html") -> web.Request:
|
|
return make_mocked_request("GET", path, headers={"Accept": accept})
|
|
|
|
|
|
def _configure_static_root(static_root: Path) -> None:
|
|
_static.STATIC_STATE.root = static_root.resolve()
|
|
_static.STATIC_STATE.index_file = (_static.STATIC_STATE.root / "index.html").resolve()
|
|
|
|
|
|
class TestServeStaticFile:
|
|
@pytest.mark.asyncio
|
|
async def test_nested_doc_falls_back(self, tmp_path: Path) -> None:
|
|
config = Config.get_instance()
|
|
index_file = tmp_path / "index.html"
|
|
index_file.write_text("<html>root shell</html>", encoding="utf-8")
|
|
_configure_static_root(tmp_path)
|
|
|
|
response = await _static.serve_static_file(_make_request("/docs/readme"), config)
|
|
|
|
assert isinstance(response, web.FileResponse)
|
|
assert response._path == index_file
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_nested_index_uses_root(self, tmp_path: Path) -> None:
|
|
config = Config.get_instance()
|
|
root_index = tmp_path / "index.html"
|
|
nested_index = tmp_path / "docs" / "readme" / "index.html"
|
|
nested_index.parent.mkdir(parents=True)
|
|
root_index.write_text("<html>root shell</html>", encoding="utf-8")
|
|
nested_index.write_text("<html>nested shell</html>", encoding="utf-8")
|
|
_configure_static_root(tmp_path)
|
|
|
|
response = await _static.serve_static_file(_make_request("/docs/readme"), config)
|
|
|
|
assert isinstance(response, web.FileResponse)
|
|
assert response._path == root_index
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_asset_does_not_fall_back(self, tmp_path: Path) -> None:
|
|
config = Config.get_instance()
|
|
(tmp_path / "index.html").write_text("<html>root shell</html>", encoding="utf-8")
|
|
assets_dir = tmp_path / "assets"
|
|
assets_dir.mkdir()
|
|
_configure_static_root(tmp_path)
|
|
|
|
request = make_mocked_request(
|
|
"GET",
|
|
"/assets/missing.js",
|
|
headers={"Accept": "*/*", "Sec-Fetch-Dest": "script"},
|
|
)
|
|
response = await _static.serve_static_file(request, config)
|
|
|
|
assert isinstance(response, web.Response)
|
|
body = response.body
|
|
assert isinstance(body, bytes)
|
|
assert response.status == web.HTTPNotFound.status_code
|
|
assert b"missing.js" in body
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_unknown_no_fallback(self, tmp_path: Path) -> None:
|
|
config = Config.get_instance()
|
|
(tmp_path / "index.html").write_text("<html>root shell</html>", encoding="utf-8")
|
|
assets_dir = tmp_path / "assets"
|
|
assets_dir.mkdir()
|
|
_configure_static_root(tmp_path)
|
|
|
|
request = make_mocked_request(
|
|
"GET",
|
|
"/assets/missing.abcd123",
|
|
headers={"Accept": "*/*", "Sec-Fetch-Dest": "script"},
|
|
)
|
|
response = await _static.serve_static_file(request, config)
|
|
|
|
assert isinstance(response, web.Response)
|
|
body = response.body
|
|
assert isinstance(body, bytes)
|
|
assert response.status == web.HTTPNotFound.status_code
|
|
assert b"missing.abcd123" in body
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_symlink_outside_rejected(self, tmp_path: Path) -> None:
|
|
config = Config.get_instance()
|
|
(tmp_path / "index.html").write_text("<html>root shell</html>", encoding="utf-8")
|
|
outside_dir = tmp_path.parent / "outside-static-root"
|
|
outside_dir.mkdir(exist_ok=True)
|
|
outside_file = outside_dir / "secret.js"
|
|
outside_file.write_text("console.log('outside')", encoding="utf-8")
|
|
try:
|
|
(tmp_path / "leak.js").symlink_to(outside_file)
|
|
_configure_static_root(tmp_path)
|
|
|
|
response = await _static.serve_static_file(_make_request("/leak.js"), config)
|
|
|
|
assert isinstance(response, web.Response)
|
|
body = response.body
|
|
assert isinstance(body, bytes)
|
|
assert response.status == web.HTTPNotFound.status_code
|
|
assert b"/leak.js" in body
|
|
finally:
|
|
if (tmp_path / "leak.js").exists() or (tmp_path / "leak.js").is_symlink():
|
|
(tmp_path / "leak.js").unlink()
|
|
if outside_file.exists():
|
|
outside_file.unlink()
|
|
if outside_dir.exists():
|
|
outside_dir.rmdir()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_api_path_does_not_fall_back(self, tmp_path: Path) -> None:
|
|
config = Config.get_instance()
|
|
(tmp_path / "index.html").write_text("<html>root shell</html>", encoding="utf-8")
|
|
_configure_static_root(tmp_path)
|
|
|
|
response = await _static.serve_static_file(_make_request("/api/missing"), config)
|
|
|
|
assert isinstance(response, web.Response)
|
|
body = response.body
|
|
assert isinstance(body, bytes)
|
|
assert response.status == web.HTTPNotFound.status_code
|
|
assert b"/api/missing" in body
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dotted_browser_path_404(self, tmp_path: Path) -> None:
|
|
config = Config.get_instance()
|
|
(tmp_path / "index.html").write_text("<html>root shell</html>", encoding="utf-8")
|
|
_configure_static_root(tmp_path)
|
|
|
|
response = await _static.serve_static_file(_make_request("/browser/foo/bar.txt"), config)
|
|
|
|
assert isinstance(response, web.Response)
|
|
body = response.body
|
|
assert isinstance(body, bytes)
|
|
assert response.status == web.HTTPNotFound.status_code
|
|
assert b"/browser/foo/bar.txt" in body
|
|
|
|
def test_registers_root_routes(self, tmp_path: Path) -> None:
|
|
config = Config.get_instance()
|
|
static_root = tmp_path / "ui-exported"
|
|
static_root.mkdir()
|
|
(static_root / "index.html").write_text("<html>root shell</html>", encoding="utf-8")
|
|
(static_root / "assets").mkdir()
|
|
(static_root / "assets" / "app.js").write_text("console.log(1)", encoding="utf-8")
|
|
|
|
config.static_ui_path = str(static_root)
|
|
|
|
_static.setup_static_routes(tmp_path, config)
|
|
|
|
http_routes = ROUTES.get("http", {})
|
|
assert "index" in http_routes
|
|
assert "static_fallback" in http_routes
|
|
assert "/assets/app.js" not in {route.path for route in http_routes.values()}
|