Skip to main content
Glama
johannhartmann

MCP Code Analysis Server

test_bounded_context_tools.py21.1 kB
"""Tests for bounded context analysis tools.""" from typing import Any, Tuple # noqa: UP035 from unittest.mock import AsyncMock, MagicMock import pytest from fastmcp import FastMCP from sqlalchemy.ext.asyncio import AsyncSession from src.database.domain_models import ( BoundedContext, BoundedContextMembership, ContextRelationship, DomainEntity, ) from src.mcp_server.tools.domain_tools import DomainTools @pytest.fixture def mock_db_session() -> AsyncSession: """Create mock database session.""" return AsyncMock(spec=AsyncSession) @pytest.fixture def mock_mcp() -> FastMCP: """Create mock FastMCP instance.""" mcp = MagicMock(spec=FastMCP) mcp.tool = MagicMock(side_effect=lambda **kwargs: lambda func: func) return mcp # Helper builders to reduce statements in tests def build_bounded_context_with_memberships() -> tuple[BoundedContext, MagicMock]: mock_context = MagicMock(spec=BoundedContext) mock_context.id = 1 mock_context.name = "OrderContext" mock_context.description = "Order management bounded context" mock_context.ubiquitous_language = [ "Order", "OrderItem", "Customer", "Payment", "Shipping", ] mock_context.core_concepts = [ "Order fulfillment", "Payment processing", "Inventory management", ] mock_context.cohesion_score = 0.85 mock_context.coupling_score = 0.3 mock_context.modularity_score = 0.75 memberships = [] for _i, (_entity_type, count) in enumerate( [ ("aggregate_root", 2), ("entity", 5), ("value_object", 3), ("domain_service", 1), ] ): for j in range(count): membership = MagicMock(spec=BoundedContextMembership) membership.domain_entity_id = _i * 10 + j memberships.append(membership) mock_context.memberships = memberships context_result = MagicMock() context_result.scalar_one_or_none.return_value = mock_context return mock_context, context_result def build_entities_and_result() -> tuple[list[DomainEntity], MagicMock]: entities = [] entity_types = [ ("aggregate_root", "Order", 2), ("entity", "OrderItem", 5), ("value_object", "Money", 3), ("domain_service", "PricingService", 1), ] for entity_type, base_name, count in entity_types: for i in range(count): entity = MagicMock(spec=DomainEntity) entity.name = f"{base_name}{i}" if count > 1 else base_name entity.entity_type = entity_type entity.description = f"{entity_type} for {base_name}" entities.append(entity) entity_result = MagicMock() entity_result.scalars.return_value.all.return_value = entities return entities, entity_result def build_internal_and_external_relationship_results( entities: list[DomainEntity], ) -> tuple[MagicMock, MagicMock]: # Internal internal_rels = [] for i in range(3): rel = MagicMock() rel.source_entity = entities[i] rel.target_entity = entities[i + 1] rel.relationship_type = ["aggregates", "uses", "depends_on"][i % 3] internal_rels.append(rel) rel_result = MagicMock() rel_result.scalars.return_value.all.return_value = internal_rels # External external_entity = MagicMock(spec=DomainEntity) external_entity.name = "ExternalPaymentGateway" external_rels = [] for i in range(2): rel = MagicMock() rel.source_entity = entities[i] rel.target_entity = external_entity rel.relationship_type = "integrates_with" external_rels.append(rel) ext_rel_result = MagicMock() ext_rel_result.scalars.return_value.all.return_value = external_rels return rel_result, ext_rel_result def build_summary_none_result() -> MagicMock: summary_result = MagicMock() summary_result.scalar_one_or_none.return_value = None return summary_result @pytest.fixture def domain_tools(mock_db_session: Any, mock_mcp: Any) -> DomainTools: """Create domain tools fixture for bounded context tests.""" return DomainTools(mock_db_session, mock_mcp) class TestBoundedContextTools: """Tests specifically for bounded context related tools.""" @pytest.mark.asyncio async def test_analyze_bounded_context_with_relationships( self, domain_tools: DomainTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test analyzing bounded context with complex relationships.""" # Mock bounded context, entities, and relationships via helpers mock_context, context_result = build_bounded_context_with_memberships() entities, entity_result = build_entities_and_result() rel_result, ext_rel_result = build_internal_and_external_relationship_results( entities ) summary_result = build_summary_none_result() monkeypatch.setattr( mock_db_session, "execute", AsyncMock( side_effect=[ context_result, entity_result, rel_result, ext_rel_result, summary_result, ] ), ) result = await domain_tools.analyze_bounded_context("OrderContext") assert result["name"] == "OrderContext" assert result["total_entities"] == 11 assert result["entities_by_type"]["aggregate_root"][0]["name"] == "Order" assert len(result["entities_by_type"]["entity"]) == 5 assert len(result["entities_by_type"]["value_object"]) == 3 assert result["internal_relationships"] == 3 assert result["external_dependencies"] == 2 assert result["cohesion_score"] == 0.85 assert result["summary"] is None @pytest.mark.asyncio async def test_find_bounded_contexts_with_filtering( self, domain_tools: DomainTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test finding bounded contexts with entity count filtering.""" # Create contexts with varying entity counts contexts = [] for _i, (name, entity_count, cohesion, ctx_type) in enumerate( [ ("UserContext", 10, 0.95, "core"), ("NotificationContext", 2, 0.7, "supporting"), # Below threshold ("PaymentContext", 5, 0.85, "core"), ("ReportingContext", 1, 0.6, "generic"), # Below threshold ("InventoryContext", 7, 0.8, "supporting"), ] ): context = MagicMock(spec=BoundedContext) context.name = name context.description = f"{name} description" context.core_concepts = [f"Concept{j}" for j in range(min(5, entity_count))] context.cohesion_score = cohesion context.context_type = ctx_type context.memberships = [MagicMock() for _ in range(entity_count)] contexts.append(context) context_result = MagicMock() context_result.scalars.return_value.all.return_value = contexts monkeypatch.setattr( mock_db_session, "execute", AsyncMock(return_value=context_result), ) # Test with default min_entities (3) result = await domain_tools.find_bounded_contexts() assert len(result) == 3 # Only contexts with >= 3 entities assert result[0]["name"] == "UserContext" # Highest cohesion assert result[0]["entity_count"] == 10 assert result[0]["type"] == "core" assert result[1]["name"] == "PaymentContext" assert result[2]["name"] == "InventoryContext" # Test with higher threshold result = await domain_tools.find_bounded_contexts(min_entities=6) assert len(result) == 2 # Only UserContext and InventoryContext @pytest.mark.asyncio async def test_generate_context_map_with_all_relationship_types( self, domain_tools: DomainTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test generating context map with all relationship types.""" # Create contexts contexts = [] for i, (name, ctx_type) in enumerate( [ ("OrderContext", "core"), ("InventoryContext", "supporting"), ("PaymentContext", "core"), ("ShippingContext", "supporting"), ("LegacyContext", "generic"), ] ): ctx = MagicMock(spec=BoundedContext) ctx.id = i ctx.name = name ctx.context_type = ctx_type ctx.description = f"{name} description" contexts.append(ctx) context_result = MagicMock() context_result.scalars.return_value.all.return_value = contexts # Create various relationship types relationship_types = [ ("shared_kernel", contexts[0], contexts[1]), ("customer_supplier", contexts[0], contexts[2]), ("conformist", contexts[1], contexts[0]), ("anti_corruption_layer", contexts[0], contexts[4]), ("open_host_service", contexts[2], contexts[3]), ("published_language", contexts[2], contexts[1]), ("partnership", contexts[1], contexts[3]), ("big_ball_of_mud", contexts[4], contexts[3]), ] relationships = [] for rel_type, source, target in relationship_types: rel = MagicMock(spec=ContextRelationship) rel.source_context = source rel.target_context = target rel.relationship_type = rel_type rel.description = f"{rel_type} between {source.name} and {target.name}" relationships.append(rel) rel_result = MagicMock() rel_result.scalars.return_value.all.return_value = relationships monkeypatch.setattr( mock_db_session, "execute", AsyncMock(side_effect=[context_result, rel_result]), ) # Test JSON format result = await domain_tools.generate_context_map("json") assert len(result["contexts"]) == 5 assert len(result["relationships"]) == 8 # Verify all relationship types are present rel_types = {rel["type"] for rel in result["relationships"]} assert rel_types == { "shared_kernel", "customer_supplier", "conformist", "anti_corruption_layer", "open_host_service", "published_language", "partnership", "big_ball_of_mud", } @pytest.mark.asyncio async def test_generate_context_map_plantuml_format( self, domain_tools: DomainTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test generating context map in PlantUML format.""" # Create contexts ctx1 = MagicMock(spec=BoundedContext) ctx1.name = "CoreDomain" ctx1.context_type = "core" ctx2 = MagicMock(spec=BoundedContext) ctx2.name = "SupportingDomain" ctx2.context_type = "supporting" contexts = [ctx1, ctx2] context_result = MagicMock() context_result.scalars.return_value.all.return_value = contexts # Create relationship rel = MagicMock(spec=ContextRelationship) rel.source_context = ctx1 rel.target_context = ctx2 rel.relationship_type = "customer_supplier" rel_result = MagicMock() rel_result.scalars.return_value.all.return_value = [rel] monkeypatch.setattr( mock_db_session, "execute", AsyncMock(side_effect=[context_result, rel_result]), ) result = await domain_tools.generate_context_map("plantuml") assert "diagram" in result diagram = result["diagram"] # Check PlantUML structure assert "@startuml" in diagram assert "@enduml" in diagram assert 'package "CoreDomain" <<Core>>' in diagram assert 'package "SupportingDomain"' in diagram assert '"CoreDomain" --> : <<Customer/Supplier>> "SupportingDomain"' in diagram @pytest.mark.asyncio async def test_suggest_ddd_refactoring_context_boundary_violation( self, domain_tools: DomainTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test DDD refactoring suggestion for context boundary violations.""" # Mock file mock_file = MagicMock() mock_file.id = 1 file_result = MagicMock() file_result.scalar_one_or_none.return_value = mock_file # Mock entities from different contexts entities = [] for _i, (name, _ctx_name) in enumerate( [ ("Order", "OrderContext"), ("OrderItem", "OrderContext"), ("Customer", "CustomerContext"), ("Product", "ProductContext"), ] ): entity = MagicMock(spec=DomainEntity) entity.id = _i entity.name = name entity.entity_type = "entity" entity.business_rules = ["Rule"] entity.invariants = ["Invariant"] entity.responsibilities = ["Responsibility"] entities.append(entity) entity_result = MagicMock() entity_result.scalars.return_value.all.return_value = entities # Mock context memberships contexts = {} memberships = [] for _i, (entity, ctx_name) in enumerate( zip( entities, ["OrderContext", "OrderContext", "CustomerContext", "ProductContext"], strict=False, ) ): if ctx_name not in contexts: ctx = MagicMock(spec=BoundedContext) ctx.name = ctx_name contexts[ctx_name] = ctx membership = MagicMock(spec=BoundedContextMembership) membership.domain_entity_id = entity.id membership.bounded_context = contexts[ctx_name] memberships.append(membership) membership_result = MagicMock() membership_result.scalars.return_value.all.return_value = memberships monkeypatch.setattr( mock_db_session, "execute", AsyncMock( side_effect=[ file_result, entity_result, membership_result, ] ), ) result = await domain_tools.suggest_ddd_refactoring("mixed_contexts.py") # Should have context boundary violation boundary_violations = [ s for s in result if s["type"] == "context_boundary_violation" ] assert len(boundary_violations) == 1 assert boundary_violations[0]["severity"] == "high" assert len(boundary_violations[0]["contexts"]) == 3 assert set(boundary_violations[0]["contexts"]) == { "OrderContext", "CustomerContext", "ProductContext", } @pytest.mark.asyncio async def test_find_aggregate_roots_with_complex_aggregates( self, domain_tools: DomainTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test finding aggregate roots with complex member relationships.""" # Mock bounded context mock_context = MagicMock(spec=BoundedContext) mock_context.id = 1 mock_context.name = "ComplexContext" context_result = MagicMock() context_result.scalar_one_or_none.return_value = mock_context # Mock membership IDs membership_result = MagicMock() membership_result.__iter__ = MagicMock( return_value=iter([(1,), (2,), (3,), (4,), (5,)]) ) # Mock aggregate roots agg1 = MagicMock(spec=DomainEntity) agg1.id = 1 agg1.name = "CustomerAggregate" agg1.description = "Customer aggregate root" agg1.invariants = ["Email must be unique", "Age must be positive"] agg2 = MagicMock(spec=DomainEntity) agg2.id = 2 agg2.name = "OrderAggregate" agg2.description = "Order aggregate root" agg2.invariants = ["Total must equal sum of items"] agg_result = MagicMock() agg_result.scalars.return_value.all.return_value = [agg1, agg2] # Mock aggregate members # Customer aggregate members customer_members = [ ("Address", "value_object"), ("ContactInfo", "value_object"), ("CustomerProfile", "entity"), ] # Order aggregate members order_members = [ ("OrderItem", "entity"), ("ShippingInfo", "value_object"), ("PaymentInfo", "value_object"), ("Discount", "value_object"), ] def create_members(member_list: list[tuple[str, str]]) -> list[DomainEntity]: members: list[DomainEntity] = [] for name, entity_type in member_list: member = MagicMock(spec=DomainEntity) member.name = name member.entity_type = entity_type members.append(member) return members customer_member_result = MagicMock() customer_member_result.scalars.return_value.all.return_value = create_members( customer_members ) order_member_result = MagicMock() order_member_result.scalars.return_value.all.return_value = create_members( order_members ) # Mock file paths file_result1 = MagicMock() file_result1.__iter__ = MagicMock( return_value=iter([("/src/domain/customer.py",)]) ) file_result2 = MagicMock() file_result2.__iter__ = MagicMock( return_value=iter( [("/src/domain/order.py",), ("/src/domain/order_item.py",)] ) ) monkeypatch.setattr( mock_db_session, "execute", AsyncMock( side_effect=[ context_result, membership_result, agg_result, customer_member_result, file_result1, order_member_result, file_result2, ] ), ) result = await domain_tools.find_aggregate_roots("ComplexContext") assert len(result) == 2 # Check Customer aggregate customer_agg = next(r for r in result if r["name"] == "CustomerAggregate") assert len(customer_agg["members"]) == 3 assert len(customer_agg["invariants"]) == 2 member_types = {m["type"] for m in customer_agg["members"]} assert member_types == {"value_object", "entity"} # Check Order aggregate order_agg = next(r for r in result if r["name"] == "OrderAggregate") assert len(order_agg["members"]) == 4 assert len(order_agg["source_files"]) == 2 @pytest.mark.asyncio async def test_generate_context_map_empty_relationships( self, domain_tools: DomainTools, mock_db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test generating context map with no relationships.""" # Create isolated contexts contexts = [] for i, name in enumerate( ["IsolatedContext1", "IsolatedContext2", "IsolatedContext3"] ): ctx = MagicMock(spec=BoundedContext) ctx.id = i ctx.name = name ctx.context_type = "supporting" ctx.description = f"{name} with no relationships" contexts.append(ctx) context_result = MagicMock() context_result.scalars.return_value.all.return_value = contexts # No relationships rel_result = MagicMock() rel_result.scalars.return_value.all.return_value = [] monkeypatch.setattr( mock_db_session, "execute", AsyncMock(side_effect=[context_result, rel_result]), ) # Test Mermaid format with isolated contexts result = await domain_tools.generate_context_map("mermaid") assert "diagram" in result diagram = result["diagram"] # Should have all contexts but no arrows assert "IsolatedContext1[IsolatedContext1]" in diagram assert "IsolatedContext2[IsolatedContext2]" in diagram assert "IsolatedContext3[IsolatedContext3]" in diagram assert "-->" not in diagram # No relationships

Latest Blog Posts

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/johannhartmann/mcpcodeanalysis'

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