"""Unit tests for BasicAuthMiddleware.""" import base64 import pytest from nextcloud_mcp_server.app import BasicAuthMiddleware class MockApp: """Mock ASGI app for testing middleware.""" def __init__(self): self.called = False self.received_scope = None async def __call__(self, scope, receive, send): self.called = True self.received_scope = scope @pytest.mark.unit async def test_basic_auth_middleware_valid_credentials(): """Test that middleware correctly extracts valid BasicAuth credentials.""" # Arrange mock_app = MockApp() middleware = BasicAuthMiddleware(mock_app) credentials = base64.b64encode(b"admin:password123").decode("utf-8") scope = { "type": "http", "headers": [(b"authorization", f"Basic {credentials}".encode())], } # Act await middleware(scope, None, None) # type: ignore[arg-type] # Assert assert mock_app.called assert "state" in scope assert "basic_auth" in scope["state"] assert scope["state"]["basic_auth"]["username"] == "admin" assert scope["state"]["basic_auth"]["password"] == "password123" @pytest.mark.unit async def test_basic_auth_middleware_password_with_colon(): """Test that middleware handles passwords containing colons.""" # Arrange mock_app = MockApp() middleware = BasicAuthMiddleware(mock_app) # Password contains colon - should split on first colon only credentials = base64.b64encode(b"user:pass:word:123").decode("utf-8") scope = { "type": "http", "headers": [(b"authorization", f"Basic {credentials}".encode())], } # Act await middleware(scope, None, None) # type: ignore[arg-type] # Assert assert scope["state"]["basic_auth"]["username"] == "user" assert scope["state"]["basic_auth"]["password"] == "pass:word:123" @pytest.mark.unit async def test_basic_auth_middleware_invalid_base64(): """Test that middleware handles invalid base64 encoding gracefully.""" # Arrange mock_app = MockApp() middleware = BasicAuthMiddleware(mock_app) scope = { "type": "http", "headers": [(b"authorization", b"Basic INVALID_BASE64!!!")], } # Act await middleware(scope, None, None) # type: ignore[arg-type] # Assert assert mock_app.called # Should not have basic_auth in state due to error assert "basic_auth" not in scope.get("state", {}) @pytest.mark.unit async def test_basic_auth_middleware_missing_authorization_header(): """Test that middleware handles missing Authorization header.""" # Arrange mock_app = MockApp() middleware = BasicAuthMiddleware(mock_app) scope = { "type": "http", "headers": [], } # Act await middleware(scope, None, None) # type: ignore[arg-type] # Assert assert mock_app.called # Should not have basic_auth in state assert "basic_auth" not in scope.get("state", {}) @pytest.mark.unit async def test_basic_auth_middleware_wrong_auth_scheme(): """Test that middleware ignores non-Basic auth schemes.""" # Arrange mock_app = MockApp() middleware = BasicAuthMiddleware(mock_app) scope = { "type": "http", "headers": [(b"authorization", b"Bearer some_token")], } # Act await middleware(scope, None, None) # type: ignore[arg-type] # Assert assert mock_app.called # Should not have basic_auth in state assert "basic_auth" not in scope.get("state", {}) @pytest.mark.unit async def test_basic_auth_middleware_malformed_credentials(): """Test that middleware handles credentials without colon separator.""" # Arrange mock_app = MockApp() middleware = BasicAuthMiddleware(mock_app) # Credentials without colon separator credentials = base64.b64encode(b"username_no_password").decode("utf-8") scope = { "type": "http", "headers": [(b"authorization", f"Basic {credentials}".encode())], } # Act await middleware(scope, None, None) # type: ignore[arg-type] # Assert assert mock_app.called # Should not have basic_auth in state due to error assert "basic_auth" not in scope.get("state", {}) @pytest.mark.unit async def test_basic_auth_middleware_non_http_scope(): """Test that middleware passes through non-HTTP scopes unchanged.""" # Arrange mock_app = MockApp() middleware = BasicAuthMiddleware(mock_app) scope = { "type": "websocket", "headers": [(b"authorization", b"Basic dXNlcjpwYXNz")], } # Act await middleware(scope, None, None) # type: ignore[arg-type] # Assert assert mock_app.called # Should not process websocket scopes assert "state" not in scope @pytest.mark.unit async def test_basic_auth_middleware_preserves_existing_state(): """Test that middleware preserves existing state data.""" # Arrange mock_app = MockApp() middleware = BasicAuthMiddleware(mock_app) credentials = base64.b64encode(b"user:pass").decode("utf-8") scope = { "type": "http", "headers": [(b"authorization", f"Basic {credentials}".encode())], "state": {"existing_key": "existing_value"}, } # Act await middleware(scope, None, None) # type: ignore[arg-type] # Assert assert mock_app.called assert scope["state"]["existing_key"] == "existing_value" assert scope["state"]["basic_auth"]["username"] == "user" assert scope["state"]["basic_auth"]["password"] == "pass" @pytest.mark.unit async def test_basic_auth_middleware_empty_password(): """Test that middleware handles empty passwords.""" # Arrange mock_app = MockApp() middleware = BasicAuthMiddleware(mock_app) credentials = base64.b64encode(b"user:").decode("utf-8") scope = { "type": "http", "headers": [(b"authorization", f"Basic {credentials}".encode())], } # Act await middleware(scope, None, None) # type: ignore[arg-type] # Assert assert mock_app.called assert scope["state"]["basic_auth"]["username"] == "user" assert scope["state"]["basic_auth"]["password"] == "" @pytest.mark.unit async def test_basic_auth_middleware_unicode_credentials(): """Test that middleware handles Unicode characters in credentials.""" # Arrange mock_app = MockApp() middleware = BasicAuthMiddleware(mock_app) # Username and password with Unicode characters credentials = base64.b64encode("üser:pässwörd".encode("utf-8")).decode("utf-8") scope = { "type": "http", "headers": [(b"authorization", f"Basic {credentials}".encode())], } # Act await middleware(scope, None, None) # type: ignore[arg-type] # Assert assert mock_app.called assert scope["state"]["basic_auth"]["username"] == "üser" assert scope["state"]["basic_auth"]["password"] == "pässwörd"