File size: 8,468 Bytes
eb0499d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
"""Integration tests for the complete Federated Learning simulation orchestrator."""

from __future__ import annotations

import logging
from typing import Any

import pytest

from app.application.services.data_generator import DataGenerator
from app.application.services.fl_engine import FederatedLearningEngine
from app.application.services.metrics_service import MetricsService
from app.application.services.model_service import ModelService
from app.application.services.privacy_service import PrivacyService
from app.application.services.simulation_service import SimulationService
from app.config import get_settings
from app.domain.enums import ClientStatus, SimulationStatus
from app.domain.value_objects import SimulationConfig

logger = logging.getLogger(__name__)


@pytest.fixture
def simulation_service() -> SimulationService:
    """Fixture to build the orchestrator service."""
    settings = get_settings()
    model_service = ModelService(settings)
    privacy_service = PrivacyService()
    fl_engine = FederatedLearningEngine(settings, model_service, privacy_service)
    data_generator = DataGenerator()
    metrics_service = MetricsService()

    return SimulationService(
        settings=settings,
        simulation_repo=None,
        bank_repo=None,
        metrics_repo=None,
        data_generator=data_generator,
        fl_engine=fl_engine,
        metrics_service=metrics_service,
        model_service=model_service,
    )


def test_simulation_flow_success(simulation_service: SimulationService, sample_config: dict):
    """Test that a standard simulation completes successfully and fires all expected progress events."""
    # Scale down sizes for faster test runs
    sample_config["bank_a_transactions"] = 1000
    sample_config["bank_b_transactions"] = 1000
    sample_config["bank_c_transactions"] = 1000
    sample_config["num_rounds"] = 2
    sample_config["local_epochs"] = 1

    config = SimulationConfig(**sample_config)
    events = []

    def progress_callback(sim_id: str, event_type: str, data: dict[str, Any]) -> None:
        events.append((event_type, data))

    simulation = simulation_service.run_simulation(config, progress_callback=progress_callback)

    # 1. Verify final status and attributes
    assert simulation.status == SimulationStatus.COMPLETED
    assert len(simulation.banks) == 3
    assert simulation.current_round == 2
    assert simulation.total_rounds == 2
    assert simulation.duration_seconds is not None
    assert simulation.duration_seconds > 0
    assert simulation.error_message is None

    # 2. Check generated bank metrics
    for bank in simulation.banks:
        assert bank.local_metrics is not None
        assert bank.federated_metrics is not None
        assert bank.improvement is not None
        assert "f1_score" in bank.improvement
        assert "auc_roc" in bank.improvement

    # 3. Check progress events order
    event_types = [e[0] for e in events]
    assert "status" in event_types
    assert "local_training" in event_types
    assert "round_start" in event_types
    assert "round_complete" in event_types
    assert "completed" in event_types


def test_simulation_with_differential_privacy(
    simulation_service: SimulationService, sample_config: dict
):
    """Test FL simulation with Differential Privacy enabled."""
    sample_config["enable_differential_privacy"] = True
    sample_config.pop("privacy_mechanism", None)
    sample_config["dp_epsilon"] = 0.5
    sample_config["dp_delta"] = 1e-5
    sample_config["num_rounds"] = 2
    sample_config["local_epochs"] = 1

    config = SimulationConfig(**sample_config)
    simulation = simulation_service.run_simulation(config)

    assert simulation.status == SimulationStatus.COMPLETED
    # Ensure DP is applied (privacy budget must be tracked)
    assert simulation.current_round == 2


def test_simulation_with_secure_aggregation(
    simulation_service: SimulationService, sample_config: dict
):
    """Test FL simulation with Secure Aggregation enabled."""
    sample_config["enable_secure_aggregation"] = True
    sample_config.pop("privacy_mechanism", None)
    sample_config["num_rounds"] = 2
    sample_config["local_epochs"] = 1

    config = SimulationConfig(**sample_config)
    simulation = simulation_service.run_simulation(config)

    assert simulation.status == SimulationStatus.COMPLETED


def test_simulation_with_client_dropouts(
    simulation_service: SimulationService, sample_config: dict
):
    """Test FL simulation with simulated dropout rates and client failures."""
    sample_config["enable_dropout_simulation"] = True
    sample_config["dropout_probability"] = 0.8  # High probability to force dropouts
    sample_config["num_rounds"] = 3
    sample_config["local_epochs"] = 1

    config = SimulationConfig(**sample_config)
    simulation = simulation_service.run_simulation(config)

    assert simulation.status == SimulationStatus.COMPLETED
    # Some banks might have been inactive/dropped
    statuses = [bank.status for bank in simulation.banks]
    assert any(
        status in (ClientStatus.DROPPED, ClientStatus.ACTIVE, ClientStatus.RECONNECTED)
        for status in statuses
    )


def test_simulation_with_graph_embedding(
    simulation_service: SimulationService, sample_config: dict
):
    """Test FL simulation with Federated Graph Embedding (FedGNN) enabled."""
    # Populate the graph engine with mock nodes and edges to train on
    from app.application.services.graph_engine import GraphEngine
    from app.domain.enums import EntityType, RelationshipType, RiskLevel
    from app.domain.investigation_entities import Entity, Relationship

    ge = GraphEngine()

    # Clean previous test residues
    ge._entities.clear()
    ge._relationships.clear()

    # Register nodes for each bank
    banks = ["bank_a", "bank_b", "bank_c"]
    for i, b_id in enumerate(banks):
        e1 = Entity(
            id=f"cust_{b_id}_1",
            entity_type=EntityType.CUSTOMER,
            bank_id=b_id,
            risk_level=RiskLevel.MINIMAL,
        )
        e2 = Entity(
            id=f"dev_{b_id}_2",
            entity_type=EntityType.DEVICE,
            bank_id=b_id,
            risk_level=RiskLevel.HIGH,
        )
        e3 = Entity(
            id=f"cust_{b_id}_3",
            entity_type=EntityType.CUSTOMER,
            bank_id=b_id,
            risk_level=RiskLevel.CRITICAL,
        )

        ge.register_entity(e1)
        ge.register_entity(e2)
        ge.register_entity(e3)

        # Connect them
        ge.add_relationship(
            Relationship(
                id=f"rel_{b_id}_1",
                source_entity_id=e1.id,
                target_entity_id=e2.id,
                relationship_type=RelationshipType.USES,
            )
        )
        ge.add_relationship(
            Relationship(
                id=f"rel_{b_id}_2",
                source_entity_id=e3.id,
                target_entity_id=e2.id,
                relationship_type=RelationshipType.USES,
            )
        )

    sample_config["enable_graph_embedding"] = True
    sample_config["gnn_embedding_dim"] = 16
    sample_config["gnn_hidden_dim"] = 32
    sample_config["gnn_num_layers"] = 2
    sample_config["gnn_epochs_per_round"] = 1
    sample_config["num_rounds"] = 2
    sample_config["local_epochs"] = 1

    config = SimulationConfig(**sample_config)
    events = []

    def progress_callback(sim_id: str, event_type: str, data: dict[str, Any]) -> None:
        events.append((event_type, data))

    try:
        simulation = simulation_service.run_simulation(config, progress_callback=progress_callback)

        assert simulation.status == SimulationStatus.COMPLETED

        event_types = [e[0] for e in events]
        assert "gnn_round_start" in event_types
        assert "gnn_round_complete" in event_types

        # Ensure embeddings are synchronized and accessible via stats
        from app.presentation.routers import graph

        stats = graph._graph_embedding_service.get_embedding_stats()
        assert stats["num_embedded_nodes"] == 9
        assert stats["embedding_dim"] == 16
        assert stats["model_parameters"] > 0
    finally:
        from app.application.services.graph_embedding_service import GraphEmbeddingService
        from app.presentation.routers import graph

        graph._graph_embedding_service = GraphEmbeddingService(
            graph_engine=graph._graph_engine, embedding_dim=64
        )