test: add middleware ordering regression tests, just targets, fix base URL derivation

TestMiddlewareOrdering (6 tests): stacked WellKnown→BearerAuth integration
tests that prove the ordering invariant for issue #17. Includes a negative
test that explicitly verifies the wrong order (BearerAuth outer) produces
401 on /.well-known/ — the exact regression scenario.

Justfile: add test-http, test-http-no-auth, test-http-remote targets.

test-http.sh: harden BASE_URL derivation to handle trailing slashes and
non-/mcp URL suffixes correctly.

Auth test suite now: 42 tests (was 26).
This commit is contained in:
Jacob Magar 2026-04-03 23:06:33 -04:00
parent 9867b0a3a2
commit ebf0b0387b
3 changed files with 123 additions and 3 deletions

View file

@ -72,6 +72,19 @@ health:
test-live:
uv run pytest tests/ -v -m live
# Run HTTP end-to-end smoke-test against the local server (auto-reads token from ~/.unraid-mcp/.env)
test-http:
bash tests/mcporter/test-http.sh
# Run HTTP e2e test with auth disabled (for gateway-protected deployments)
test-http-no-auth:
bash tests/mcporter/test-http.sh --skip-auth
# Run HTTP e2e test against a remote URL
# Usage: just test-http-remote https://unraid.tootie.tv/mcp
test-http-remote url:
bash tests/mcporter/test-http.sh --url {{url}} --skip-auth
# ── Setup ─────────────────────────────────────────────────────────────────────
# Create .env from .env.example if missing

View file

@ -72,9 +72,19 @@ while [[ $# -gt 0 ]]; do
esac
done
# Derive base URL (strip /mcp suffix for non-tool endpoints)
BASE_URL="${MCP_URL%/mcp}"
[[ "$BASE_URL" == "$MCP_URL" ]] && BASE_URL="${MCP_URL%/}"
# Derive base URL: strip trailing /mcp (with or without trailing slash).
# Examples:
# http://localhost:6970/mcp → http://localhost:6970
# https://host/api/mcp/ → https://host/api
# https://host/mcp → https://host
# If the URL doesn't end in /mcp[/], use it as-is (strip trailing slash only).
_stripped="${MCP_URL%/}" # remove optional trailing slash
if [[ "$_stripped" == */mcp ]]; then
BASE_URL="${_stripped%/mcp}"
else
BASE_URL="$_stripped"
fi
unset _stripped
# ---------------------------------------------------------------------------
# Auto-detect token from ~/.unraid-mcp/.env if not supplied

View file

@ -608,3 +608,100 @@ class TestWellKnownMiddleware:
asyncio.get_event_loop().run_until_complete(mw(scope, receive, send))
assert called["value"]
# ---------------------------------------------------------------------------
# Stacked middleware integration — ordering invariant (regression for #17)
#
# WellKnownMiddleware MUST sit outside (before) BearerAuthMiddleware so that
# OAuth discovery endpoints are reachable without a token even when auth is
# enabled. If the order is swapped the well-known endpoint returns 401 and
# MCP clients (e.g. Claude Code) surface an "unknown error".
# ---------------------------------------------------------------------------
async def _run_stack(path: str, method: str = "GET", auth_header: bytes | None = None) -> int:
"""Run the production middleware stack and return the HTTP status code.
Stack (outermost to innermost): WellKnownMiddleware BearerAuthMiddleware app
"""
app, _ = _app_called_flag()
authed_app = BearerAuthMiddleware(app, token="test-token", disabled=False)
stacked = WellKnownMiddleware(authed_app)
headers: list[tuple[bytes, bytes]] = [(b"host", b"localhost:6970")]
if auth_header is not None:
headers.append((b"authorization", auth_header))
scope = {
"type": "http",
"method": method,
"path": path,
"scheme": "http",
"headers": headers,
"client": ("127.0.0.1", 9999),
}
received: list[dict] = []
async def receive():
return {"type": "http.request", "body": b"", "more_body": False}
async def send(msg: dict):
received.append(msg)
await stacked(scope, receive, send)
start = next((m for m in received if m["type"] == "http.response.start"), None)
return start["status"] if start else 200
class TestMiddlewareOrdering:
"""Regression tests for the WellKnown→BearerAuth ordering invariant."""
def _run(self, path, method="GET", auth_header=None):
return asyncio.get_event_loop().run_until_complete(
_run_stack(path, method=method, auth_header=auth_header)
)
def test_well_known_accessible_without_token(self):
"""Core regression: discovery must be reachable even with auth enabled."""
assert self._run("/.well-known/oauth-protected-resource") == 200
def test_well_known_mcp_subpath_accessible_without_token(self):
assert self._run("/.well-known/oauth-protected-resource/mcp") == 200
def test_mcp_endpoint_blocked_without_token(self):
"""Auth layer must still protect /mcp when no token is provided."""
assert self._run("/mcp") == 401
def test_mcp_endpoint_blocked_with_wrong_token(self):
assert self._run("/mcp", auth_header=b"Bearer wrong-token") == 401
def test_mcp_endpoint_passes_with_correct_token(self):
assert self._run("/mcp", auth_header=b"Bearer test-token") == 200
def test_swapped_order_would_block_well_known(self):
"""Negative test: wrong middleware order produces 401 on well-known (issue #17)."""
app, _ = _app_called_flag()
# Wrong: BearerAuth wraps WellKnown — auth fires before discovery can respond
wrong_stack = BearerAuthMiddleware(
WellKnownMiddleware(app), token="test-token", disabled=False
)
scope = {
"type": "http",
"method": "GET",
"path": "/.well-known/oauth-protected-resource",
"scheme": "http",
"headers": [(b"host", b"localhost:6970")],
"client": ("127.0.0.1", 9999),
}
received: list[dict] = []
async def receive():
return {"type": "http.request", "body": b"", "more_body": False}
async def send(msg: dict):
received.append(msg)
asyncio.get_event_loop().run_until_complete(wrong_stack(scope, receive, send))
start = next(m for m in received if m["type"] == "http.response.start")
assert start["status"] == 401, "Wrong order should produce 401 (the issue #17 regression)"