"""Tests for authentication module.""" import pytest from aegis_gitea_mcp.auth import ( APIKeyValidator, generate_api_key, get_validator, hash_api_key, reset_validator, ) from aegis_gitea_mcp.config import reset_settings @pytest.fixture(autouse=True) def reset_auth(): """Reset authentication state between tests.""" reset_validator() reset_settings() yield reset_validator() reset_settings() @pytest.fixture def mock_env_with_key(monkeypatch): """Set up environment with valid API key.""" monkeypatch.setenv("GITEA_URL", "https://gitea.example.com") monkeypatch.setenv("GITEA_TOKEN", "test-token-12345") monkeypatch.setenv("AUTH_ENABLED", "true") monkeypatch.setenv("MCP_API_KEYS", "a" * 64) # 64-char key @pytest.fixture def validator(mock_env_with_key): """Create API key validator instance.""" return APIKeyValidator() def test_generate_api_key(): """Test API key generation.""" key = generate_api_key(length=64) assert len(key) == 64 assert all(c in "0123456789abcdef" for c in key) def test_generate_api_key_custom_length(): """Test API key generation with custom length.""" key = generate_api_key(length=128) assert len(key) == 128 def test_hash_api_key(): """Test API key hashing.""" key = "test-key-12345" hashed = hash_api_key(key) assert len(hashed) == 64 # SHA256 produces 64-char hex string assert hashed == hash_api_key(key) # Deterministic def test_validator_singleton(mock_env_with_key): """Test that get_validator returns same instance.""" validator1 = get_validator() validator2 = get_validator() assert validator1 is validator2 def test_constant_time_compare(validator): """Test constant-time string comparison.""" # Same strings assert validator._constant_time_compare("test", "test") # Different strings assert not validator._constant_time_compare("test", "fail") # Different lengths assert not validator._constant_time_compare("test", "testing") def test_extract_bearer_token(validator): """Test bearer token extraction from Authorization header.""" # Valid bearer token token = validator.extract_bearer_token("Bearer abc123") assert token == "abc123" # No header token = validator.extract_bearer_token(None) assert token is None # Wrong format token = validator.extract_bearer_token("abc123") assert token is None # Wrong scheme token = validator.extract_bearer_token("Basic abc123") assert token is None # Too many parts token = validator.extract_bearer_token("Bearer abc 123") assert token is None def test_validate_api_key_missing(validator): """Test validation with missing API key.""" is_valid, error = validator.validate_api_key(None, "127.0.0.1", "test-agent") assert not is_valid assert "Authorization header missing" in error def test_validate_api_key_too_short(validator): """Test validation with too-short API key.""" is_valid, error = validator.validate_api_key("short", "127.0.0.1", "test-agent") assert not is_valid assert "Invalid API key format" in error def test_validate_api_key_invalid(validator, mock_env_with_key): """Test validation with invalid API key.""" is_valid, error = validator.validate_api_key("b" * 64, "127.0.0.1", "test-agent") assert not is_valid assert "Invalid API key" in error def test_validate_api_key_valid(validator, mock_env_with_key): """Test validation with valid API key.""" is_valid, error = validator.validate_api_key("a" * 64, "127.0.0.1", "test-agent") assert is_valid assert error is None def test_rate_limiting(validator): """Test rate limiting after multiple failures.""" client_ip = "192.168.1.1" # First 5 attempts should be allowed for _ in range(5): is_valid, error = validator.validate_api_key("b" * 64, client_ip, "test-agent") assert not is_valid # 6th attempt should be rate limited is_valid, error = validator.validate_api_key("b" * 64, client_ip, "test-agent") assert not is_valid assert "Too many failed" in error def test_rate_limiting_per_ip(validator): """Test that rate limiting is per IP address.""" ip1 = "192.168.1.1" ip2 = "192.168.1.2" # Fail 5 times from IP1 for _ in range(5): validator.validate_api_key("b" * 64, ip1, "test-agent") # IP1 should be rate limited is_valid, error = validator.validate_api_key("b" * 64, ip1, "test-agent") assert "Too many failed" in error # IP2 should still work is_valid, error = validator.validate_api_key("b" * 64, ip2, "test-agent") assert "Invalid API key" in error # Wrong key, but not rate limited def test_auth_disabled(monkeypatch): """Test that authentication can be disabled.""" monkeypatch.setenv("GITEA_URL", "https://gitea.example.com") monkeypatch.setenv("GITEA_TOKEN", "test-token-12345") monkeypatch.setenv("AUTH_ENABLED", "false") monkeypatch.setenv("MCP_API_KEYS", "") # No keys needed when disabled validator = APIKeyValidator() # Should allow access without key is_valid, error = validator.validate_api_key(None, "127.0.0.1", "test-agent") assert is_valid assert error is None def test_multiple_keys(monkeypatch): """Test validation with multiple API keys.""" monkeypatch.setenv("GITEA_URL", "https://gitea.example.com") monkeypatch.setenv("GITEA_TOKEN", "test-token-12345") monkeypatch.setenv("AUTH_ENABLED", "true") monkeypatch.setenv("MCP_API_KEYS", f"{'a' * 64},{'b' * 64},{'c' * 64}") validator = APIKeyValidator() # All three keys should work is_valid, _ = validator.validate_api_key("a" * 64, "127.0.0.1", "test-agent") assert is_valid is_valid, _ = validator.validate_api_key("b" * 64, "127.0.0.1", "test-agent") assert is_valid is_valid, _ = validator.validate_api_key("c" * 64, "127.0.0.1", "test-agent") assert is_valid # Invalid key should fail is_valid, _ = validator.validate_api_key("d" * 64, "127.0.0.1", "test-agent") assert not is_valid