Initial implementation
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
"""Tests for auth.py."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from mcp_synology_container.auth import AuthManager, AuthenticationError
|
||||
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()
|
||||
Reference in New Issue
Block a user