Files
mcp-nextcloud/tests/integration/test_app_password_provisioning.py
T
Chris CoutinhoandClaude Opus 4.7 65345fd6eb refactor: drop OAuth-refresh background-sync path from oauth_sync.py
Follow-up to #787/#789 (ADR-022 cleanup). After
`oauth_enabled ↔ enable_login_flow` became an invariant, the
`use_basic_auth=False` branch in `vector/oauth_sync.py` — and the
parameter wiring that fed it — was no longer reachable from any
supported deployment mode. This commit removes the dead code.

- nextcloud_mcp_server/vector/oauth_sync.py:
  - Deleted `get_user_client_oauth` (the OAuth-token refresh helper) and
    its `VECTOR_SYNC_SCOPES` constant.
  - Deleted the `get_user_client` dispatcher. Internal callers now call
    `get_user_client_basic_auth` directly.
  - Dropped the `use_basic_auth: bool` parameter from `user_scanner_task`,
    `multi_user_processor_task`, `_run_user_scanner_with_scope`, and
    `user_manager_task`.
  - Dropped the `token_broker` parameter from the same four functions —
    they no longer need it now that the OAuth-refresh path is gone. The
    `TokenBrokerService` constructed in `app.py` is still used by the
    management API revoke endpoint, just not by background sync.
  - Simplified the user-list query in `user_manager_task` to always read
    from the `app_passwords` table.
  - Replaced all `mode_label = "BasicAuth" if use_basic_auth else "OAuth"`
    with a literal `[BasicAuth]` log prefix (keeps existing log filters
    working).
  - Updated the module docstring to describe the post-cleanup shape.
  - Dropped the now-unused `TYPE_CHECKING` import of `TokenBrokerService`.

- nextcloud_mcp_server/app.py: dropped the `use_basic_auth = True` block
  and the now-stale `token_broker if not use_basic_auth else None` /
  `use_basic_auth` positional args from the two `tg.start(...)` calls in
  the multi-user vector-sync lifespan. Token broker construction stays —
  still consumed by the management API revoke endpoint via
  `app.state.oauth_context["token_broker"]`.

- tests/integration/test_app_password_provisioning.py: deleted four tests
  that exercised the now-removed OAuth-refresh path
  (`test_oauth_mode_uses_refresh_token_only`,
  `test_oauth_mode_raises_error_without_token`,
  `test_get_user_client_oauth_function`,
  `test_oauth_mode_requires_token_broker`) plus the
  `test_get_user_client_dispatches_to_basic_auth` test for the deleted
  dispatcher. Updated the module docstring + imports accordingly. The
  BasicAuth-mode tests (`test_basic_auth_mode_uses_local_storage`,
  `test_multiple_users_basic_auth_mode`, etc.) all remain.

No runtime-behaviour change in any supported deployment mode — the deleted
branches were already unreachable post-PR #787. 3 files changed,
+59 / -301; 1010 unit tests pass; integration jobs for
`mcp-login-flow` and `mcp-multi-user-basic` are the critical regression
gates before merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:13:53 +02:00

150 lines
5.0 KiB
Python

"""Integration tests for app password provisioning via management API.
Tests the complete flow for multi-user BasicAuth and Login Flow v2 modes:
1. User stores app password via management API endpoint (or Login Flow v2 browser flow)
2. MCP server stores it locally (encrypted)
3. Background sync uses locally stored password to access Nextcloud
The earlier OAuth refresh-token background-sync path was removed in the
ADR-022 cleanup — these tests now cover the only supported path.
"""
import tempfile
from pathlib import Path
import pytest
from cryptography.fernet import Fernet
from nextcloud_mcp_server.auth.storage import RefreshTokenStorage
from nextcloud_mcp_server.vector.oauth_sync import (
NotProvisionedError,
get_user_client_basic_auth,
)
@pytest.fixture
def encryption_key():
"""Generate a test encryption key."""
return Fernet.generate_key().decode()
@pytest.fixture
async def temp_storage(encryption_key):
"""Create temporary storage instance with encryption for testing."""
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "test_provisioning.db"
storage = RefreshTokenStorage(
db_path=str(db_path), encryption_key=encryption_key
)
await storage.initialize()
yield storage
@pytest.mark.integration
async def test_basic_auth_mode_uses_local_storage(temp_storage, mocker):
"""Test that BasicAuth mode uses locally stored app passwords.
In multi-user BasicAuth mode, app passwords are stored locally
in the MCP server's database after being provisioned via the API.
"""
# Store an app password in local storage
await temp_storage.store_app_password("test_user", "JHWzB-ZYgLZ-3qBDj-ZQe5o-LdKpB")
# Call get_user_client_basic_auth with local storage
client = await get_user_client_basic_auth(
user_id="test_user",
nextcloud_host="http://localhost:8080",
storage=temp_storage,
)
# Verify client was created with correct credentials
assert client is not None
assert client.username == "test_user"
@pytest.mark.integration
async def test_basic_auth_mode_raises_error_without_app_password(temp_storage):
"""Test that BasicAuth mode raises NotProvisionedError if no app password.
There is NO fallback to OAuth - if no app password, user must provision one.
"""
# Don't store any app password
# Call get_user_client_basic_auth - should raise NotProvisionedError
with pytest.raises(NotProvisionedError) as exc_info:
await get_user_client_basic_auth(
user_id="test_user",
nextcloud_host="http://localhost:8080",
storage=temp_storage,
)
# Verify error message mentions app password provisioning
assert "app password" in str(exc_info.value).lower()
assert "test_user" in str(exc_info.value)
@pytest.mark.integration
async def test_multiple_users_basic_auth_mode(temp_storage, mocker):
"""Test that multiple users can be provisioned independently."""
# Store app passwords for multiple users
users = {
"alice": "aaaaa-aaaaa-aaaaa-aaaaa-aaaaa",
"bob": "bbbbb-bbbbb-bbbbb-bbbbb-bbbbb",
"charlie": "ccccc-ccccc-ccccc-ccccc-ccccc",
}
for user_id, password in users.items():
await temp_storage.store_app_password(user_id, password)
# Verify each user can get a client
for user_id in users.keys():
client = await get_user_client_basic_auth(
user_id=user_id,
nextcloud_host="http://localhost:8080",
storage=temp_storage,
)
assert client is not None
assert client.username == user_id
@pytest.mark.integration
async def test_get_all_provisioned_users(temp_storage):
"""Test that we can list all provisioned users for BasicAuth mode."""
# Store app passwords for multiple users
await temp_storage.store_app_password("alice", "aaaaa-aaaaa-aaaaa-aaaaa-aaaaa")
await temp_storage.store_app_password("bob", "bbbbb-bbbbb-bbbbb-bbbbb-bbbbb")
# Get all provisioned users
user_ids = await temp_storage.get_all_app_password_user_ids()
assert len(user_ids) == 2
assert "alice" in user_ids
assert "bob" in user_ids
@pytest.mark.integration
async def test_revoke_app_password(temp_storage):
"""Test that deleting app password revokes background access."""
# Provision user
await temp_storage.store_app_password("alice", "aaaaa-aaaaa-aaaaa-aaaaa-aaaaa")
# Verify user is provisioned
user_ids = await temp_storage.get_all_app_password_user_ids()
assert "alice" in user_ids
# Revoke access
deleted = await temp_storage.delete_app_password("alice")
assert deleted is True
# Verify user is no longer provisioned
user_ids = await temp_storage.get_all_app_password_user_ids()
assert "alice" not in user_ids
# Verify get_user_client now raises NotProvisionedError
with pytest.raises(NotProvisionedError):
await get_user_client_basic_auth(
user_id="alice",
nextcloud_host="http://localhost:8080",
storage=temp_storage,
)