"""PrefixProxy: the contract that lets daggr's canvas live under /canvas.
Every assertion here maps to a way a prefix-mounted frontend breaks. The rewrite rules are
deliberately narrow, so most of these tests are about what must NOT be touched.
"""
from __future__ import annotations
import httpx
import pytest
from fastapi import FastAPI, Response
from fastapi.responses import HTMLResponse, JSONResponse
from daggrstudio.web.proxy import PrefixProxy, strip_prefix
def make_inner() -> FastAPI:
app = FastAPI()
@app.get("/")
async def index() -> HTMLResponse:
return HTMLResponse(
'
inner'
''
''
""
)
@app.get("/assets/index-abc.js")
async def asset() -> Response:
return Response(
'const g = await fetch("/api/graph");'
'const ws = new WebSocket(`/ws/${id}`);'
'const png = "/file/tmp/a.png";',
media_type="application/javascript",
)
@app.get("/api/graph")
async def graph() -> JSONResponse:
# JSON must never be rewritten, even though it contains a path-like string
return JSONResponse({"url": "/api/graph", "path": "/assets/keep.js"})
@app.get("/tree/other")
async def other() -> HTMLResponse:
return HTMLResponse("other page")
@app.get("/stream")
async def stream() -> Response:
# no content-length -> the proxy must stream it through untouched
return Response(b"/api/should-not-change", media_type="application/javascript")
return app
def client_for(proxy: PrefixProxy) -> httpx.AsyncClient:
return httpx.AsyncClient(transport=httpx.ASGITransport(app=proxy), base_url="http://t")
@pytest.mark.asyncio
async def test_paths_are_stripped_before_forwarding():
proxy = PrefixProxy(make_inner(), "/canvas")
async with client_for(proxy) as c:
assert (await c.get("/canvas/")).status_code == 200
assert (await c.get("/canvas/api/graph")).status_code == 200
assert (await c.get("/canvas/tree/other")).status_code == 200
assert strip_prefix("/canvas/api/graph", "/canvas") == "/api/graph"
assert strip_prefix("/canvas", "/canvas") == "/"
assert strip_prefix("/elsewhere", "/canvas") == "/elsewhere"
@pytest.mark.asyncio
async def test_non_prefixed_paths_are_not_swallowed():
proxy = PrefixProxy(make_inner(), "/canvas")
async with client_for(proxy) as c:
resp = await c.get("/api/graph") # our own app's route must stay reachable
assert resp.status_code == 404
assert "canvas" in resp.json()["hint"]
@pytest.mark.asyncio
async def test_passthrough_mode_forwards_everything():
proxy = PrefixProxy(make_inner(), "/canvas", passthrough_other=True)
async with client_for(proxy) as c:
assert (await c.get("/api/graph")).status_code == 200
@pytest.mark.asyncio
async def test_html_is_rewritten_and_markup_injected():
proxy = PrefixProxy(make_inner(), "/canvas", inject="
back
")
async with client_for(proxy) as c:
resp = await c.get("/canvas/")
body = resp.text
assert 'href="/canvas/theme.css"' in body
assert 'src="/canvas/assets/index-abc.js"' in body
assert '' in body
assert "