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>
This commit is contained in:
Chris Coutinho
2026-02-28 10:08:55 +01:00
parent 87ec3c4f5b
commit e28af5453b
6 changed files with 67 additions and 72 deletions
+5 -32
View File
@@ -4,41 +4,17 @@ Tests the third enforcement mode in scope_authorization.py that checks
application-level scopes stored alongside app passwords.
"""
import os
from unittest.mock import AsyncMock, patch
import pytest
from nextcloud_mcp_server.auth.scope_authorization import (
_get_stored_scopes,
_is_login_flow_mode,
)
pytestmark = pytest.mark.unit
def test_is_login_flow_mode_disabled():
"""Test that login flow mode is off by default."""
with patch.dict(os.environ, {}, clear=True):
assert _is_login_flow_mode() is False
def test_is_login_flow_mode_enabled():
"""Test that login flow mode is enabled when env var is set."""
with patch.dict(os.environ, {"ENABLE_LOGIN_FLOW": "true"}):
assert _is_login_flow_mode() is True
def test_is_login_flow_mode_case_insensitive():
"""Test case insensitivity of the env var."""
with patch.dict(os.environ, {"ENABLE_LOGIN_FLOW": "True"}):
assert _is_login_flow_mode() is True
with patch.dict(os.environ, {"ENABLE_LOGIN_FLOW": "TRUE"}):
assert _is_login_flow_mode() is True
with patch.dict(os.environ, {"ENABLE_LOGIN_FLOW": "false"}):
assert _is_login_flow_mode() is False
async def test_get_stored_scopes_with_scopes():
"""Test getting specific scopes from storage."""
mock_storage = AsyncMock()
@@ -49,10 +25,9 @@ async def test_get_stored_scopes_with_scopes():
"created_at": 1000,
"updated_at": 1000,
}
mock_storage.initialize = AsyncMock()
with patch(
"nextcloud_mcp_server.auth.storage.RefreshTokenStorage.from_env",
"nextcloud_mcp_server.auth.scope_authorization._get_scope_storage",
return_value=mock_storage,
):
result = await _get_stored_scopes("alice")
@@ -70,10 +45,9 @@ async def test_get_stored_scopes_null_scopes():
"created_at": 1000,
"updated_at": 1000,
}
mock_storage.initialize = AsyncMock()
with patch(
"nextcloud_mcp_server.auth.storage.RefreshTokenStorage.from_env",
"nextcloud_mcp_server.auth.scope_authorization._get_scope_storage",
return_value=mock_storage,
):
result = await _get_stored_scopes("bob")
@@ -85,10 +59,9 @@ 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
mock_storage.initialize = AsyncMock()
with patch(
"nextcloud_mcp_server.auth.storage.RefreshTokenStorage.from_env",
"nextcloud_mcp_server.auth.scope_authorization._get_scope_storage",
return_value=mock_storage,
):
result = await _get_stored_scopes("nobody")
@@ -99,10 +72,10 @@ async def test_get_stored_scopes_no_password():
async def test_get_stored_scopes_storage_error():
"""Test that storage errors return None (fail-closed)."""
mock_storage = AsyncMock()
mock_storage.initialize.side_effect = RuntimeError("DB error")
mock_storage.get_app_password_with_scopes.side_effect = RuntimeError("DB error")
with patch(
"nextcloud_mcp_server.auth.storage.RefreshTokenStorage.from_env",
"nextcloud_mcp_server.auth.scope_authorization._get_scope_storage",
return_value=mock_storage,
):
result = await _get_stored_scopes("alice")