feat: implement auth infrastructure and first two FileStation tools
- auth.py: AuthManager with OS keyring, env var fallback, 2FA device token flow - config.py: AppConfig/ConnectionConfig dataclasses, YAML load/save, env overrides - client.py: FileStationClient with lazy init, session re-auth, upload/download - cli.py: setup / check / serve subcommands (anyio.run throughout) - server.py: create_server factory wiring FastMCP to FileStation tools - tools/filestation.py: list_shares and list_dir with ASCII table output, pagination hints, input validation, DSM error mapping - tests: 30 unit tests, all passing (auth, config, tools) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
"""Tests for config.py: loading, validation, env overrides, save."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import textwrap
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
from mcp_synology_filestation.config import (
|
||||
AppConfig,
|
||||
ConnectionConfig,
|
||||
load_config,
|
||||
save_config,
|
||||
)
|
||||
|
||||
|
||||
def _write_config(path: Path, content: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(textwrap.dedent(content), encoding="utf-8")
|
||||
|
||||
|
||||
def test_load_config_minimal(tmp_path: Path) -> None:
|
||||
"""Minimal valid config loads successfully."""
|
||||
cfg_file = tmp_path / "config.yaml"
|
||||
_write_config(
|
||||
cfg_file,
|
||||
"""\
|
||||
schema_version: 1
|
||||
connection:
|
||||
host: nas.example.com
|
||||
""",
|
||||
)
|
||||
config = load_config(cfg_file)
|
||||
assert config.connection.host == "nas.example.com"
|
||||
assert config.connection.port == 443
|
||||
assert config.connection.https is True
|
||||
assert config.connection.verify_ssl is True
|
||||
assert config.connection.timeout == 30
|
||||
assert config.alias is None
|
||||
|
||||
|
||||
def test_load_config_full(tmp_path: Path) -> None:
|
||||
"""All fields are parsed correctly."""
|
||||
cfg_file = tmp_path / "config.yaml"
|
||||
_write_config(
|
||||
cfg_file,
|
||||
"""\
|
||||
schema_version: 1
|
||||
alias: home-nas
|
||||
connection:
|
||||
host: 192.168.1.2
|
||||
port: 5001
|
||||
https: true
|
||||
verify_ssl: false
|
||||
timeout: 60
|
||||
""",
|
||||
)
|
||||
config = load_config(cfg_file)
|
||||
assert config.connection.host == "192.168.1.2"
|
||||
assert config.connection.port == 5001
|
||||
assert config.connection.verify_ssl is False
|
||||
assert config.connection.timeout == 60
|
||||
assert config.alias == "home-nas"
|
||||
|
||||
|
||||
def test_load_config_missing_file(tmp_path: Path) -> None:
|
||||
"""Missing config file raises FileNotFoundError."""
|
||||
with pytest.raises(FileNotFoundError):
|
||||
load_config(tmp_path / "nonexistent.yaml")
|
||||
|
||||
|
||||
def test_load_config_wrong_schema_version(tmp_path: Path) -> None:
|
||||
"""Wrong schema_version raises ValueError."""
|
||||
cfg_file = tmp_path / "config.yaml"
|
||||
_write_config(
|
||||
cfg_file,
|
||||
"""\
|
||||
schema_version: 99
|
||||
connection:
|
||||
host: nas.example.com
|
||||
""",
|
||||
)
|
||||
with pytest.raises(ValueError, match="schema_version"):
|
||||
load_config(cfg_file)
|
||||
|
||||
|
||||
def test_load_config_missing_host(tmp_path: Path) -> None:
|
||||
"""Missing host raises ValueError."""
|
||||
cfg_file = tmp_path / "config.yaml"
|
||||
_write_config(
|
||||
cfg_file,
|
||||
"""\
|
||||
schema_version: 1
|
||||
connection: {}
|
||||
""",
|
||||
)
|
||||
with pytest.raises(ValueError, match="connection.host"):
|
||||
load_config(cfg_file)
|
||||
|
||||
|
||||
def test_load_config_env_override(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Environment variables override YAML values."""
|
||||
cfg_file = tmp_path / "config.yaml"
|
||||
_write_config(
|
||||
cfg_file,
|
||||
"""\
|
||||
schema_version: 1
|
||||
connection:
|
||||
host: original-host.example.com
|
||||
""",
|
||||
)
|
||||
monkeypatch.setenv("SYNOLOGY_HOST", "env-host.example.com")
|
||||
monkeypatch.setenv("SYNOLOGY_PORT", "9999")
|
||||
config = load_config(cfg_file)
|
||||
assert config.connection.host == "env-host.example.com"
|
||||
assert config.connection.port == 9999
|
||||
|
||||
|
||||
def test_base_url_https(tmp_path: Path) -> None:
|
||||
"""base_url uses https scheme when https=True."""
|
||||
cfg_file = tmp_path / "config.yaml"
|
||||
_write_config(
|
||||
cfg_file,
|
||||
"""\
|
||||
schema_version: 1
|
||||
connection:
|
||||
host: nas.example.com
|
||||
port: 443
|
||||
https: true
|
||||
""",
|
||||
)
|
||||
config = load_config(cfg_file)
|
||||
assert config.base_url == "https://nas.example.com:443"
|
||||
|
||||
|
||||
def test_base_url_http(tmp_path: Path) -> None:
|
||||
"""base_url uses http scheme when https=False."""
|
||||
cfg_file = tmp_path / "config.yaml"
|
||||
_write_config(
|
||||
cfg_file,
|
||||
"""\
|
||||
schema_version: 1
|
||||
connection:
|
||||
host: nas.example.com
|
||||
port: 5000
|
||||
https: false
|
||||
""",
|
||||
)
|
||||
config = load_config(cfg_file)
|
||||
assert config.base_url == "http://nas.example.com:5000"
|
||||
|
||||
|
||||
def test_keyring_service() -> None:
|
||||
"""keyring_service is always the fixed service name."""
|
||||
config = AppConfig(
|
||||
schema_version=1,
|
||||
connection=ConnectionConfig(host="anything"),
|
||||
)
|
||||
assert config.keyring_service == "mcp-synology-filestation"
|
||||
|
||||
|
||||
def test_save_and_reload(tmp_path: Path) -> None:
|
||||
"""save_config + load_config round-trips successfully."""
|
||||
config = AppConfig(
|
||||
schema_version=1,
|
||||
connection=ConnectionConfig(
|
||||
host="nas.example.com",
|
||||
port=443,
|
||||
https=True,
|
||||
verify_ssl=False,
|
||||
timeout=45,
|
||||
),
|
||||
alias="test",
|
||||
)
|
||||
cfg_file = tmp_path / "config.yaml"
|
||||
save_config(config, cfg_file)
|
||||
|
||||
loaded = load_config(cfg_file)
|
||||
assert loaded.connection.host == "nas.example.com"
|
||||
assert loaded.connection.verify_ssl is False
|
||||
assert loaded.connection.timeout == 45
|
||||
assert loaded.alias == "test"
|
||||
Reference in New Issue
Block a user