Seven findings from the latest review on #758, plus a regression test catching the substance of the cache-stampede fix: - verify_id_token: widen id_token annotation to str | None to match callers passing nc_token_response.get("id_token") - extract_user_id_from_token: use JSON-RPC reserved error code -32001 instead of -1 - _get_cached: per-URL anyio.Lock dict + meta-lock coalesces concurrent cache misses into a single IdP fetch (mirrors token_broker.py idiom) - delete_browser_session: collapse SELECT+DELETE into atomic DELETE ... RETURNING user_id (SQLite >= 3.35) - new test_origin_normalise.py: parametrized port/scheme/host equivalence cases for the CSRF Origin guard - browser_oauth_routes: correct misleading "PR #758 finding 5" cross- references (finding 5 was Fernet-key hardening, not CSRF) - ASProxySession.nonce: make required, drop spurious "legacy session" default; reword the in-flight `or None` comment to reflect that ASProxySession is purely in-memory - new test_get_cached_coalesces_concurrent_misses: pins the cache-stampede protection — fires 10 concurrent _get_cached calls and asserts exactly one HTTP fetch Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
42 lines
1.8 KiB
Python
42 lines
1.8 KiB
Python
"""Tests for ``_normalise_origin`` port + scheme + host normalisation.
|
|
|
|
The CSRF guard on POST /oauth/logout (PR #758 round-3 review hardening)
|
|
compares ``Origin`` / ``Referer`` against the configured ``mcp_server_url``
|
|
via ``_normalise_origin``. RFC 6454 §6.2 says browsers omit default ports
|
|
(80 for http, 443 for https) from Origin headers, so the function strips
|
|
those before comparison. These tests pin that behaviour so it can't
|
|
silently regress.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from nextcloud_mcp_server.auth.browser_oauth_routes import _normalise_origin
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"left, right, equal",
|
|
[
|
|
# Default ports are stripped — these MUST compare equal.
|
|
("https://example.com", "https://example.com:443", True),
|
|
("https://example.com:443", "https://example.com", True),
|
|
("http://example.com", "http://example.com:80", True),
|
|
("http://example.com:80", "http://example.com", True),
|
|
# Non-default ports are preserved.
|
|
("https://example.com:8443", "https://example.com", False),
|
|
("http://example.com:8080", "http://example.com", False),
|
|
("https://example.com:8443", "https://example.com:443", False),
|
|
# Cross-scheme defaults don't collapse (https:443 != http:80 even
|
|
# though both ports get stripped, because the scheme differs).
|
|
("https://example.com", "http://example.com", False),
|
|
("https://example.com:443", "http://example.com:80", False),
|
|
# Hostname matters and is case-insensitive.
|
|
("https://example.com", "https://other.com", False),
|
|
("https://example.com", "https://EXAMPLE.COM", True),
|
|
("https://Example.Com:443", "https://example.com", True),
|
|
],
|
|
)
|
|
def test_normalise_origin_equivalence(left: str, right: str, equal: bool):
|
|
assert (_normalise_origin(left) == _normalise_origin(right)) is equal
|