"""Unit tests for the ConfluenceConfig class."""
import os
from unittest.mock import patch
import pytest
from mcp_atlassian.confluence.config import ConfluenceConfig
def test_from_env_success():
"""Test that from_env successfully creates a config from environment variables."""
# Need to clear and reset the environment for this test
with patch.dict(
"os.environ",
{
"CONFLUENCE_URL": "https://test.atlassian.net/wiki",
"CONFLUENCE_USERNAME": "test_username",
"CONFLUENCE_API_TOKEN": "test_token",
},
clear=True, # Clear existing environment variables
):
config = ConfluenceConfig.from_env()
assert config.url == "https://test.atlassian.net/wiki"
assert config.username == "test_username"
assert config.api_token == "test_token"
def test_from_env_missing_url():
"""Test that from_env raises ValueError when URL is missing."""
original_env = os.environ.copy()
try:
os.environ.clear()
with pytest.raises(
ValueError, match="Missing required CONFLUENCE_URL environment variable"
):
ConfluenceConfig.from_env()
finally:
# Restore original environment
os.environ.clear()
os.environ.update(original_env)
def test_from_env_missing_cloud_auth():
"""Test that from_env raises ValueError when cloud auth credentials are missing."""
with patch.dict(
os.environ,
{
"CONFLUENCE_URL": "https://test.atlassian.net", # Cloud URL
},
clear=True,
):
with pytest.raises(
ValueError,
match="Cloud authentication requires CONFLUENCE_USERNAME and CONFLUENCE_API_TOKEN",
):
ConfluenceConfig.from_env()
def test_from_env_missing_server_auth():
"""Test that from_env raises ValueError when server auth credentials are missing."""
with patch.dict(
os.environ,
{
"CONFLUENCE_URL": "https://confluence.example.com", # Server URL
},
clear=True,
):
with pytest.raises(
ValueError,
match="Server/Data Center authentication requires CONFLUENCE_PERSONAL_TOKEN",
):
ConfluenceConfig.from_env()
def test_is_cloud():
"""Test that is_cloud property returns correct value."""
# Arrange & Act - Cloud URL
config = ConfluenceConfig(
url="https://example.atlassian.net/wiki",
auth_type="basic",
username="test",
api_token="test",
)
# Assert
assert config.is_cloud is True
# Arrange & Act - Server URL
config = ConfluenceConfig(
url="https://confluence.example.com",
auth_type="pat",
personal_token="test",
)
# Assert
assert config.is_cloud is False
# Arrange & Act - Localhost URL (Data Center/Server)
config = ConfluenceConfig(
url="http://localhost:8090",
auth_type="pat",
personal_token="test",
)
# Assert
assert config.is_cloud is False
# Arrange & Act - IP localhost URL (Data Center/Server)
config = ConfluenceConfig(
url="http://127.0.0.1:8090",
auth_type="pat",
personal_token="test",
)
# Assert
assert config.is_cloud is False
def test_from_env_proxy_settings():
"""Test that from_env correctly loads proxy environment variables."""
with patch.dict(
os.environ,
{
"CONFLUENCE_URL": "https://test.atlassian.net/wiki",
"CONFLUENCE_USERNAME": "test_username",
"CONFLUENCE_API_TOKEN": "test_token",
"HTTP_PROXY": "http://proxy.example.com:8080",
"HTTPS_PROXY": "https://proxy.example.com:8443",
"SOCKS_PROXY": "socks5://user:pass@proxy.example.com:1080",
"NO_PROXY": "localhost,127.0.0.1",
},
clear=True,
):
config = ConfluenceConfig.from_env()
assert config.http_proxy == "http://proxy.example.com:8080"
assert config.https_proxy == "https://proxy.example.com:8443"
assert config.socks_proxy == "socks5://user:pass@proxy.example.com:1080"
assert config.no_proxy == "localhost,127.0.0.1"
# Service-specific overrides
with patch.dict(
os.environ,
{
"CONFLUENCE_URL": "https://test.atlassian.net/wiki",
"CONFLUENCE_USERNAME": "test_username",
"CONFLUENCE_API_TOKEN": "test_token",
"CONFLUENCE_HTTP_PROXY": "http://confluence-proxy.example.com:8080",
"CONFLUENCE_HTTPS_PROXY": "https://confluence-proxy.example.com:8443",
"CONFLUENCE_SOCKS_PROXY": "socks5://user:pass@confluence-proxy.example.com:1080",
"CONFLUENCE_NO_PROXY": "localhost,127.0.0.1,.internal.example.com",
},
clear=True,
):
config = ConfluenceConfig.from_env()
assert config.http_proxy == "http://confluence-proxy.example.com:8080"
assert config.https_proxy == "https://confluence-proxy.example.com:8443"
assert (
config.socks_proxy == "socks5://user:pass@confluence-proxy.example.com:1080"
)
assert config.no_proxy == "localhost,127.0.0.1,.internal.example.com"
def test_is_cloud_oauth_with_cloud_id():
"""Test that is_cloud returns True for OAuth with cloud_id regardless of URL."""
from mcp_atlassian.utils.oauth import BYOAccessTokenOAuthConfig
# OAuth with cloud_id and no URL - should be Cloud
oauth_config = BYOAccessTokenOAuthConfig(
cloud_id="test-cloud-id", access_token="test-token"
)
config = ConfluenceConfig(
url=None, # URL can be None in Multi-Cloud OAuth mode
auth_type="oauth",
oauth_config=oauth_config,
)
assert config.is_cloud is True
# OAuth with cloud_id and server URL - should still be Cloud
config = ConfluenceConfig(
url="https://confluence.example.com", # Server-like URL
auth_type="oauth",
oauth_config=oauth_config,
)
assert config.is_cloud is True
def test_from_env_pat_priority_over_oauth(caplog):
"""Test that PAT takes priority over OAuth for Server/DC (fixes #824)."""
with patch.dict(
os.environ,
{
"CONFLUENCE_URL": "https://confluence.example.com", # Server/DC URL
"CONFLUENCE_PERSONAL_TOKEN": "test_pat",
"ATLASSIAN_OAUTH_ENABLE": "true", # OAuth also enabled
},
clear=True,
):
config = ConfluenceConfig.from_env()
assert config.auth_type == "pat"
assert config.personal_token == "test_pat"
# Verify warning is logged when both are configured
assert "Both PAT and OAuth configured for Server/DC. Using PAT." in caplog.text
def test_from_env_with_client_cert():
"""Test loading config with client certificate settings from environment."""
with patch.dict(
"os.environ",
{
"CONFLUENCE_URL": "https://confluence.example.com",
"CONFLUENCE_PERSONAL_TOKEN": "test_pat",
"CONFLUENCE_CLIENT_CERT": "/path/to/cert.pem",
"CONFLUENCE_CLIENT_KEY": "/path/to/key.pem",
"CONFLUENCE_CLIENT_KEY_PASSWORD": "secret",
},
clear=True,
):
config = ConfluenceConfig.from_env()
assert config.url == "https://confluence.example.com"
assert config.client_cert == "/path/to/cert.pem"
assert config.client_key == "/path/to/key.pem"
assert config.client_key_password == "secret"
def test_from_env_http_settings():
"""Test that from_env correctly loads HTTP settings."""
with patch.dict(
os.environ,
{
"CONFLUENCE_URL": "https://test.atlassian.net/wiki",
"CONFLUENCE_USERNAME": "test_username",
"CONFLUENCE_API_TOKEN": "test_token",
"CONFLUENCE_HTTP_TIMEOUT": "25",
"CONFLUENCE_HTTP_CONNECT_TIMEOUT": "6",
"CONFLUENCE_HTTP_READ_TIMEOUT": "18",
"CONFLUENCE_RETRY_MAX": "2",
"CONFLUENCE_RETRY_BACKOFF": "0.7",
"CONFLUENCE_RETRY_STATUS_CODES": "429,502,503",
"CONFLUENCE_RETRY_METHODS": "GET,PUT",
"CONFLUENCE_HTTP_POOL_CONNECTIONS": "12",
"CONFLUENCE_HTTP_POOL_MAXSIZE": "22",
},
clear=True,
):
config = ConfluenceConfig.from_env()
assert config.http_timeout == 25.0
assert config.http_connect_timeout == 6.0
assert config.http_read_timeout == 18.0
assert config.retry_max == 2
assert config.retry_backoff == 0.7
assert config.retry_statuses == (429, 502, 503)
assert config.retry_methods == ("GET", "PUT")
assert config.http_pool_connections == 12
assert config.http_pool_maxsize == 22
def test_from_env_without_client_cert():
"""Test loading config without client certificate settings."""
with patch.dict(
"os.environ",
{
"CONFLUENCE_URL": "https://confluence.example.com",
"CONFLUENCE_PERSONAL_TOKEN": "test_pat",
},
clear=True,
):
config = ConfluenceConfig.from_env()
assert config.url == "https://confluence.example.com"
assert config.client_cert is None
assert config.client_key is None
assert config.client_key_password is None