209 lines
6.0 KiB
Python
209 lines
6.0 KiB
Python
"""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
|