"""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()