"""Unit tests for ``extract_user_id_from_token`` (PR #758 follow-up review). The function used to silently fall back to ``"default_user"`` whenever the verified access token had no ``sub`` claim. In a multi-tenant deployment that would let a malformed IdP token bucket every request under a single sentinel user, risking cross-tenant data exposure. The fix is to keep the no-token fallback (BasicAuth mode legitimately calls this without an OAuth identity) but raise ``McpError`` whenever an access token is present and ``resource`` is empty. """ import time from unittest.mock import MagicMock, patch import pytest from mcp.server.auth.provider import AccessToken from mcp.shared.exceptions import McpError from nextcloud_mcp_server.auth.token_utils import extract_user_id_from_token pytestmark = pytest.mark.unit def _token(resource: str | None = "alice") -> AccessToken: return AccessToken( token="t", client_id="test-client", scopes=["openid"], expires_at=int(time.time() + 3600), resource=resource, ) async def test_returns_user_id_when_token_has_sub(): """Happy path: verified access token with sub → returns the sub.""" with patch( "nextcloud_mcp_server.auth.token_utils.get_access_token", return_value=_token("alice"), ): user_id = await extract_user_id_from_token(MagicMock()) assert user_id == "alice" async def test_returns_default_user_when_no_access_token(): """BasicAuth mode: get_access_token() returns None → sentinel. BasicAuth deployments don't issue OAuth tokens; the sentinel lets BasicAuth-aware callers branch on it. Removing this fallback would break the BasicAuth path. """ with patch( "nextcloud_mcp_server.auth.token_utils.get_access_token", return_value=None, ): user_id = await extract_user_id_from_token(MagicMock()) assert user_id == "default_user" async def test_raises_when_token_present_but_resource_empty(): """Token present but ``resource`` empty → fail closed with McpError. Pins the PR #758 follow-up review fix: a malformed IdP token must not silently funnel users into a shared ``"default_user"`` SQLite bucket. """ with patch( "nextcloud_mcp_server.auth.token_utils.get_access_token", return_value=_token(""), ): with pytest.raises(McpError, match="Cannot determine user identity"): await extract_user_id_from_token(MagicMock()) async def test_raises_when_resource_is_none(): """Same fail-closed behaviour when ``resource`` is None rather than ''.""" with patch( "nextcloud_mcp_server.auth.token_utils.get_access_token", return_value=_token(None), ): with pytest.raises(McpError, match="Cannot determine user identity"): await extract_user_id_from_token(MagicMock())