"""Tests for SSL/TLS configuration (NEXTCLOUD_VERIFY_SSL, NEXTCLOUD_CA_BUNDLE)."""
import logging
import os
import ssl
from unittest.mock import patch
import certifi
import httpx
import pytest
from nextcloud_mcp_server.config import Settings, get_nextcloud_ssl_verify, get_settings
from nextcloud_mcp_server.http import nextcloud_httpx_client, nextcloud_httpx_transport
class TestSSLSettings:
"""Test SSL/TLS fields on Settings dataclass."""
def test_defaults(self):
"""verify_ssl defaults to True, ca_bundle defaults to None."""
settings = Settings()
assert settings.nextcloud_verify_ssl is True
assert settings.nextcloud_ca_bundle is None
def test_verify_ssl_false_logs_warning(self, caplog):
caplog.set_level(logging.WARNING, logger="nextcloud_mcp_server.config")
Settings(nextcloud_verify_ssl=False)
assert "NEXTCLOUD_VERIFY_SSL is disabled" in caplog.text
def test_ca_bundle_nonexistent_path_raises(self):
with pytest.raises(ValueError, match="does not exist"):
Settings(nextcloud_ca_bundle="/nonexistent/path/ca.pem")
def test_ca_bundle_existing_path_logs_info(self, caplog, tmp_path):
ca_file = tmp_path / "ca.pem"
ca_file.write_text(
"-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----\n"
)
caplog.set_level(logging.INFO, logger="nextcloud_mcp_server.config")
settings = Settings(nextcloud_ca_bundle=str(ca_file))
assert settings.nextcloud_ca_bundle == str(ca_file)
assert "Using custom CA bundle" in caplog.text
class TestGetNextcloudSSLVerify:
"""Test the get_nextcloud_ssl_verify() helper function."""
def test_default_returns_true(self):
env = {
"NEXTCLOUD_VERIFY_SSL": "true",
}
with patch.dict(os.environ, env, clear=False):
# Clear any cached settings
result = get_nextcloud_ssl_verify()
assert result is True
def test_verify_false_returns_false(self):
env = {
"NEXTCLOUD_VERIFY_SSL": "false",
}
with patch.dict(os.environ, env, clear=False):
with patch(
"nextcloud_mcp_server.config.get_settings",
return_value=Settings(nextcloud_verify_ssl=False),
):
result = get_nextcloud_ssl_verify()
assert result is False
def test_ca_bundle_returns_ssl_context(self):
ca_bundle = certifi.where()
with patch(
"nextcloud_mcp_server.config.get_settings",
return_value=Settings(nextcloud_ca_bundle=ca_bundle),
):
result = get_nextcloud_ssl_verify()
assert isinstance(result, ssl.SSLContext)
def test_ca_bundle_ssl_context_has_loaded_certs(self):
"""SSLContext created from CA bundle should have loaded certificates."""
ca_bundle = certifi.where()
with patch(
"nextcloud_mcp_server.config.get_settings",
return_value=Settings(nextcloud_ca_bundle=ca_bundle),
):
result = get_nextcloud_ssl_verify()
assert isinstance(result, ssl.SSLContext)
stats = result.cert_store_stats()
assert stats["x509_ca"] > 0
def test_verify_false_takes_precedence_over_ca_bundle(self, tmp_path):
"""When verify_ssl=False, ca_bundle is ignored (False wins)."""
ca_file = tmp_path / "ca.pem"
ca_file.write_text(
"-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----\n"
)
with patch(
"nextcloud_mcp_server.config.get_settings",
return_value=Settings(
nextcloud_verify_ssl=False,
nextcloud_ca_bundle=str(ca_file),
),
):
result = get_nextcloud_ssl_verify()
assert result is False
class TestGetSettingsSSLEnvVars:
"""Test that get_settings() reads SSL env vars correctly."""
def test_verify_ssl_env_true(self):
env = {"NEXTCLOUD_VERIFY_SSL": "true"}
with patch.dict(os.environ, env, clear=False):
settings = get_settings()
assert settings.nextcloud_verify_ssl is True
def test_verify_ssl_env_false(self):
env = {"NEXTCLOUD_VERIFY_SSL": "false"}
with patch.dict(os.environ, env, clear=False):
settings = get_settings()
assert settings.nextcloud_verify_ssl is False
def test_verify_ssl_env_missing_defaults_true(self):
with patch.dict(os.environ, {}, clear=False):
# Remove NEXTCLOUD_VERIFY_SSL if it exists
os.environ.pop("NEXTCLOUD_VERIFY_SSL", None)
settings = get_settings()
assert settings.nextcloud_verify_ssl is True
def test_ca_bundle_env(self, tmp_path):
ca_file = tmp_path / "ca.pem"
ca_file.write_text(
"-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----\n"
)
env = {"NEXTCLOUD_CA_BUNDLE": str(ca_file)}
with patch.dict(os.environ, env, clear=False):
settings = get_settings()
assert settings.nextcloud_ca_bundle == str(ca_file)
class TestHTTPClientFactory:
"""Test that factory functions apply verify correctly."""
def test_client_applies_verify_true(self):
with patch(
"nextcloud_mcp_server.http.get_nextcloud_ssl_verify", return_value=True
):
client = nextcloud_httpx_client()
# httpx stores verify as an SSLConfig; check the _transport
assert isinstance(client, httpx.AsyncClient)
def test_client_applies_verify_false(self):
with patch(
"nextcloud_mcp_server.http.get_nextcloud_ssl_verify", return_value=False
):
client = nextcloud_httpx_client()
assert isinstance(client, httpx.AsyncClient)
def test_client_caller_override_takes_precedence(self):
"""Caller-supplied verify kwarg should not be overridden."""
with patch(
"nextcloud_mcp_server.http.get_nextcloud_ssl_verify", return_value=True
):
client = nextcloud_httpx_client(verify=False)
assert isinstance(client, httpx.AsyncClient)
def test_transport_applies_verify(self):
with patch(
"nextcloud_mcp_server.http.get_nextcloud_ssl_verify", return_value=False
):
transport = nextcloud_httpx_transport()
assert isinstance(transport, httpx.AsyncHTTPTransport)
def test_client_passes_extra_kwargs(self):
with patch(
"nextcloud_mcp_server.http.get_nextcloud_ssl_verify", return_value=True
):
client = nextcloud_httpx_client(timeout=5.0, follow_redirects=True)
assert isinstance(client, httpx.AsyncClient)