Skip to main content
Glama

Codebase MCP Server

by Ravenight13
test_invalid_project_id.py13.2 kB
"""T015: Contract tests for invalid project_id validation. Tests validate that MCP tools reject invalid project identifiers with proper ValidationError responses, preventing security vulnerabilities and database errors. Constitutional Compliance: - Principle III: Protocol Compliance (MCP-compliant error responses) - Principle V: Production Quality (comprehensive validation) - Principle VII: Test-Driven Development (contract validation) - Principle VIII: Type Safety (mypy --strict compliance) - Principle XVI: Security-first design (SQL injection prevention) Functional Requirements: - FR-004: System MUST validate project identifiers before use - FR-005: System MUST enforce lowercase alphanumeric with hyphen format - FR-006: System MUST enforce maximum 50-character length - FR-007: System MUST prevent identifiers starting or ending with hyphens - FR-008: System MUST prevent consecutive hyphens - FR-016: System MUST prevent security vulnerabilities via identifier validation """ from __future__ import annotations from pathlib import Path from unittest.mock import AsyncMock, patch import pytest from pydantic import ValidationError from src.mcp.tools.indexing import index_repository from src.mcp.tools.search import search_code # Extract underlying functions from FastMCP FunctionTool wrappers index_repository_fn = index_repository.fn search_code_fn = search_code.fn @pytest.mark.contract @pytest.mark.parametrize( "invalid_id,reason", [ ("My_Project", "Uppercase and underscore"), ("My-Project", "Uppercase letter"), ("my_project", "Underscore instead of hyphen"), ("-project", "Leading hyphen"), ("project-", "Trailing hyphen"), ("project--name", "Consecutive hyphens"), ("project'; DROP TABLE--", "SQL injection attempt"), ("a" * 51, "51+ characters (exceeds max length)"), ("Project-123", "Uppercase letter at start"), ("my-Project", "Uppercase letter in middle"), ("", "Empty string"), ("my project", "Contains space"), ("my/project", "Contains slash"), ("my.project", "Contains dot"), ("my@project", "Contains special character"), ], ) @pytest.mark.asyncio async def test_index_repository_invalid_project_id_validation( invalid_id: str, reason: str ) -> None: """Test index_repository rejects invalid project identifiers. Validates: - Invalid project_id formats are rejected - ValueError is raised with detailed message - No database operations are attempted - Error message explains the constraint violation Traces to: - contracts/mcp-tools.yaml lines 161-173 (ValidationError response) - src/models/project_identifier.py lines 66-109 (validation logic) """ # Create temporary directory for testing test_repo = Path("/tmp/test-repo-invalid") test_repo.mkdir(exist_ok=True, parents=True) try: # Attempt to call tool with invalid project_id # Should raise ValueError during ProjectIdentifier validation with pytest.raises(ValueError) as exc_info: await index_repository_fn( repo_path=str(test_repo), project_id=invalid_id, force_reindex=False, ) # Validate error message contains helpful information error_msg = str(exc_info.value) assert "Invalid project_id format" in error_msg or "Project identifier must be lowercase" in error_msg # Log reason for test documentation print(f"✅ Rejected: {invalid_id} - {reason}") finally: # Cleanup if test_repo.exists(): test_repo.rmdir() @pytest.mark.contract @pytest.mark.parametrize( "invalid_id,reason", [ ("My_Project", "Uppercase and underscore"), ("FRONTEND", "All uppercase"), ("-project", "Leading hyphen"), ("project-", "Trailing hyphen"), ("project--name", "Consecutive hyphens"), ("project'; DROP TABLE--", "SQL injection attempt"), ("a" * 51, "51+ characters (exceeds max length)"), ("123-project-" + "x" * 50, "Far exceeds max length"), ("my project", "Contains space"), ("my/project", "Contains slash"), ], ) @pytest.mark.asyncio async def test_search_code_invalid_project_id_validation( invalid_id: str, reason: str ) -> None: """Test search_code rejects invalid project identifiers. Validates: - Invalid project_id formats are rejected - ValueError is raised with detailed message - No database operations are attempted - Error message explains the constraint violation Traces to: - contracts/mcp-tools.yaml lines 268-273 (ValidationError response) - src/models/project_identifier.py lines 66-109 (validation logic) """ # Attempt to call tool with invalid project_id # Should raise ValueError during ProjectIdentifier validation with pytest.raises(ValueError) as exc_info: await search_code_fn( query="test query", project_id=invalid_id, limit=10, ) # Validate error message contains helpful information error_msg = str(exc_info.value) assert "Invalid project_id" in error_msg or "Project identifier must be lowercase" in error_msg # Log reason for test documentation print(f"✅ Rejected: {invalid_id} - {reason}") @pytest.mark.contract @pytest.mark.asyncio async def test_uppercase_project_id_rejected() -> None: """Test uppercase project_id returns 400 ValidationError. Specific test case from tasks.md requirement. Validates: - "My_Project" is rejected (uppercase + underscore) - ValueError contains explanation of format rules - Error message includes examples of valid identifiers Traces to: tasks.md lines 206-207 """ test_repo = Path("/tmp/test-repo-uppercase") test_repo.mkdir(exist_ok=True, parents=True) try: with pytest.raises(ValueError) as exc_info: await index_repository_fn( repo_path=str(test_repo), project_id="My_Project", force_reindex=False, ) error_msg = str(exc_info.value) # Should contain format explanation assert "lowercase" in error_msg.lower() # Should contain examples assert "client-a" in error_msg or "Examples" in error_msg finally: if test_repo.exists(): test_repo.rmdir() @pytest.mark.contract @pytest.mark.asyncio async def test_leading_hyphen_project_id_rejected() -> None: """Test leading hyphen project_id returns 400 ValidationError. Specific test case from tasks.md requirement. Validates: - "-project" is rejected (leading hyphen) - ValueError explains constraint violation - Error message helpful for user correction Traces to: tasks.md lines 208-209 """ test_repo = Path("/tmp/test-repo-leading-hyphen") test_repo.mkdir(exist_ok=True, parents=True) try: with pytest.raises(ValueError) as exc_info: await index_repository_fn( repo_path=str(test_repo), project_id="-project", force_reindex=False, ) error_msg = str(exc_info.value) # Should explain hyphen constraint assert "hyphen" in error_msg.lower() finally: if test_repo.exists(): test_repo.rmdir() @pytest.mark.contract @pytest.mark.asyncio async def test_sql_injection_project_id_rejected() -> None: """Test SQL injection attempt project_id returns 400 ValidationError. Specific test case from tasks.md requirement. Validates: - "project'; DROP TABLE--" is rejected (SQL injection) - Validation prevents database security vulnerability - Error message does not leak security information Traces to: tasks.md lines 210-211 """ test_repo = Path("/tmp/test-repo-sql-injection") test_repo.mkdir(exist_ok=True, parents=True) try: with pytest.raises(ValueError) as exc_info: await index_repository_fn( repo_path=str(test_repo), project_id="project'; DROP TABLE--", force_reindex=False, ) error_msg = str(exc_info.value) # Should reject due to format violation (single quote, semicolon, space) assert "Invalid project_id format" in error_msg or "lowercase" in error_msg.lower() # Should NOT leak security information about SQL # (generic validation error is correct behavior) finally: if test_repo.exists(): test_repo.rmdir() @pytest.mark.contract @pytest.mark.asyncio async def test_max_length_exceeded_project_id_rejected() -> None: """Test 51+ character project_id returns 400 ValidationError. Specific test case from tasks.md requirement. Validates: - 51+ character identifier is rejected - Validation enforces 50-character maximum - Error message mentions length constraint Traces to: tasks.md lines 212-213 """ # Create 51-character identifier long_id = "a" * 51 assert len(long_id) == 51 test_repo = Path("/tmp/test-repo-max-length") test_repo.mkdir(exist_ok=True, parents=True) try: with pytest.raises(ValueError) as exc_info: await index_repository_fn( repo_path=str(test_repo), project_id=long_id, force_reindex=False, ) error_msg = str(exc_info.value) # Should mention length or max length constraint # Pydantic ValidationError from Field(max_length=50) assert "50" in error_msg or "length" in error_msg.lower() finally: if test_repo.exists(): test_repo.rmdir() @pytest.mark.contract @pytest.mark.asyncio async def test_consecutive_hyphens_project_id_rejected() -> None: """Test consecutive hyphens project_id returns 400 ValidationError. Validates: - "project--name" is rejected (consecutive hyphens) - Validation enforces single-hyphen separation - Error message explains constraint Traces to: contracts/mcp-tools.yaml lines 28-46 """ test_repo = Path("/tmp/test-repo-consecutive-hyphens") test_repo.mkdir(exist_ok=True, parents=True) try: with pytest.raises(ValueError) as exc_info: await index_repository_fn( repo_path=str(test_repo), project_id="project--name", force_reindex=False, ) error_msg = str(exc_info.value) # Should mention consecutive hyphens assert "consecutive hyphens" in error_msg or "hyphen" in error_msg.lower() finally: if test_repo.exists(): test_repo.rmdir() @pytest.mark.contract @pytest.mark.asyncio async def test_empty_project_id_rejected() -> None: """Test empty string project_id returns 400 ValidationError. Validates: - Empty string is rejected - Validation enforces min_length=1 - Error message mentions required constraint Traces to: contracts/mcp-tools.yaml lines 28-46 """ test_repo = Path("/tmp/test-repo-empty") test_repo.mkdir(exist_ok=True, parents=True) try: with pytest.raises(ValueError) as exc_info: await index_repository_fn( repo_path=str(test_repo), project_id="", force_reindex=False, ) error_msg = str(exc_info.value) # Should mention length or required constraint assert "at least 1 character" in error_msg or "length" in error_msg.lower() finally: if test_repo.exists(): test_repo.rmdir() @pytest.mark.contract def test_project_identifier_validation_comprehensive() -> None: """Test ProjectIdentifier model validation comprehensively. Validates: - Direct Pydantic model validation - All constraint violations raise ValidationError - Error messages are informative This test validates the model directly without MCP tool invocation. Traces to: src/models/project_identifier.py lines 33-127 """ from src.models.project_identifier import ProjectIdentifier # Valid identifiers should pass valid_ids = [ "client-a", "frontend", "my-project-123", "a", "a" * 50, # Max length "project-with-many-hyphens-separating-words", ] for valid_id in valid_ids: identifier = ProjectIdentifier(value=valid_id) assert identifier.value == valid_id print(f"✅ Valid: {valid_id}") # Invalid identifiers should raise ValidationError invalid_ids = [ "My_Project", # Uppercase + underscore "-project", # Leading hyphen "project-", # Trailing hyphen "project--name", # Consecutive hyphens "a" * 51, # Exceeds max length "", # Empty string "my project", # Space "my.project", # Dot ] for invalid_id in invalid_ids: with pytest.raises(ValidationError): ProjectIdentifier(value=invalid_id) print(f"✅ Rejected: {invalid_id}")

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ravenight13/codebase-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server