caldav 3.x lists niquests as a mandatory dependency and prefers it over httpx. Passing httpx.BasicAuth via the auth= argument breaks under the niquests backend with "Unexpected non-callable authentication" — see #731. Switch CalendarClient.__init__ from auth=Auth|None to keyword-only password/token, and forward them to AsyncDAVClient as password= plus an explicit auth_type ("basic" or "bearer"). caldav then builds whichever auth object its active backend needs (niquests.auth.HTTPBasicAuth or httpx.BasicAuth), so we stay backend-agnostic. Threaded raw credentials through NextcloudClient — added keyword-only password/token to its __init__, and updated from_env, from_token, and the four call sites that build NextcloudClient (context.py basic-auth and Login Flow paths, auth/userinfo_routes.py, vector/oauth_sync.py). Four new unit tests pin the construction wiring so the niquests regression can't recur silently — basic, bearer, no-creds, and password-precedence cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
3.5 KiB
Python
100 lines
3.5 KiB
Python
"""Unit tests for the CalendarClient construction path.
|
|
|
|
These pin the wiring into ``caldav.aio.AsyncDAVClient``. caldav v3.x prefers
|
|
``niquests`` over ``httpx`` and rejects ``httpx.Auth`` objects when ``niquests``
|
|
is the active backend (issue #731), so we no longer build an httpx auth object
|
|
ourselves — we pass the raw credential plus an explicit ``auth_type`` and let
|
|
caldav build whichever auth its backend needs.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
def test_basic_auth_passes_password_and_auth_type_basic(mocker):
|
|
"""Password path: pass ``password=`` + ``auth_type='basic'``, no ``auth=`` arg.
|
|
|
|
The previous wiring passed ``auth=httpx.BasicAuth(...)`` which caldav-on-niquests
|
|
rejects with "Unexpected non-callable authentication" — the regression #731 came
|
|
in via caldav 3.x's mandatory niquests dependency.
|
|
"""
|
|
mock_dav_client = mocker.patch(
|
|
"nextcloud_mcp_server.client.calendar.AsyncDAVClient"
|
|
)
|
|
|
|
from nextcloud_mcp_server.client.calendar import CalendarClient
|
|
|
|
CalendarClient("https://cloud.example.org", "alice", password="app-pw-1234")
|
|
|
|
mock_dav_client.assert_called_once()
|
|
call_kwargs = mock_dav_client.call_args.kwargs
|
|
assert call_kwargs["url"] == "https://cloud.example.org/remote.php/dav/"
|
|
assert call_kwargs["username"] == "alice"
|
|
assert call_kwargs["password"] == "app-pw-1234"
|
|
assert call_kwargs["auth_type"] == "basic"
|
|
# Critical: no httpx.Auth object — that's what broke under niquests.
|
|
assert "auth" not in call_kwargs
|
|
|
|
|
|
def test_token_passes_token_and_auth_type_bearer(mocker):
|
|
"""Token path: pass ``password=<token>`` + ``auth_type='bearer'``.
|
|
|
|
caldav v3 reuses the ``password`` slot for bearer tokens — see
|
|
``async_davclient.build_auth_object``.
|
|
"""
|
|
mock_dav_client = mocker.patch(
|
|
"nextcloud_mcp_server.client.calendar.AsyncDAVClient"
|
|
)
|
|
|
|
from nextcloud_mcp_server.client.calendar import CalendarClient
|
|
|
|
CalendarClient("https://cloud.example.org", "alice", token="oauth-bearer-xyz")
|
|
|
|
call_kwargs = mock_dav_client.call_args.kwargs
|
|
assert call_kwargs["password"] == "oauth-bearer-xyz"
|
|
assert call_kwargs["auth_type"] == "bearer"
|
|
assert "auth" not in call_kwargs
|
|
|
|
|
|
def test_no_credentials_leaves_dav_client_unauthenticated(mocker):
|
|
"""Defensive: if neither credential is provided, don't pass any auth kwargs.
|
|
|
|
AsyncDAVClient handles its own discovery when no auth is configured; we
|
|
don't want to silently inject an empty password.
|
|
"""
|
|
mock_dav_client = mocker.patch(
|
|
"nextcloud_mcp_server.client.calendar.AsyncDAVClient"
|
|
)
|
|
|
|
from nextcloud_mcp_server.client.calendar import CalendarClient
|
|
|
|
CalendarClient("https://cloud.example.org", "alice")
|
|
|
|
call_kwargs = mock_dav_client.call_args.kwargs
|
|
assert "password" not in call_kwargs
|
|
assert "auth_type" not in call_kwargs
|
|
assert "auth" not in call_kwargs
|
|
|
|
|
|
def test_password_takes_precedence_over_token(mocker):
|
|
"""If a caller supplies both, password wins. Documents the precedence so a
|
|
future caller passing both isn't surprised by which one selects auth_type.
|
|
"""
|
|
mock_dav_client = mocker.patch(
|
|
"nextcloud_mcp_server.client.calendar.AsyncDAVClient"
|
|
)
|
|
|
|
from nextcloud_mcp_server.client.calendar import CalendarClient
|
|
|
|
CalendarClient(
|
|
"https://cloud.example.org",
|
|
"alice",
|
|
password="app-pw",
|
|
token="bearer-tok",
|
|
)
|
|
|
|
call_kwargs = mock_dav_client.call_args.kwargs
|
|
assert call_kwargs["password"] == "app-pw"
|
|
assert call_kwargs["auth_type"] == "basic"
|