"""Unit tests for configuration validation and mode detection. Tests cover: - Mode detection logic - Configuration validation for each mode - Error message generation - Edge cases and boundary conditions """ import os from unittest.mock import patch from nextcloud_mcp_server.config import Settings from nextcloud_mcp_server.config_validators import ( AuthMode, detect_auth_mode, get_mode_summary, validate_configuration, ) class TestModeDetection: """Test auth mode detection from configuration.""" def test_smithery_mode_detection(self): """Test Smithery mode is detected from environment variable.""" settings = Settings() with patch.dict(os.environ, {"SMITHERY_DEPLOYMENT": "true"}): mode = detect_auth_mode(settings) assert mode == AuthMode.SMITHERY_STATELESS def test_token_exchange_mode_detection(self): """Test token exchange mode is detected.""" settings = Settings( nextcloud_host="http://localhost", enable_token_exchange=True, ) mode = detect_auth_mode(settings) assert mode == AuthMode.OAUTH_TOKEN_EXCHANGE def test_multi_user_basic_mode_detection(self): """Test multi-user BasicAuth mode is detected.""" settings = Settings( nextcloud_host="http://localhost", enable_multi_user_basic_auth=True, ) mode = detect_auth_mode(settings) assert mode == AuthMode.MULTI_USER_BASIC def test_single_user_basic_mode_detection(self): """Test single-user BasicAuth mode is detected.""" settings = Settings( nextcloud_host="http://localhost", nextcloud_username="admin", nextcloud_password="password", ) mode = detect_auth_mode(settings) assert mode == AuthMode.SINGLE_USER_BASIC def test_oauth_single_audience_default(self): """Test OAuth single-audience is default mode.""" settings = Settings( nextcloud_host="http://localhost", ) mode = detect_auth_mode(settings) assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE def test_mode_priority_smithery_over_all(self): """Test Smithery mode has highest priority.""" settings = Settings( nextcloud_host="http://localhost", nextcloud_username="admin", nextcloud_password="password", enable_token_exchange=True, enable_multi_user_basic_auth=True, ) with patch.dict(os.environ, {"SMITHERY_DEPLOYMENT": "true"}): mode = detect_auth_mode(settings) assert mode == AuthMode.SMITHERY_STATELESS def test_mode_priority_token_exchange_over_basic(self): """Test token exchange has priority over BasicAuth.""" settings = Settings( nextcloud_host="http://localhost", nextcloud_username="admin", nextcloud_password="password", enable_token_exchange=True, ) mode = detect_auth_mode(settings) assert mode == AuthMode.OAUTH_TOKEN_EXCHANGE class TestSingleUserBasicValidation: """Test validation for single-user BasicAuth mode.""" def test_valid_minimal_config(self): """Test valid minimal single-user BasicAuth config.""" settings = Settings( nextcloud_host="http://localhost", nextcloud_username="admin", nextcloud_password="password", ) mode, errors = validate_configuration(settings) assert mode == AuthMode.SINGLE_USER_BASIC assert len(errors) == 0 def test_valid_with_vector_sync(self): """Test valid config with vector sync enabled.""" settings = Settings( nextcloud_host="http://localhost", nextcloud_username="admin", nextcloud_password="password", vector_sync_enabled=True, qdrant_location=":memory:", ollama_base_url="http://ollama:11434", ) mode, errors = validate_configuration(settings) assert mode == AuthMode.SINGLE_USER_BASIC assert len(errors) == 0 def test_missing_required_host(self): """Test error when NEXTCLOUD_HOST is missing.""" settings = Settings( nextcloud_username="admin", nextcloud_password="password", ) mode, errors = validate_configuration(settings) assert mode == AuthMode.SINGLE_USER_BASIC assert any("nextcloud_host" in err.lower() for err in errors) def test_missing_required_username(self): """Test that partial credentials fall back to OAuth mode.""" settings = Settings( nextcloud_host="http://localhost", nextcloud_password="password", # Password without username ) mode, errors = validate_configuration(settings) # Mode detection requires BOTH username AND password for single-user BasicAuth # If only one is present, it defaults to OAuth single-audience assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE # In OAuth mode, having a password set is forbidden assert any("nextcloud_password" in err.lower() for err in errors) def test_missing_required_password(self): """Test that partial credentials fall back to OAuth mode.""" settings = Settings( nextcloud_host="http://localhost", nextcloud_username="admin", # Username without password ) mode, errors = validate_configuration(settings) # Mode detection requires BOTH username AND password for single-user BasicAuth # If only one is present, it defaults to OAuth single-audience assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE # In OAuth mode, having a username set is forbidden assert any("nextcloud_username" in err.lower() for err in errors) def test_forbidden_multi_user_basic_auth(self): """Test error when ENABLE_MULTI_USER_BASIC_AUTH is set.""" settings = Settings( nextcloud_host="http://localhost", nextcloud_username="admin", nextcloud_password="password", enable_multi_user_basic_auth=True, ) # Note: This will detect as MULTI_USER_BASIC due to priority mode, errors = validate_configuration(settings) assert mode == AuthMode.MULTI_USER_BASIC # It will fail multi-user validation because username/password are forbidden assert len(errors) > 0 def test_forbidden_token_exchange(self): """Test error when ENABLE_TOKEN_EXCHANGE is set.""" settings = Settings( nextcloud_host="http://localhost", nextcloud_username="admin", nextcloud_password="password", enable_token_exchange=True, ) # Note: This will detect as OAUTH_TOKEN_EXCHANGE due to priority mode, errors = validate_configuration(settings) assert mode == AuthMode.OAUTH_TOKEN_EXCHANGE # It will fail OAuth validation def test_vector_sync_without_embedding_provider_uses_fallback(self): """Test that vector sync works with Simple provider fallback (no config needed).""" settings = Settings( nextcloud_host="http://localhost", nextcloud_username="admin", nextcloud_password="password", vector_sync_enabled=True, qdrant_location=":memory:", ) mode, errors = validate_configuration(settings) assert mode == AuthMode.SINGLE_USER_BASIC # Should pass - Simple provider is always available as fallback assert len(errors) == 0 class TestMultiUserBasicValidation: """Test validation for multi-user BasicAuth mode.""" def test_valid_minimal_config(self): """Test valid minimal multi-user BasicAuth config.""" settings = Settings( nextcloud_host="http://localhost", enable_multi_user_basic_auth=True, ) mode, errors = validate_configuration(settings) assert mode == AuthMode.MULTI_USER_BASIC assert len(errors) == 0 def test_valid_with_offline_access(self): """Test valid config with offline access enabled.""" settings = Settings( nextcloud_host="http://localhost", enable_multi_user_basic_auth=True, enable_offline_access=True, oidc_client_id="test-client", oidc_client_secret="test-secret", token_encryption_key="test-key-" + "a" * 32, token_storage_db="/tmp/tokens.db", ) mode, errors = validate_configuration(settings) assert mode == AuthMode.MULTI_USER_BASIC assert len(errors) == 0 def test_missing_required_host(self): """Test error when NEXTCLOUD_HOST is missing.""" settings = Settings( enable_multi_user_basic_auth=True, ) mode, errors = validate_configuration(settings) assert mode == AuthMode.MULTI_USER_BASIC assert any("nextcloud_host" in err.lower() for err in errors) def test_forbidden_username_password(self): """Test error when NEXTCLOUD_USERNAME/PASSWORD are set.""" settings = Settings( nextcloud_host="http://localhost", nextcloud_username="admin", nextcloud_password="password", enable_multi_user_basic_auth=True, ) mode, errors = validate_configuration(settings) # Multi-user BasicAuth has higher priority than single-user in detection # (explicit flags come before credentials) assert mode == AuthMode.MULTI_USER_BASIC # Should report errors for forbidden username/password assert any("nextcloud_username" in err.lower() for err in errors) assert any("nextcloud_password" in err.lower() for err in errors) def test_offline_access_missing_oauth_credentials(self): """Test that offline access works without OAuth credentials (will use DCR).""" settings = Settings( nextcloud_host="http://localhost", enable_multi_user_basic_auth=True, enable_offline_access=True, token_encryption_key="test-key-" + "a" * 32, token_storage_db="/tmp/tokens.db", ) mode, errors = validate_configuration(settings) assert mode == AuthMode.MULTI_USER_BASIC # No errors - DCR will be used as fallback (consistent with OAuth modes) assert len(errors) == 0 def test_offline_access_missing_encryption_key(self): """Test error when offline access enabled but encryption key missing.""" settings = Settings( nextcloud_host="http://localhost", enable_multi_user_basic_auth=True, enable_offline_access=True, oidc_client_id="test-client", oidc_client_secret="test-secret", token_storage_db="/tmp/tokens.db", ) mode, errors = validate_configuration(settings) assert mode == AuthMode.MULTI_USER_BASIC assert any("token_encryption_key" in err.lower() for err in errors) def test_vector_sync_auto_enables_background_ops_in_multi_user_mode(self): """Test vector sync automatically enables background operations in multi-user mode (ADR-021).""" # Before ADR-021: This would have failed validation (required explicit ENABLE_OFFLINE_ACCESS) # After ADR-021: vector_sync_enabled auto-enables background operations with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "ENABLE_MULTI_USER_BASIC_AUTH": "true", "VECTOR_SYNC_ENABLED": "true", # Using old name for backward compat test "QDRANT_LOCATION": ":memory:", "OLLAMA_BASE_URL": "http://ollama:11434", "TOKEN_ENCRYPTION_KEY": "test-key", "TOKEN_STORAGE_DB": "/tmp/test.db", "NEXTCLOUD_OIDC_CLIENT_ID": "test-client-id", "NEXTCLOUD_OIDC_CLIENT_SECRET": "test-client-secret", }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() mode, errors = validate_configuration(settings) assert mode == AuthMode.MULTI_USER_BASIC # Should have no errors - background operations auto-enabled assert len(errors) == 0 # Verify background operations were auto-enabled assert settings.enable_offline_access is True class TestOAuthSingleAudienceValidation: """Test validation for OAuth single-audience mode.""" def test_valid_minimal_config(self): """Test valid minimal OAuth single-audience config.""" settings = Settings( nextcloud_host="http://localhost", ) mode, errors = validate_configuration(settings) assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE assert len(errors) == 0 def test_valid_with_static_credentials(self): """Test valid config with static OAuth credentials.""" settings = Settings( nextcloud_host="http://localhost", oidc_client_id="test-client", oidc_client_secret="test-secret", ) mode, errors = validate_configuration(settings) assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE assert len(errors) == 0 def test_valid_with_offline_access(self): """Test valid config with offline access.""" settings = Settings( nextcloud_host="http://localhost", oidc_client_id="test-client", oidc_client_secret="test-secret", enable_offline_access=True, token_encryption_key="test-key-" + "a" * 32, token_storage_db="/tmp/tokens.db", ) mode, errors = validate_configuration(settings) assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE assert len(errors) == 0 def test_forbidden_username_password(self): """Test that username/password trigger single-user mode instead.""" settings = Settings( nextcloud_host="http://localhost", nextcloud_username="admin", nextcloud_password="password", ) mode, errors = validate_configuration(settings) # This should detect as SINGLE_USER_BASIC assert mode == AuthMode.SINGLE_USER_BASIC def test_offline_access_missing_encryption_key(self): """Test error when offline access enabled but encryption key missing.""" settings = Settings( nextcloud_host="http://localhost", enable_offline_access=True, token_storage_db="/tmp/tokens.db", ) mode, errors = validate_configuration(settings) assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE assert any("token_encryption_key" in err.lower() for err in errors) def test_vector_sync_auto_enables_background_ops_in_oauth_mode(self): """Test vector sync automatically enables background operations in OAuth mode (ADR-021).""" # Before ADR-021: This would have failed validation (required explicit ENABLE_OFFLINE_ACCESS) # After ADR-021: vector_sync_enabled auto-enables background operations in multi-user modes with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "VECTOR_SYNC_ENABLED": "true", "QDRANT_LOCATION": ":memory:", "OLLAMA_BASE_URL": "http://ollama:11434", "TOKEN_ENCRYPTION_KEY": "test-key", "TOKEN_STORAGE_DB": "/tmp/test.db", # Note: No username/password = OAuth mode }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() mode, errors = validate_configuration(settings) assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE # Should have no errors - background operations auto-enabled assert len(errors) == 0 # Verify background operations were auto-enabled assert settings.enable_offline_access is True class TestOAuthTokenExchangeValidation: """Test validation for OAuth token exchange mode.""" def test_valid_minimal_config(self): """Test valid minimal OAuth token exchange config.""" settings = Settings( nextcloud_host="http://localhost", enable_token_exchange=True, ) mode, errors = validate_configuration(settings) assert mode == AuthMode.OAUTH_TOKEN_EXCHANGE assert len(errors) == 0 def test_valid_with_credentials(self): """Test valid config with OAuth credentials.""" settings = Settings( nextcloud_host="http://localhost", enable_token_exchange=True, oidc_client_id="test-client", oidc_client_secret="test-secret", ) mode, errors = validate_configuration(settings) assert mode == AuthMode.OAUTH_TOKEN_EXCHANGE assert len(errors) == 0 def test_forbidden_username_password(self): """Test error when username/password are set.""" settings = Settings( nextcloud_host="http://localhost", enable_token_exchange=True, nextcloud_username="admin", nextcloud_password="password", ) mode, errors = validate_configuration(settings) assert mode == AuthMode.OAUTH_TOKEN_EXCHANGE assert any("nextcloud_username" in err.lower() for err in errors) assert any("nextcloud_password" in err.lower() for err in errors) class TestSmitheryValidation: """Test validation for Smithery stateless mode.""" def test_valid_empty_config(self): """Test valid empty config for Smithery mode.""" settings = Settings() with patch.dict(os.environ, {"SMITHERY_DEPLOYMENT": "true"}): mode, errors = validate_configuration(settings) assert mode == AuthMode.SMITHERY_STATELESS assert len(errors) == 0 def test_forbidden_nextcloud_host(self): """Test error when NEXTCLOUD_HOST is set.""" settings = Settings( nextcloud_host="http://localhost", ) with patch.dict(os.environ, {"SMITHERY_DEPLOYMENT": "true"}): mode, errors = validate_configuration(settings) assert mode == AuthMode.SMITHERY_STATELESS assert any("nextcloud_host" in err.lower() for err in errors) def test_forbidden_credentials(self): """Test error when credentials are set.""" settings = Settings( nextcloud_username="admin", nextcloud_password="password", ) with patch.dict(os.environ, {"SMITHERY_DEPLOYMENT": "true"}): mode, errors = validate_configuration(settings) assert mode == AuthMode.SMITHERY_STATELESS assert any("nextcloud_username" in err.lower() for err in errors) def test_forbidden_vector_sync(self): """Test error when vector sync is enabled.""" settings = Settings( vector_sync_enabled=True, ) with patch.dict(os.environ, {"SMITHERY_DEPLOYMENT": "true"}): mode, errors = validate_configuration(settings) assert mode == AuthMode.SMITHERY_STATELESS assert any("vector_sync_enabled" in err.lower() for err in errors) class TestModeSummary: """Test mode summary generation.""" def test_single_user_basic_summary(self): """Test summary for single-user BasicAuth mode.""" summary = get_mode_summary(AuthMode.SINGLE_USER_BASIC) assert "single_user_basic" in summary assert "NEXTCLOUD_HOST" in summary assert "NEXTCLOUD_USERNAME" in summary assert "NEXTCLOUD_PASSWORD" in summary assert "VECTOR_SYNC_ENABLED" in summary def test_smithery_summary(self): """Test summary for Smithery mode.""" summary = get_mode_summary(AuthMode.SMITHERY_STATELESS) assert "smithery" in summary assert "session" in summary.lower() assert "(none" in summary # No required config def test_oauth_token_exchange_summary(self): """Test summary for OAuth token exchange mode.""" summary = get_mode_summary(AuthMode.OAUTH_TOKEN_EXCHANGE) assert "oauth_exchange" in summary assert "ENABLE_TOKEN_EXCHANGE" in summary assert "RFC 8693" in summary class TestEdgeCases: """Test edge cases and boundary conditions.""" def test_empty_string_treated_as_missing(self): """Test that empty strings are treated as missing values.""" settings = Settings( nextcloud_host="", # Empty string nextcloud_username="admin", nextcloud_password="password", ) mode, errors = validate_configuration(settings) # Should fail because nextcloud_host is effectively missing assert any("nextcloud_host" in err.lower() for err in errors) def test_whitespace_treated_as_missing(self): """Test that whitespace-only strings are treated as missing.""" settings = Settings( nextcloud_host=" ", # Whitespace only nextcloud_username="admin", nextcloud_password="password", ) mode, errors = validate_configuration(settings) # Should fail because nextcloud_host is effectively missing assert any("nextcloud_host" in err.lower() for err in errors) def test_multiple_errors_reported(self): """Test that multiple errors are all reported.""" settings = Settings( # Missing all required fields for single-user BasicAuth ) mode, errors = validate_configuration(settings) # Should have errors for missing host (OAuth mode is default) assert len(errors) > 0 class TestConfigurationConsolidation: """Test ADR-021 configuration consolidation and backward compatibility. Tests verify: - New variable names work (ENABLE_SEMANTIC_SEARCH, ENABLE_BACKGROUND_OPERATIONS) - Old variable names still work (VECTOR_SYNC_ENABLED, ENABLE_OFFLINE_ACCESS) - Deprecation warnings are logged - Auto-enablement of background operations in multi-user modes """ def test_new_semantic_search_variable_name(self): """Test ENABLE_SEMANTIC_SEARCH (new name) works correctly.""" with patch.dict( os.environ, { "ENABLE_SEMANTIC_SEARCH": "true", "QDRANT_LOCATION": ":memory:", }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() assert settings.vector_sync_enabled is True def test_old_vector_sync_variable_name_backward_compat(self): """Test VECTOR_SYNC_ENABLED (old name) still works for backward compatibility.""" with patch.dict( os.environ, { "VECTOR_SYNC_ENABLED": "true", "QDRANT_LOCATION": ":memory:", }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() assert settings.vector_sync_enabled is True def test_new_background_operations_variable_name(self): """Test ENABLE_BACKGROUND_OPERATIONS (new name) works correctly.""" with patch.dict( os.environ, { "ENABLE_BACKGROUND_OPERATIONS": "true", "TOKEN_ENCRYPTION_KEY": "test-key", "TOKEN_STORAGE_DB": "/tmp/test.db", }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() assert settings.enable_offline_access is True def test_old_offline_access_variable_name_backward_compat(self): """Test ENABLE_OFFLINE_ACCESS (old name) still works for backward compatibility.""" with patch.dict( os.environ, { "ENABLE_OFFLINE_ACCESS": "true", "TOKEN_ENCRYPTION_KEY": "test-key", "TOKEN_STORAGE_DB": "/tmp/test.db", }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() assert settings.enable_offline_access is True def test_semantic_search_auto_enables_background_ops_in_oauth_mode(self): """Test ENABLE_SEMANTIC_SEARCH automatically enables background operations in OAuth mode.""" with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "ENABLE_SEMANTIC_SEARCH": "true", "QDRANT_LOCATION": ":memory:", "TOKEN_ENCRYPTION_KEY": "test-key", "TOKEN_STORAGE_DB": "/tmp/test.db", # Note: No NEXTCLOUD_USERNAME/PASSWORD = OAuth mode }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() # Semantic search enabled assert settings.vector_sync_enabled is True # Background operations auto-enabled (even though not explicitly set) assert settings.enable_offline_access is True def test_semantic_search_does_not_auto_enable_in_single_user_mode(self): """Test ENABLE_SEMANTIC_SEARCH does NOT auto-enable background ops in single-user mode.""" with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "NEXTCLOUD_USERNAME": "admin", "NEXTCLOUD_PASSWORD": "password", "ENABLE_SEMANTIC_SEARCH": "true", "QDRANT_LOCATION": ":memory:", # Note: Username/password set = single-user BasicAuth mode }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() # Semantic search enabled assert settings.vector_sync_enabled is True # Background operations NOT auto-enabled (not needed in single-user mode) assert settings.enable_offline_access is False def test_explicit_background_ops_still_works(self): """Test explicitly setting ENABLE_BACKGROUND_OPERATIONS works even without semantic search.""" with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "ENABLE_BACKGROUND_OPERATIONS": "true", "TOKEN_ENCRYPTION_KEY": "test-key", "TOKEN_STORAGE_DB": "/tmp/test.db", # Note: No semantic search enabled }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() # Semantic search NOT enabled assert settings.vector_sync_enabled is False # Background operations explicitly enabled assert settings.enable_offline_access is True def test_both_old_and_new_semantic_search_names_prefers_new(self): """Test setting both ENABLE_SEMANTIC_SEARCH and VECTOR_SYNC_ENABLED uses new name.""" with patch.dict( os.environ, { "ENABLE_SEMANTIC_SEARCH": "true", "VECTOR_SYNC_ENABLED": "false", # Old name says false "QDRANT_LOCATION": ":memory:", }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() # Should use new name value (true) assert settings.vector_sync_enabled is True def test_both_old_and_new_background_ops_names_prefers_new(self): """Test setting both ENABLE_BACKGROUND_OPERATIONS and ENABLE_OFFLINE_ACCESS uses new name.""" with patch.dict( os.environ, { "ENABLE_BACKGROUND_OPERATIONS": "true", "ENABLE_OFFLINE_ACCESS": "false", # Old name says false "TOKEN_ENCRYPTION_KEY": "test-key", "TOKEN_STORAGE_DB": "/tmp/test.db", }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() # Should use new name value (true) assert settings.enable_offline_access is True def test_validation_no_longer_requires_both_variables(self): """Test validation no longer requires explicit ENABLE_OFFLINE_ACCESS when semantic search enabled.""" with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "ENABLE_MULTI_USER_BASIC_AUTH": "true", "ENABLE_SEMANTIC_SEARCH": "true", "QDRANT_LOCATION": ":memory:", "TOKEN_ENCRYPTION_KEY": "test-key", "TOKEN_STORAGE_DB": "/tmp/test.db", # OAuth credentials required for app password retrieval (when background ops enabled) "NEXTCLOUD_OIDC_CLIENT_ID": "test-client-id", "NEXTCLOUD_OIDC_CLIENT_SECRET": "test-client-secret", # Note: ENABLE_OFFLINE_ACCESS not set - should auto-enable }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() mode, errors = validate_configuration(settings) # Should have no validation errors # (Previously would have required explicit ENABLE_OFFLINE_ACCESS) assert len(errors) == 0 assert mode == AuthMode.MULTI_USER_BASIC # Verify background operations were auto-enabled assert settings.enable_offline_access is True class TestExplicitModeSelection: """Test ADR-021 explicit mode selection via MCP_DEPLOYMENT_MODE. Tests verify: - Explicit mode selection works for all modes - Invalid mode names raise ValueError - Explicit mode takes precedence over auto-detection """ def test_explicit_single_user_basic_mode(self): """Test explicit single_user_basic mode selection.""" with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "MCP_DEPLOYMENT_MODE": "single_user_basic", "NEXTCLOUD_USERNAME": "admin", "NEXTCLOUD_PASSWORD": "password", }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() mode = detect_auth_mode(settings) assert mode == AuthMode.SINGLE_USER_BASIC def test_explicit_multi_user_basic_mode(self): """Test explicit multi_user_basic mode selection.""" with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "MCP_DEPLOYMENT_MODE": "multi_user_basic", }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() mode = detect_auth_mode(settings) assert mode == AuthMode.MULTI_USER_BASIC def test_explicit_oauth_single_audience_mode(self): """Test explicit oauth_single_audience mode selection.""" with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "MCP_DEPLOYMENT_MODE": "oauth_single_audience", }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() mode = detect_auth_mode(settings) assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE def test_explicit_oauth_token_exchange_mode(self): """Test explicit oauth_token_exchange mode selection.""" with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "MCP_DEPLOYMENT_MODE": "oauth_token_exchange", }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() mode = detect_auth_mode(settings) assert mode == AuthMode.OAUTH_TOKEN_EXCHANGE def test_explicit_smithery_mode(self): """Test explicit smithery mode selection.""" with patch.dict( os.environ, { "MCP_DEPLOYMENT_MODE": "smithery", }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() mode = detect_auth_mode(settings) assert mode == AuthMode.SMITHERY_STATELESS def test_invalid_deployment_mode_raises_error(self): """Test invalid MCP_DEPLOYMENT_MODE raises ValueError.""" with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "MCP_DEPLOYMENT_MODE": "invalid_mode", }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() # Should raise ValueError with clear message try: detect_auth_mode(settings) assert False, "Should have raised ValueError" except ValueError as e: assert "Invalid MCP_DEPLOYMENT_MODE" in str(e) assert "invalid_mode" in str(e) assert "Valid values:" in str(e) def test_explicit_mode_overrides_auto_detection(self): """Test explicit mode takes precedence over auto-detection.""" with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "NEXTCLOUD_USERNAME": "admin", # Would auto-detect as single_user_basic "NEXTCLOUD_PASSWORD": "password", "MCP_DEPLOYMENT_MODE": "oauth_single_audience", # Explicit override }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() mode = detect_auth_mode(settings) # Should use explicit mode, not auto-detected mode assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE def test_case_insensitive_mode_names(self): """Test MCP_DEPLOYMENT_MODE is case-insensitive.""" with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "MCP_DEPLOYMENT_MODE": "OAUTH_SINGLE_AUDIENCE", # Uppercase }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() mode = detect_auth_mode(settings) assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE def test_whitespace_in_mode_name_stripped(self): """Test whitespace in MCP_DEPLOYMENT_MODE is stripped.""" with patch.dict( os.environ, { "NEXTCLOUD_HOST": "http://localhost:8080", "MCP_DEPLOYMENT_MODE": " oauth_single_audience ", # Whitespace }, clear=True, ): from nextcloud_mcp_server.config import get_settings settings = get_settings() mode = detect_auth_mode(settings) assert mode == AuthMode.OAUTH_SINGLE_AUDIENCE