5b14af8ea1
Mechanical cleanup via `ruff check --fix` + `ruff format`: - cli.py, test_auth.py: import sorting (isort convention) - cli.py: remove unused AuthenticationError import in _run_setup - config.py: remove unused `field` import - test_auth.py: remove unused MagicMock import - test_config.py: remove unused Path import No functional change. All 131 tests remain green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
169 lines
4.8 KiB
Python
169 lines
4.8 KiB
Python
"""Tests for auth.py."""
|
|
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from mcp_synology_container.auth import AuthenticationError, AuthManager
|
|
from mcp_synology_container.config import AppConfig, ConnectionConfig
|
|
|
|
|
|
def make_config(host: str = "nas.local") -> AppConfig:
|
|
return AppConfig(
|
|
schema_version=1,
|
|
connection=ConnectionConfig(host=host, port=443, https=True, verify_ssl=True),
|
|
)
|
|
|
|
|
|
def test_resolve_credentials_from_env(monkeypatch):
|
|
monkeypatch.setenv("SYNOLOGY_USERNAME", "admin")
|
|
monkeypatch.setenv("SYNOLOGY_PASSWORD", "secret")
|
|
|
|
config = make_config()
|
|
auth = AuthManager(config)
|
|
username, password, device_token = auth.resolve_credentials()
|
|
|
|
assert username == "admin"
|
|
assert password == "secret"
|
|
assert device_token is None
|
|
|
|
|
|
def test_resolve_credentials_no_credentials(monkeypatch):
|
|
monkeypatch.delenv("SYNOLOGY_USERNAME", raising=False)
|
|
monkeypatch.delenv("SYNOLOGY_PASSWORD", raising=False)
|
|
|
|
config = make_config()
|
|
auth = AuthManager(config)
|
|
|
|
with patch("keyring.get_password", return_value=None):
|
|
with pytest.raises(AuthenticationError, match="No credentials found"):
|
|
auth.resolve_credentials()
|
|
|
|
|
|
def test_resolve_credentials_from_keyring(monkeypatch):
|
|
monkeypatch.delenv("SYNOLOGY_USERNAME", raising=False)
|
|
monkeypatch.delenv("SYNOLOGY_PASSWORD", raising=False)
|
|
|
|
def mock_get_password(service, key):
|
|
data = {"username": "keyring_user", "password": "keyring_pass", "device_token": "tok123"}
|
|
return data.get(key)
|
|
|
|
config = make_config()
|
|
auth = AuthManager(config)
|
|
|
|
with patch("keyring.get_password", side_effect=mock_get_password):
|
|
username, password, device_token = auth.resolve_credentials()
|
|
|
|
assert username == "keyring_user"
|
|
assert password == "keyring_pass"
|
|
assert device_token == "tok123"
|
|
|
|
|
|
def test_store_credentials_success():
|
|
config = make_config()
|
|
auth = AuthManager(config)
|
|
|
|
with patch("keyring.set_password") as mock_set:
|
|
result = auth.store_credentials("user", "pass")
|
|
|
|
assert result is True
|
|
assert mock_set.call_count == 2
|
|
|
|
|
|
def test_store_credentials_keyring_unavailable():
|
|
config = make_config()
|
|
auth = AuthManager(config)
|
|
|
|
with patch("keyring.set_password", side_effect=Exception("no keyring")):
|
|
result = auth.store_credentials("user", "pass")
|
|
|
|
assert result is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_success():
|
|
config = make_config()
|
|
auth = AuthManager(config)
|
|
|
|
mock_client = AsyncMock()
|
|
mock_client.request.return_value = {"sid": "test_session_id"}
|
|
|
|
with patch.object(auth, "resolve_credentials", return_value=("user", "pass", None)):
|
|
sid = await auth.login(mock_client)
|
|
|
|
assert sid == "test_session_id"
|
|
mock_client.request.assert_called_once()
|
|
call_kwargs = mock_client.request.call_args
|
|
assert call_kwargs[0][0] == "SYNO.API.Auth"
|
|
assert call_kwargs[0][1] == "login"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_with_device_token():
|
|
config = make_config()
|
|
auth = AuthManager(config)
|
|
|
|
mock_client = AsyncMock()
|
|
mock_client.request.return_value = {"sid": "test_sid"}
|
|
|
|
with patch.object(auth, "resolve_credentials", return_value=("user", "pass", "dev_token")):
|
|
sid = await auth.login(mock_client)
|
|
|
|
assert sid == "test_sid"
|
|
params = mock_client.request.call_args[1]["params"]
|
|
assert params["device_id"] == "dev_token"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_2fa_required():
|
|
from mcp_synology_container.dsm_client import SynologyError
|
|
|
|
config = make_config()
|
|
auth = AuthManager(config)
|
|
|
|
mock_client = AsyncMock()
|
|
mock_client.request.side_effect = SynologyError("2FA required", code=403)
|
|
|
|
with patch.object(auth, "resolve_credentials", return_value=("user", "pass", None)):
|
|
with pytest.raises(AuthenticationError, match="2FA is required"):
|
|
await auth.login(mock_client)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_no_sid_returned():
|
|
config = make_config()
|
|
auth = AuthManager(config)
|
|
|
|
mock_client = AsyncMock()
|
|
mock_client.request.return_value = {} # No 'sid' key
|
|
|
|
with patch.object(auth, "resolve_credentials", return_value=("user", "pass", None)):
|
|
with pytest.raises(AuthenticationError, match="no session ID"):
|
|
await auth.login(mock_client)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_logout():
|
|
config = make_config()
|
|
auth = AuthManager(config)
|
|
|
|
mock_client = AsyncMock()
|
|
mock_client.sid = "active_sid"
|
|
|
|
await auth.logout(mock_client)
|
|
|
|
mock_client.request.assert_called_once()
|
|
assert mock_client.sid is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_logout_no_session():
|
|
config = make_config()
|
|
auth = AuthManager(config)
|
|
|
|
mock_client = AsyncMock()
|
|
mock_client.sid = None
|
|
|
|
await auth.logout(mock_client)
|
|
mock_client.request.assert_not_called()
|