auth_framework/migration/
executors.rs

1//! Migration execution engine
2//!
3//! This module provides the execution engine for migration plans,
4//! including progress tracking, error handling, and rollback capabilities.
5
6use super::{
7    MigrationConfig, MigrationError, MigrationMetrics, MigrationOperation, MigrationPlan,
8    MigrationResult, MigrationStatus,
9};
10use std::collections::HashMap;
11use tokio::fs;
12use uuid::Uuid;
13
14/// Execute migration plan
15pub async fn execute_migration_plan(
16    plan: &MigrationPlan,
17    config: &MigrationConfig,
18) -> Result<MigrationResult, MigrationError> {
19    let _execution_id = Uuid::new_v4().to_string();
20    let started_at = chrono::Utc::now();
21
22    let mut result = MigrationResult {
23        plan_id: plan.id.clone(),
24        status: MigrationStatus::InProgress,
25        started_at,
26        completed_at: None,
27        phases_completed: Vec::new(),
28        current_phase: None,
29        errors: Vec::new(),
30        warnings: Vec::new(),
31        metrics: MigrationMetrics {
32            roles_migrated: 0,
33            permissions_migrated: 0,
34            users_migrated: 0,
35            errors_encountered: 0,
36            warnings_generated: 0,
37            validation_failures: 0,
38            rollback_count: 0,
39        },
40    };
41
42    // Save initial status
43    save_migration_status(&result, config).await?;
44
45    if config.dry_run {
46        log_message(config, "DRY RUN MODE - No actual changes will be made");
47        return execute_dry_run(plan, config, result).await;
48    }
49
50    // Execute pre-validation steps
51    if let Err(e) = execute_pre_validation(plan, config, &mut result).await {
52        result.status = MigrationStatus::Failed;
53        result.errors.push(format!("Pre-validation failed: {}", e));
54        save_migration_status(&result, config).await?;
55        return Ok(result);
56    }
57
58    // Execute migration phases
59    for phase in &plan.phases {
60        result.current_phase = Some(phase.id.clone());
61        save_migration_status(&result, config).await?;
62
63        log_message(
64            config,
65            &format!("Executing phase: {} - {}", phase.id, phase.name),
66        );
67
68        match execute_phase(phase, config, &mut result).await {
69            Ok(_) => {
70                result.phases_completed.push(phase.id.clone());
71                log_message(
72                    config,
73                    &format!("Phase '{}' completed successfully", phase.id),
74                );
75            }
76            Err(e) => {
77                result.status = MigrationStatus::Failed;
78                result
79                    .errors
80                    .push(format!("Phase '{}' failed: {}", phase.id, e));
81                result.metrics.errors_encountered += 1;
82
83                log_message(config, &format!("Phase '{}' failed: {}", phase.id, e));
84
85                // Attempt automatic rollback
86                if let Err(rollback_error) =
87                    execute_rollback_for_phase(phase, config, &mut result).await
88                {
89                    result.errors.push(format!(
90                        "Rollback for phase '{}' failed: {}",
91                        phase.id, rollback_error
92                    ));
93                }
94
95                save_migration_status(&result, config).await?;
96                return Ok(result);
97            }
98        }
99    }
100
101    // Execute post-validation steps
102    if let Err(e) = execute_post_validation(plan, config, &mut result).await {
103        result.status = MigrationStatus::Failed;
104        result.errors.push(format!("Post-validation failed: {}", e));
105        save_migration_status(&result, config).await?;
106        return Ok(result);
107    }
108
109    // Migration completed successfully
110    result.status = MigrationStatus::Completed;
111    result.completed_at = Some(chrono::Utc::now());
112    result.current_phase = None;
113
114    log_message(config, "Migration completed successfully");
115    save_migration_status(&result, config).await?;
116
117    Ok(result)
118}
119
120/// Execute migration plan in dry-run mode
121async fn execute_dry_run(
122    plan: &MigrationPlan,
123    config: &MigrationConfig,
124    mut result: MigrationResult,
125) -> Result<MigrationResult, MigrationError> {
126    log_message(config, "=== DRY RUN EXECUTION ===");
127
128    for phase in &plan.phases {
129        log_message(
130            config,
131            &format!("DRY RUN - Phase: {} - {}", phase.id, phase.name),
132        );
133
134        for operation in &phase.operations {
135            match operation {
136                MigrationOperation::CreateRole { role_id, name, .. } => {
137                    log_message(
138                        config,
139                        &format!("  [DRY RUN] Would create role: {} ({})", role_id, name),
140                    );
141                    result.metrics.roles_migrated += 1;
142                }
143                MigrationOperation::CreatePermission {
144                    permission_id,
145                    action,
146                    resource,
147                    ..
148                } => {
149                    log_message(
150                        config,
151                        &format!(
152                            "  [DRY RUN] Would create permission: {} ({}:{})",
153                            permission_id, action, resource
154                        ),
155                    );
156                    result.metrics.permissions_migrated += 1;
157                }
158                MigrationOperation::AssignUserRole {
159                    user_id, role_id, ..
160                } => {
161                    log_message(
162                        config,
163                        &format!(
164                            "  [DRY RUN] Would assign role {} to user {}",
165                            role_id, user_id
166                        ),
167                    );
168                    result.metrics.users_migrated += 1;
169                }
170                MigrationOperation::Backup {
171                    backup_location,
172                    backup_type,
173                } => {
174                    log_message(
175                        config,
176                        &format!(
177                            "  [DRY RUN] Would create {:?} backup at {:?}",
178                            backup_type, backup_location
179                        ),
180                    );
181                }
182                MigrationOperation::ValidateIntegrity {
183                    validation_type, ..
184                } => {
185                    log_message(
186                        config,
187                        &format!("  [DRY RUN] Would validate: {}", validation_type),
188                    );
189                }
190                MigrationOperation::MigrateCustomAttribute { attribute_name, .. } => {
191                    log_message(
192                        config,
193                        &format!(
194                            "  [DRY RUN] Would migrate custom attribute: {}",
195                            attribute_name
196                        ),
197                    );
198                }
199            }
200        }
201
202        result.phases_completed.push(phase.id.clone());
203    }
204
205    result.status = MigrationStatus::Completed;
206    result.completed_at = Some(chrono::Utc::now());
207
208    log_message(config, "=== DRY RUN COMPLETED ===");
209
210    Ok(result)
211}
212
213/// Execute pre-validation steps
214async fn execute_pre_validation(
215    plan: &MigrationPlan,
216    config: &MigrationConfig,
217    result: &mut MigrationResult,
218) -> Result<(), MigrationError> {
219    log_message(config, "Executing pre-validation steps");
220
221    for step in &plan.pre_validation_steps {
222        log_message(
223            config,
224            &format!("Pre-validation: {} - {}", step.id, step.name),
225        );
226
227        match execute_validation_step(step, config).await {
228            Ok(_) => {
229                log_message(config, &format!("Pre-validation '{}' passed", step.id));
230            }
231            Err(e) => {
232                if step.required {
233                    return Err(MigrationError::ValidationError(format!(
234                        "Required pre-validation '{}' failed: {}",
235                        step.id, e
236                    )));
237                } else {
238                    result.warnings.push(format!(
239                        "Optional pre-validation '{}' failed: {}",
240                        step.id, e
241                    ));
242                    result.metrics.warnings_generated += 1;
243                }
244            }
245        }
246    }
247
248    Ok(())
249}
250
251/// Execute post-validation steps
252async fn execute_post_validation(
253    plan: &MigrationPlan,
254    config: &MigrationConfig,
255    result: &mut MigrationResult,
256) -> Result<(), MigrationError> {
257    log_message(config, "Executing post-validation steps");
258
259    for step in &plan.post_validation_steps {
260        log_message(
261            config,
262            &format!("Post-validation: {} - {}", step.id, step.name),
263        );
264
265        match execute_validation_step(step, config).await {
266            Ok(_) => {
267                log_message(config, &format!("Post-validation '{}' passed", step.id));
268            }
269            Err(e) => {
270                if step.required {
271                    result.metrics.validation_failures += 1;
272                    return Err(MigrationError::ValidationError(format!(
273                        "Required post-validation '{}' failed: {}",
274                        step.id, e
275                    )));
276                } else {
277                    result.warnings.push(format!(
278                        "Optional post-validation '{}' failed: {}",
279                        step.id, e
280                    ));
281                    result.metrics.warnings_generated += 1;
282                }
283            }
284        }
285    }
286
287    Ok(())
288}
289
290/// Execute individual validation step
291async fn execute_validation_step(
292    step: &super::ValidationStep,
293    config: &MigrationConfig,
294) -> Result<(), MigrationError> {
295    use super::ValidationType;
296
297    match &step.validation_type {
298        ValidationType::HierarchyIntegrity => validate_hierarchy_integrity(config).await,
299        ValidationType::PermissionConsistency => validate_permission_consistency(config).await,
300        ValidationType::UserAssignmentValidity => validate_user_assignments(config).await,
301        ValidationType::PrivilegeEscalationCheck => validate_no_privilege_escalation(config).await,
302        ValidationType::Custom(validation_name) => {
303            execute_custom_validation(validation_name, &step.parameters, config).await
304        }
305    }
306}
307
308/// Execute migration phase
309async fn execute_phase(
310    phase: &super::MigrationPhase,
311    config: &MigrationConfig,
312    result: &mut MigrationResult,
313) -> Result<(), MigrationError> {
314    for operation in &phase.operations {
315        if let Err(e) = execute_operation(operation, config, result).await {
316            return Err(MigrationError::ExecutionError(format!(
317                "Operation failed in phase '{}': {}",
318                phase.id, e
319            )));
320        }
321    }
322    Ok(())
323}
324
325/// Execute individual migration operation
326async fn execute_operation(
327    operation: &MigrationOperation,
328    config: &MigrationConfig,
329    result: &mut MigrationResult,
330) -> Result<(), MigrationError> {
331    match operation {
332        MigrationOperation::CreateRole {
333            role_id,
334            name,
335            description,
336            permissions,
337            parent_role,
338        } => {
339            execute_create_role(
340                role_id,
341                name,
342                description.as_deref(),
343                permissions,
344                parent_role.as_deref(),
345                config,
346            )
347            .await?;
348            result.metrics.roles_migrated += 1;
349        }
350        MigrationOperation::CreatePermission {
351            permission_id,
352            action,
353            resource,
354            conditions,
355        } => {
356            execute_create_permission(permission_id, action, resource, conditions, config).await?;
357            result.metrics.permissions_migrated += 1;
358        }
359        MigrationOperation::AssignUserRole {
360            user_id,
361            role_id,
362            expiration,
363        } => {
364            execute_assign_user_role(user_id, role_id, expiration.as_ref(), config).await?;
365            result.metrics.users_migrated += 1;
366        }
367        MigrationOperation::Backup {
368            backup_location,
369            backup_type,
370        } => {
371            execute_backup(backup_location, backup_type, config).await?;
372        }
373        MigrationOperation::ValidateIntegrity {
374            validation_type,
375            parameters,
376        } => {
377            execute_integrity_validation(validation_type, parameters, config).await?;
378        }
379        MigrationOperation::MigrateCustomAttribute {
380            attribute_name,
381            conversion_logic,
382        } => {
383            execute_custom_attribute_migration(attribute_name, conversion_logic, config).await?;
384        }
385    }
386
387    Ok(())
388}
389
390/// Execute role creation
391async fn execute_create_role(
392    role_id: &str,
393    name: &str,
394    description: Option<&str>,
395    permissions: &[String],
396    parent_role: Option<&str>,
397    config: &MigrationConfig,
398) -> Result<(), MigrationError> {
399    log_message(config, &format!("Creating role: {} ({})", role_id, name));
400
401    // In a real implementation, this would integrate with the role-system v1.0 API
402    // For now, we'll simulate the operation
403
404    if config.verbose {
405        log_message(config, &format!("  Description: {:?}", description));
406        log_message(config, &format!("  Permissions: {:?}", permissions));
407        log_message(config, &format!("  Parent role: {:?}", parent_role));
408    }
409
410    // Simulate API call delay
411    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
412
413    // Here you would integrate with the actual role-system v1.0 AsyncRoleSystem
414    // Example:
415    // let role_system = get_role_system(config).await?;
416    // role_system.create_role(role_id, name, description, permissions, parent_role).await?;
417
418    Ok(())
419}
420
421/// Execute permission creation
422async fn execute_create_permission(
423    permission_id: &str,
424    action: &str,
425    resource: &str,
426    conditions: &HashMap<String, String>,
427    config: &MigrationConfig,
428) -> Result<(), MigrationError> {
429    log_message(
430        config,
431        &format!(
432            "Creating permission: {} ({}:{})",
433            permission_id, action, resource
434        ),
435    );
436
437    if config.verbose {
438        log_message(config, &format!("  Conditions: {:?}", conditions));
439    }
440
441    // Simulate API call delay
442    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
443
444    // Here you would integrate with the actual role-system v1.0 AsyncRoleSystem
445    // Example:
446    // let role_system = get_role_system(config).await?;
447    // role_system.create_permission(permission_id, action, resource, conditions).await?;
448
449    Ok(())
450}
451
452/// Execute user role assignment
453async fn execute_assign_user_role(
454    user_id: &str,
455    role_id: &str,
456    expiration: Option<&chrono::DateTime<chrono::Utc>>,
457    config: &MigrationConfig,
458) -> Result<(), MigrationError> {
459    log_message(
460        config,
461        &format!("Assigning role {} to user {}", role_id, user_id),
462    );
463
464    if config.verbose {
465        log_message(config, &format!("  Expiration: {:?}", expiration));
466    }
467
468    // Simulate API call delay
469    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
470
471    // Here you would integrate with the actual role-system v1.0 AsyncRoleSystem
472    // Example:
473    // let role_system = get_role_system(config).await?;
474    // role_system.assign_user_role(user_id, role_id, expiration).await?;
475
476    Ok(())
477}
478
479/// Execute backup operation
480async fn execute_backup(
481    backup_location: &std::path::Path,
482    backup_type: &super::BackupType,
483    config: &MigrationConfig,
484) -> Result<(), MigrationError> {
485    log_message(
486        config,
487        &format!("Creating {:?} backup at {:?}", backup_type, backup_location),
488    );
489
490    // Ensure backup directory exists
491    if let Some(parent) = backup_location.parent() {
492        fs::create_dir_all(parent).await?;
493    }
494
495    // Create backup (simplified implementation)
496    let backup_data = match backup_type {
497        super::BackupType::Full => create_full_backup(config).await?,
498        super::BackupType::Incremental => create_incremental_backup(config).await?,
499        super::BackupType::ConfigOnly => create_config_backup(config).await?,
500        super::BackupType::DataOnly => create_data_backup(config).await?,
501    };
502
503    fs::write(backup_location, backup_data).await?;
504
505    log_message(
506        config,
507        &format!("Backup created successfully at {:?}", backup_location),
508    );
509
510    Ok(())
511}
512
513/// Execute integrity validation
514async fn execute_integrity_validation(
515    validation_type: &str,
516    parameters: &HashMap<String, String>,
517    config: &MigrationConfig,
518) -> Result<(), MigrationError> {
519    log_message(
520        config,
521        &format!("Executing integrity validation: {}", validation_type),
522    );
523
524    if config.verbose {
525        log_message(config, &format!("  Parameters: {:?}", parameters));
526    }
527
528    // Simulate validation
529    tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
530
531    match validation_type {
532        "pre_migration_check" => validate_pre_migration_state(config).await,
533        "post_migration_check" => validate_post_migration_state(config).await,
534        "stop_migration" => Ok(()), // No-op for stop migration
535        _ => {
536            log_message(
537                config,
538                &format!("Unknown validation type: {}", validation_type),
539            );
540            Ok(())
541        }
542    }
543}
544
545/// Execute custom attribute migration
546async fn execute_custom_attribute_migration(
547    attribute_name: &str,
548    conversion_logic: &str,
549    config: &MigrationConfig,
550) -> Result<(), MigrationError> {
551    log_message(
552        config,
553        &format!("Migrating custom attribute: {}", attribute_name),
554    );
555
556    if config.verbose {
557        log_message(config, &format!("  Conversion logic: {}", conversion_logic));
558    }
559
560    // Simulate custom attribute migration
561    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
562
563    // Here you would implement the actual custom attribute migration logic
564    // based on the conversion_logic parameter
565
566    Ok(())
567}
568
569/// Execute rollback for a specific phase
570async fn execute_rollback_for_phase(
571    phase: &super::MigrationPhase,
572    config: &MigrationConfig,
573    result: &mut MigrationResult,
574) -> Result<(), MigrationError> {
575    log_message(
576        config,
577        &format!("Executing rollback for phase: {}", phase.id),
578    );
579
580    for operation in &phase.rollback_operations {
581        if let Err(e) = execute_operation(operation, config, result).await {
582            return Err(MigrationError::RollbackError(format!(
583                "Rollback operation failed: {}",
584                e
585            )));
586        }
587    }
588
589    result.metrics.rollback_count += 1;
590    Ok(())
591}
592
593/// Execute complete migration rollback
594pub async fn rollback_migration(
595    plan: &MigrationPlan,
596    config: &MigrationConfig,
597) -> Result<MigrationResult, MigrationError> {
598    let started_at = chrono::Utc::now();
599
600    let mut result = MigrationResult {
601        plan_id: plan.id.clone(),
602        status: MigrationStatus::InProgress,
603        started_at,
604        completed_at: None,
605        phases_completed: Vec::new(),
606        current_phase: Some("rollback".to_string()),
607        errors: Vec::new(),
608        warnings: Vec::new(),
609        metrics: MigrationMetrics {
610            roles_migrated: 0,
611            permissions_migrated: 0,
612            users_migrated: 0,
613            errors_encountered: 0,
614            warnings_generated: 0,
615            validation_failures: 0,
616            rollback_count: 0,
617        },
618    };
619
620    log_message(config, "Starting migration rollback");
621
622    // Execute rollback phases in reverse order
623    for phase in plan.rollback_plan.phases.iter().rev() {
624        log_message(config, &format!("Executing rollback phase: {}", phase.id));
625
626        for operation in &phase.operations {
627            if let Err(e) = execute_operation(operation, config, &mut result).await {
628                result.status = MigrationStatus::Failed;
629                result
630                    .errors
631                    .push(format!("Rollback operation failed: {}", e));
632                save_migration_status(&result, config).await?;
633                return Ok(result);
634            }
635        }
636
637        result.phases_completed.push(phase.id.clone());
638    }
639
640    result.status = MigrationStatus::RolledBack;
641    result.completed_at = Some(chrono::Utc::now());
642    result.current_phase = None;
643
644    log_message(config, "Migration rollback completed");
645    save_migration_status(&result, config).await?;
646
647    Ok(result)
648}
649
650/// Validation implementations
651async fn validate_hierarchy_integrity(_config: &MigrationConfig) -> Result<(), MigrationError> {
652    // Simulate hierarchy validation
653    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
654    Ok(())
655}
656
657async fn validate_permission_consistency(_config: &MigrationConfig) -> Result<(), MigrationError> {
658    // Simulate permission validation
659    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
660    Ok(())
661}
662
663async fn validate_user_assignments(_config: &MigrationConfig) -> Result<(), MigrationError> {
664    // Simulate user assignment validation
665    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
666    Ok(())
667}
668
669async fn validate_no_privilege_escalation(_config: &MigrationConfig) -> Result<(), MigrationError> {
670    // Simulate privilege escalation check
671    tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
672    Ok(())
673}
674
675async fn execute_custom_validation(
676    validation_name: &str,
677    _parameters: &HashMap<String, String>,
678    config: &MigrationConfig,
679) -> Result<(), MigrationError> {
680    log_message(
681        config,
682        &format!("Executing custom validation: {}", validation_name),
683    );
684    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
685    Ok(())
686}
687
688async fn validate_pre_migration_state(_config: &MigrationConfig) -> Result<(), MigrationError> {
689    // Simulate pre-migration state validation
690    tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
691    Ok(())
692}
693
694async fn validate_post_migration_state(_config: &MigrationConfig) -> Result<(), MigrationError> {
695    // Simulate post-migration state validation
696    tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
697    Ok(())
698}
699
700/// Backup implementations
701async fn create_full_backup(_config: &MigrationConfig) -> Result<String, MigrationError> {
702    Ok("FULL_BACKUP_DATA".to_string())
703}
704
705async fn create_incremental_backup(_config: &MigrationConfig) -> Result<String, MigrationError> {
706    Ok("INCREMENTAL_BACKUP_DATA".to_string())
707}
708
709async fn create_config_backup(_config: &MigrationConfig) -> Result<String, MigrationError> {
710    Ok("CONFIG_BACKUP_DATA".to_string())
711}
712
713async fn create_data_backup(_config: &MigrationConfig) -> Result<String, MigrationError> {
714    Ok("DATA_BACKUP_DATA".to_string())
715}
716
717/// Save migration status to disk
718async fn save_migration_status(
719    result: &MigrationResult,
720    config: &MigrationConfig,
721) -> Result<(), MigrationError> {
722    let status_file = config
723        .working_directory
724        .join(format!("{}_status.json", result.plan_id));
725    let content = serde_json::to_string_pretty(result)?;
726    fs::write(status_file, content).await?;
727    Ok(())
728}
729
730/// Log message with timestamp
731fn log_message(config: &MigrationConfig, message: &str) {
732    if config.verbose {
733        let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S");
734        println!("[{}] {}", timestamp, message);
735    }
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741    use crate::migration::{
742        LegacySystemAnalysis, LegacySystemType, MigrationPhase, MigrationStrategy, RiskLevel,
743    };
744
745    fn create_test_plan() -> MigrationPlan {
746        MigrationPlan {
747            id: "test_plan".to_string(),
748            source_analysis: LegacySystemAnalysis {
749                system_type: LegacySystemType::BasicRbac,
750                role_count: 1,
751                permission_count: 1,
752                user_assignment_count: 1,
753                roles: vec![],
754                permissions: vec![],
755                user_assignments: vec![],
756                hierarchy_depth: 0,
757                duplicates_found: false,
758                orphaned_permissions: vec![],
759                circular_dependencies: vec![],
760                custom_attributes: std::collections::HashSet::new(),
761                complexity_score: 3,
762                recommended_strategy: MigrationStrategy::DirectMapping,
763            },
764            strategy: MigrationStrategy::DirectMapping,
765            phases: vec![MigrationPhase {
766                id: "test_phase".to_string(),
767                name: "Test Phase".to_string(),
768                description: "Test phase".to_string(),
769                order: 1,
770                operations: vec![MigrationOperation::CreateRole {
771                    role_id: "test_role".to_string(),
772                    name: "Test Role".to_string(),
773                    description: None,
774                    permissions: vec!["read".to_string()],
775                    parent_role: None,
776                }],
777                dependencies: vec![],
778                estimated_duration: chrono::Duration::minutes(1),
779                rollback_operations: vec![],
780            }],
781            role_mappings: std::collections::HashMap::new(),
782            permission_mappings: std::collections::HashMap::new(),
783            user_migrations: vec![],
784            pre_validation_steps: vec![],
785            post_validation_steps: vec![],
786            rollback_plan: super::super::RollbackPlan {
787                phases: vec![],
788                backup_locations: vec![],
789                recovery_time_objective: chrono::Duration::hours(1),
790                manual_steps: vec![],
791            },
792            estimated_duration: chrono::Duration::minutes(30),
793            risk_level: RiskLevel::Low,
794            downtime_required: None,
795        }
796    }
797
798    #[tokio::test]
799    async fn test_execute_migration_plan_dry_run() {
800        let plan = create_test_plan();
801        let config = MigrationConfig {
802            dry_run: true,
803            verbose: false, // Reduce test output
804            ..Default::default()
805        };
806
807        let result = execute_migration_plan(&plan, &config).await.unwrap();
808
809        assert_eq!(result.status, MigrationStatus::Completed);
810        assert_eq!(result.phases_completed.len(), 1);
811        assert_eq!(result.metrics.roles_migrated, 1);
812    }
813
814    #[tokio::test]
815    async fn test_execute_migration_plan_real() {
816        let plan = create_test_plan();
817        let config = MigrationConfig {
818            dry_run: false,
819            verbose: false, // Reduce test output
820            ..Default::default()
821        };
822
823        let result = execute_migration_plan(&plan, &config).await.unwrap();
824
825        assert_eq!(result.status, MigrationStatus::Completed);
826        assert_eq!(result.phases_completed.len(), 1);
827        assert_eq!(result.metrics.roles_migrated, 1);
828    }
829}
830
831