Files
mcp-nextcloud/tests/unit/test_scope_authorization_stored.py
T
Chris CoutinhoandClaude Opus 4.6 e28af5453b fix: address PR #589 review feedback for Login Flow v2
- Fix data loss in nc_auth_update_scopes: remove premature
  delete_app_password call; old password stays valid until upsert
  replaces it on successful re-provisioning
- Replace assert with proper error return in nc_auth_check_status
- Add lazy singleton for RefreshTokenStorage in auth_tools,
  scope_authorization, and context to avoid per-call re-initialization
- Centralize _is_login_flow_mode() to get_settings().enable_login_flow
  and remove duplicate definitions and per-call os.getenv reads
- Add dev-only comment to TOKEN_ENCRYPTION_KEY in docker-compose.yml
- Gate OIDC build steps in CI behind matrix.needs-playwright
- Add diagnostic step reporting Playwright skip count in CI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:08:55 +01:00

84 lines
2.4 KiB
Python

"""Unit tests for @require_scopes with stored app passwords (Login Flow v2).
Tests the third enforcement mode in scope_authorization.py that checks
application-level scopes stored alongside app passwords.
"""
from unittest.mock import AsyncMock, patch
import pytest
from nextcloud_mcp_server.auth.scope_authorization import (
_get_stored_scopes,
)
pytestmark = pytest.mark.unit
async def test_get_stored_scopes_with_scopes():
"""Test getting specific scopes from storage."""
mock_storage = AsyncMock()
mock_storage.get_app_password_with_scopes.return_value = {
"app_password": "xxxxx",
"scopes": ["notes:read", "calendar:read"],
"username": "alice",
"created_at": 1000,
"updated_at": 1000,
}
with patch(
"nextcloud_mcp_server.auth.scope_authorization._get_scope_storage",
return_value=mock_storage,
):
result = await _get_stored_scopes("alice")
assert result == ["notes:read", "calendar:read"]
async def test_get_stored_scopes_null_scopes():
"""Test that NULL scopes returns 'all'."""
mock_storage = AsyncMock()
mock_storage.get_app_password_with_scopes.return_value = {
"app_password": "xxxxx",
"scopes": None,
"username": "bob",
"created_at": 1000,
"updated_at": 1000,
}
with patch(
"nextcloud_mcp_server.auth.scope_authorization._get_scope_storage",
return_value=mock_storage,
):
result = await _get_stored_scopes("bob")
assert result == "all"
async def test_get_stored_scopes_no_password():
"""Test that missing app password returns None."""
mock_storage = AsyncMock()
mock_storage.get_app_password_with_scopes.return_value = None
with patch(
"nextcloud_mcp_server.auth.scope_authorization._get_scope_storage",
return_value=mock_storage,
):
result = await _get_stored_scopes("nobody")
assert result is None
async def test_get_stored_scopes_storage_error():
"""Test that storage errors return None (fail-closed)."""
mock_storage = AsyncMock()
mock_storage.get_app_password_with_scopes.side_effect = RuntimeError("DB error")
with patch(
"nextcloud_mcp_server.auth.scope_authorization._get_scope_storage",
return_value=mock_storage,
):
result = await _get_stored_scopes("alice")
assert result is None