Files
mcp-nextcloud/tests/unit/test_origin_normalise.py
T
Chris CoutinhoandClaude Opus 4.7 3a4fa8adc8 fix(auth): address PR #758 round-3 final review
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>
2026-05-02 23:14:07 +02:00

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