Files
olivv-cs-ComfyUI-FunPack/core/tests/test_routes.py
T
digital-garbage 058ca1a427 core: test the routes, and stop one try guarding two imports
routes.py had no coverage. register() now takes the route table so the real
routes mount on a throwaway app.

The tests write the request line onto the socket instead of using aiohttp's
TestClient, which normalises the URL before sending -- ask it for
modules/%2e%2e/app/x.css and the server receives a plain app/x.css, so the test
looks like a routing bypass when it is the client tidying up. On the wire the
modules route does match, dots intact, and resolve() refuses it.

Splitting the aiohttp and PromptServer imports: one try around both nulled
web whenever ComfyUI's server module was absent, which is every test run.
2026-08-28 14:31:49 +03:00

130 lines
4.4 KiB
Python

"""The routes, exercised through a real aiohttp router over a real socket.
test_serve_allowlist.py calls serve() with literal strings and cannot see what a
router does to a path before the handler runs, so routes.py had no coverage at
all. This closes that.
Requests are written onto the socket by hand rather than through aiohttp's
TestClient, because TestClient normalises the URL BEFORE sending: ask it for
`/funpack/modules/%2e%2e/app/boot.js` and the server receives a plain
`/funpack/app/boot.js`. A test written that way silently checks nothing about
traversal -- it looks like a routing bypass when it is only the client tidying up
after itself.
The loop is driven by hand rather than with pytest-asyncio: ComfyUI brings
aiohttp, and the suite should not need a plugin users would have to install.
"""
import asyncio
import pytest
pytest.importorskip("aiohttp")
from aiohttp import web # noqa: E402
from aiohttp.test_utils import TestServer # noqa: E402
from core import config, routes as routes_mod # noqa: E402
P = config.UI_PREFIX
@pytest.fixture()
def app(tmp_path, monkeypatch):
app_dir = tmp_path / "app"
(app_dir / "composer").mkdir(parents=True)
(app_dir / "boot.js").write_text("export const app = 1;")
(app_dir / "composer" / "composer.css").write_text(":root{}")
(app_dir / "index.html").write_text("<p>shell</p>")
mod_dir = tmp_path / "modules" / "timing" / "audio_clock"
mod_dir.mkdir(parents=True)
(mod_dir / "ui.js").write_text("export default 1;")
(mod_dir / "sneaky.css").write_text(".x{}")
monkeypatch.setattr(config, "APP_DIR", app_dir)
monkeypatch.setattr(config, "MODULES_DIR", tmp_path / "modules")
# A builder, not an Application: aiohttp binds an app to the loop that first
# ran it, and each request here gets its own asyncio.run().
def build():
table = web.RouteTableDef()
routes_mod.register(table)
application = web.Application()
application.add_routes(table)
return application
return build
def get(app, target):
"""(status, body) for a request line written verbatim onto the socket."""
async def go():
server = TestServer(app())
await server.start_server()
try:
reader, writer = await asyncio.open_connection("127.0.0.1", server.port)
writer.write(
f"GET {target} HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n".encode()
)
await writer.drain()
raw = await reader.read()
writer.close()
finally:
await server.close()
head, _, body = raw.partition(b"\r\n\r\n")
status = int(head.split(b"\r\n", 1)[0].split()[1])
return status, body.decode("utf-8", "replace")
return asyncio.run(go())
# --- traversal, as it actually arrives on the wire -------------------------
@pytest.mark.parametrize("target", [
P + "/modules/../app/boot.js",
P + "/modules/%2e%2e/app/boot.js",
P + "/modules/%2e%2e/app/composer/composer.css",
P + "/modules/%2e%2e%2f%2e%2e/app/boot.js",
P + "/app/../../etc/passwd",
P + "/app/%2e%2e/%2e%2e/etc/passwd",
])
def test_traversal_never_leaves_its_root(app, target):
status, body = get(app, target)
assert status in (403, 404), f"{target} -> {status} {body[:80]!r}"
def test_a_modules_url_cannot_reach_app_content(app):
# The route that matched must be the root that serves: a .css is reachable
# under /app/ and must stay unreachable under /modules/.
assert get(app, P + "/modules/../app/composer/composer.css")[0] in (403, 404)
assert get(app, P + "/app/composer/composer.css")[0] == 200
# --- the allowlist on ordinary URLs ----------------------------------------
def test_module_css_is_refused(app):
assert get(app, P + "/modules/timing/audio_clock/sneaky.css")[0] == 404
def test_module_js_is_served(app):
status, body = get(app, P + "/modules/timing/audio_clock/ui.js")
assert status == 200 and "export default" in body
def test_app_asset_is_served(app):
status, body = get(app, P + "/app/boot.js")
assert status == 200 and "export const app" in body
def test_query_string_does_not_change_the_target(app):
assert get(app, P + "/app/boot.js?v=2")[0] == 200
def test_index_is_served(app):
status, body = get(app, P + "/")
assert status == 200 and "shell" in body
def test_health(app):
status, body = get(app, P + "/api/health")
assert status == 200 and '"ok"' in body