- 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>
84 lines
2.4 KiB
Python
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
|