- _resolve_settings_files() now raises FileNotFoundError when NEXTCLOUD_MCP_SETTINGS_FILE points to a missing file, instead of silently falling back to defaults (footgun on typos). - .secrets.toml is now looked for alongside the explicit settings file when NEXTCLOUD_MCP_SETTINGS_FILE is set, matching user expectation for /etc-style deployments. Unset behaviour (cwd lookup) is unchanged. - get_token_db_path() drops the redundant os.environ.get() short-circuit; TOKEN_STORAGE_DB is already bound through dynaconf because the key is declared in _DEFAULTS. - is_ephemeral_token_db() docstring documents the "must call get_token_db_path() first" precondition. - alembic.ini comment clarifies the ./tokens.db placeholder is cwd-relative by design and points readers at the -x database_url escape hatch. - New tests/unit/test_config_paths.py (12 tests) covering the ephemeral tempfile lifecycle, the TOKEN_STORAGE_DB override path, and all six _resolve_settings_files() cases including the two new behaviours. Full unit suite now at 476 passed (464 + 12 new). Ruff + ty clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
159 lines
5.9 KiB
Python
159 lines
5.9 KiB
Python
"""Tests for config.py path resolution helpers added in PR #707.
|
|
|
|
Covers:
|
|
- get_token_db_path() / is_ephemeral_token_db() — ephemeral tempfile default
|
|
with TOKEN_STORAGE_DB override.
|
|
- _resolve_settings_files() — optional external settings file discovery,
|
|
including the NEXTCLOUD_MCP_SETTINGS_FILE env var and its colocation
|
|
semantics for .secrets.toml.
|
|
"""
|
|
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import nextcloud_mcp_server.config as cfg
|
|
from nextcloud_mcp_server.config import (
|
|
_reload_config,
|
|
_resolve_settings_files,
|
|
get_token_db_path,
|
|
is_ephemeral_token_db,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_ephemeral_state(monkeypatch):
|
|
"""Reset module-global ephemeral tempfile state between tests.
|
|
|
|
get_token_db_path() memoizes its result in a module-level global and
|
|
registers an atexit hook for cleanup. Tests need an isolated slate so
|
|
assertions about "already allocated" vs "not yet" are meaningful.
|
|
"""
|
|
old = cfg._ephemeral_db_path
|
|
cfg._ephemeral_db_path = None
|
|
monkeypatch.delenv("TOKEN_STORAGE_DB", raising=False)
|
|
monkeypatch.delenv("NEXTCLOUD_MCP_SETTINGS_FILE", raising=False)
|
|
_reload_config()
|
|
yield
|
|
if cfg._ephemeral_db_path and os.path.exists(cfg._ephemeral_db_path):
|
|
os.unlink(cfg._ephemeral_db_path)
|
|
cfg._ephemeral_db_path = old
|
|
_reload_config()
|
|
|
|
|
|
class TestGetTokenDbPath:
|
|
def test_explicit_env_var_returned(self, monkeypatch, tmp_path):
|
|
target = tmp_path / "explicit.db"
|
|
monkeypatch.setenv("TOKEN_STORAGE_DB", str(target))
|
|
_reload_config()
|
|
|
|
assert get_token_db_path() == str(target)
|
|
# No tempfile should have been allocated since we took the explicit
|
|
# branch.
|
|
assert cfg._ephemeral_db_path is None
|
|
|
|
def test_ephemeral_tempfile_when_unset(self):
|
|
path = get_token_db_path()
|
|
|
|
assert path.startswith(tempfile.gettempdir())
|
|
assert f"nextcloud-mcp-tokens-{os.getpid()}-" in os.path.basename(path)
|
|
assert path.endswith(".db")
|
|
assert os.path.exists(path)
|
|
|
|
def test_ephemeral_tempfile_is_memoized(self):
|
|
first = get_token_db_path()
|
|
second = get_token_db_path()
|
|
|
|
assert first == second
|
|
# Only the memoized file should exist — no stray siblings.
|
|
parent = Path(first).parent
|
|
matches = list(parent.glob(f"nextcloud-mcp-tokens-{os.getpid()}-*.db"))
|
|
assert matches == [Path(first)]
|
|
|
|
def test_is_ephemeral_token_db_detects_allocated_path(self):
|
|
path = get_token_db_path()
|
|
|
|
assert is_ephemeral_token_db(path) is True
|
|
assert is_ephemeral_token_db("/some/other/path") is False
|
|
|
|
def test_is_ephemeral_token_db_before_allocation(self):
|
|
# The autouse fixture reset _ephemeral_db_path to None; do not call
|
|
# get_token_db_path() first. Nothing is allocated yet.
|
|
assert cfg._ephemeral_db_path is None
|
|
assert is_ephemeral_token_db("/any/path") is False
|
|
assert is_ephemeral_token_db("") is False
|
|
|
|
def test_explicit_path_does_not_trigger_tempfile(self, monkeypatch, tmp_path):
|
|
"""Regression guard: the explicit branch must short-circuit cleanly."""
|
|
monkeypatch.setenv("TOKEN_STORAGE_DB", str(tmp_path / "pinned.db"))
|
|
_reload_config()
|
|
|
|
get_token_db_path()
|
|
# Nothing under the tempfile prefix should have been created.
|
|
matches = list(
|
|
Path(tempfile.gettempdir()).glob(f"nextcloud-mcp-tokens-{os.getpid()}-*.db")
|
|
)
|
|
assert matches == []
|
|
|
|
|
|
class TestResolveSettingsFiles:
|
|
def test_empty_list_when_nothing_present(self, monkeypatch, tmp_path):
|
|
monkeypatch.chdir(tmp_path)
|
|
assert _resolve_settings_files() == []
|
|
|
|
def test_picks_up_cwd_settings(self, monkeypatch, tmp_path):
|
|
(tmp_path / "settings.toml").write_text("[default]\n")
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
result = _resolve_settings_files()
|
|
assert str(tmp_path / "settings.toml") in result
|
|
|
|
def test_picks_up_cwd_secrets(self, monkeypatch, tmp_path):
|
|
(tmp_path / ".secrets.toml").write_text("[default]\n")
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
result = _resolve_settings_files()
|
|
assert str(tmp_path / ".secrets.toml") in result
|
|
|
|
def test_explicit_settings_file_included(self, monkeypatch, tmp_path):
|
|
explicit = tmp_path / "nested" / "my-settings.toml"
|
|
explicit.parent.mkdir()
|
|
explicit.write_text("[default]\n")
|
|
monkeypatch.setenv("NEXTCLOUD_MCP_SETTINGS_FILE", str(explicit))
|
|
# cwd has no settings.toml / .secrets.toml
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
result = _resolve_settings_files()
|
|
assert result == [str(explicit)]
|
|
|
|
def test_explicit_settings_file_secrets_colocated(self, monkeypatch, tmp_path):
|
|
"""PR #707 reviewer feedback: .secrets.toml should live beside
|
|
the explicit settings file, not always in cwd."""
|
|
config_dir = tmp_path / "etc"
|
|
config_dir.mkdir()
|
|
explicit = config_dir / "settings.toml"
|
|
explicit.write_text("[default]\n")
|
|
secrets = config_dir / ".secrets.toml"
|
|
secrets.write_text("[default]\n")
|
|
|
|
monkeypatch.setenv("NEXTCLOUD_MCP_SETTINGS_FILE", str(explicit))
|
|
# cwd is elsewhere and deliberately contains *no* secrets file
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
result = _resolve_settings_files()
|
|
assert str(explicit) in result
|
|
assert str(secrets) in result
|
|
|
|
def test_explicit_missing_raises(self, monkeypatch, tmp_path):
|
|
"""PR #707 reviewer feedback: missing explicit path must not be
|
|
silently ignored — users will think their config is applied when
|
|
it isn't."""
|
|
missing = tmp_path / "does-not-exist.toml"
|
|
monkeypatch.setenv("NEXTCLOUD_MCP_SETTINGS_FILE", str(missing))
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
with pytest.raises(FileNotFoundError, match="does-not-exist.toml"):
|
|
_resolve_settings_files()
|