Files
DumiFlex-ComfyUI-Wildcard-P…/tests/wp_api/test_spa_fallback.py
T
DumiFlex 0a8f376f4f fix(api,engine): qa hardening sweep — validators, concurrency, orphans
Lands the 15-bug catalogue from the 2026-05-19 creative-QA pass.

API layer (wp_api/):
- New _validators.py centralises meta + identifier + body-size checks
  so modules and bundles share the same shape rules
- SPA catch-all now returns JSON 404 for unknown /wp/api/* paths
  instead of the SPA shell (debugging hostile previously)
- create/update/import_from_workflow validate meta (name/desc/tags/
  category_id) before hitting the repo; tag list now rejects non-str
  entries (was silently stored, breaking the SPA type contract)
- IntegrityError on dangling category_id surfaces as 400, not 500
- Optional If-Match header on PUT enables opt-in optimistic
  concurrency — stale version → 409 instead of silent overwrite
- Bundle name collisions auto-suffixed with (copy)/(copy N) mirroring
  the modules-side forkModule rule
- Bundle children deduped by id at create + update time
- Deleting a module flags meta.orphaned=true on every bundle child
  whose snapshot referenced it so the SPA can render a stale badge

Engine layer (engine/modules/*_handler.py):
- wildcard: reject empty + __dunder var_binding, cap at 64 chars,
  reject duplicate option ids
- combine: reject empty template, cap template at 8000 chars, cap
  output_var at 64, reject __dunder output_var
- fixed_values: cap value name length, reject __dunder names, reject
  duplicate names across rows (was silent last-write-wins)
- derivation: reject duplicate rule ids (was nondeterministic for
  disabled_rule_ids / rule_order_override lookups)

Tests:
- New test_qa_hardening.py exercises every endpoint-level fix
- Extended test_payload_validation.py parametrised rejection matrix
- New test_spa_fallback.py case for /wp/api/* JSON 404

655 pytest pass, 1665 vitest pass, ruff clean.
2026-05-19 20:12:41 +03:00

83 lines
3.1 KiB
Python

"""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(
"<!doctype html><html><head><title>Wildcard Pipeline</title></head>"
"<body><div id=\"app\"></div></body></html>",
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 "<title>" 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 "<title>" 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