"""The SPA catch-all serves only files under the shell root.

Until 2026-09-10 the catch-all's file branch joined the request path onto the shell
directory without resolving it. Uvicorn passes ".." through unnormalised (and decodes
"%2e%2e" to the same), so GET /../../../../etc/hostname answered 200 with the host's
file: any file on the container filesystem, to anyone past the gate, in every mode. The
explainer route ten lines up already confined its path. Found in the accounts plan's
review. A raw ASGI scope drives the app because the test client normalises dot
segments and would never reach the branch.
"""

import asyncio

from fastapi import FastAPI

from app import main
from app.db import get_db

SHELL = (
    "<!doctype html><html><head><title>t</title>"
    '<meta name="description" content="d" /></head>'
    '<body><div id="root"></div></body></html>'
)


def build(tmp_path):
    shell = tmp_path / "static"
    (shell / "assets").mkdir(parents=True)
    (shell / "index.html").write_text(SHELL)
    (shell / "logo.png").write_bytes(b"png-bytes")
    (tmp_path / "outside.txt").write_text("the file one level above the shell root")
    app = FastAPI()
    app.dependency_overrides[get_db] = lambda: None
    main.mount_site(app, shell)
    return app


def call(app, path: str) -> tuple[int, bytes]:
    scope = {
        "type": "http", "http_version": "1.1", "method": "GET", "scheme": "http",
        "path": path, "raw_path": path.encode(), "query_string": b"", "root_path": "",
        "headers": [(b"host", b"s.example")], "client": ("127.0.0.1", 1),
        "server": ("s.example", 80),
    }
    status: dict = {}
    body = bytearray()

    async def receive():
        return {"type": "http.request", "body": b"", "more_body": False}

    async def send(message):
        if message["type"] == "http.response.start":
            status["code"] = message["status"]
        elif message["type"] == "http.response.body":
            body.extend(message.get("body", b""))

    asyncio.run(app(scope, receive, send))
    return status["code"], bytes(body)


def test_a_root_file_is_served(tmp_path):
    code, body = call(build(tmp_path), "/logo.png")
    assert code == 200 and body == b"png-bytes"


def test_dot_segments_never_leave_the_shell_root(tmp_path):
    app = build(tmp_path)
    for path in ("/../outside.txt", "/assets/../../outside.txt", "/../../../../../../etc/hostname"):
        code, body = call(app, path)
        assert code == 404, path
        assert b"above the shell root" not in body, path
        assert b"png-bytes" not in body, path
