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>
150 lines
5.0 KiB
Python
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,
|
|
)
|