//! FTP command recovery against an in-memory protocol transport. Live QA //! separately exercises the compiled WASM component over real host sockets. use super::{Ftp, stage_path_with, start_writer, test_host}; use memona_plugin_sdk::{ErrorCode, Guest, GuestWriter, Locator, Setting}; type TestResult = Result>; fn guest(value: Result) -> TestResult { value.map_err(|error| format!("guest operation failed: {error:?}").into()) } fn config() -> Vec<(String, Setting)> { vec![ ("host".into(), Setting::Text("127.0.0.1".into())), ( "port".into(), Setting::Text(test_host::CONTROL_PORT.to_string()), ), ("username".into(), Setting::Text("fixture".into())), ( "password".into(), Setting::Secret(" fixture secret ".into()), ), ] } #[tokio::test] async fn a_listing_reconnects_after_the_server_expires_the_control_session() -> TestResult { let fixture = test_host::install(test_host::Scenario::ExpiredListing); guest(Ftp::open("ftp".into(), config()))?; let first = guest(Ftp::list(Locator::Path("/".into()), None, 10))?; assert_eq!(first.entries[0].name, " report.md "); let next = guest(Ftp::list(Locator::Path("/".into()), None, 10))?; assert_eq!(next.entries[0].name, " report.md "); Ftp::close(); let summary = fixture.summary(); assert_eq!(summary.control_connections, 2); assert_eq!(summary.listings, 2); assert_eq!(summary.mkdir_commands, 0); Ok(()) } #[tokio::test] async fn recovery_before_a_mutation_never_replays_a_lost_mutation_reply() -> TestResult { let fixture = test_host::install(test_host::Scenario::LostMutationReply); guest(Ftp::open("ftp".into(), config()))?; assert!(matches!( Ftp::mkdir(Locator::Path("/new".into())), Err(ErrorCode::OutcomeUnknown) )); Ftp::close(); let summary = fixture.summary(); assert_eq!(summary.control_connections, 2); assert_eq!(summary.listings, 0); assert_eq!(summary.mkdir_commands, 1); Ok(()) } #[tokio::test] async fn cancelled_preflight_never_reconnects_or_executes_the_mutation() -> TestResult { let fixture = test_host::install(test_host::Scenario::CancelledPreflight); guest(Ftp::open("ftp".into(), config()))?; assert!(matches!( Ftp::mkdir(Locator::Path("/cancelled".into())), Err(ErrorCode::Cancelled) )); Ftp::close(); let summary = fixture.summary(); assert_eq!(summary.control_connections, 1); assert_eq!(summary.listings, 0); assert_eq!(summary.mkdir_commands, 0); Ok(()) } #[tokio::test] async fn a_lost_upload_completion_reports_an_unknown_outcome() -> TestResult { let fixture = test_host::install(test_host::Scenario::LostUploadCompletion); guest(Ftp::open("ftp".into(), config()))?; let writer = guest(start_writer("/target".into()))?; guest(writer.push(b"body".to_vec()))?; assert!(matches!(writer.finish(), Err(ErrorCode::OutcomeUnknown))); drop(writer); Ftp::close(); let summary = fixture.summary(); assert_eq!(summary.control_connections, 2); assert_eq!(summary.upload_commands, 1); assert_eq!(summary.target_upload_commands, 0); assert_eq!(summary.uploaded_bytes, 4); assert_eq!(summary.stage_deletes, 1); assert_eq!(summary.promotions, 0); Ok(()) } #[tokio::test] async fn an_uncertain_upload_write_discards_the_control_session() -> TestResult { let fixture = test_host::install(test_host::Scenario::LostUploadData); guest(Ftp::open("ftp".into(), config()))?; let writer = guest(start_writer("/target".into()))?; assert!(matches!( writer.push(b"body".to_vec()), Err(ErrorCode::OutcomeUnknown) )); let page = guest(Ftp::list(Locator::Path("/".into()), None, 10))?; assert_eq!(page.entries[0].name, " report.md "); drop(writer); Ftp::close(); let summary = fixture.summary(); assert_eq!(summary.control_connections, 2); assert_eq!(summary.upload_commands, 1); assert_eq!(summary.target_upload_commands, 0); assert_eq!(summary.uploaded_bytes, 0); assert_eq!(summary.stage_deletes, 1); assert_eq!(summary.promotions, 0); assert_eq!(summary.listings, 1); Ok(()) } #[tokio::test] async fn a_complete_upload_promotes_its_private_stage() -> TestResult { let fixture = test_host::install(test_host::Scenario::SuccessfulUpload); guest(Ftp::open("ftp".into(), config()))?; let writer = guest(start_writer("/target".into()))?; guest(writer.push(b"body".to_vec()))?; let info = guest(writer.finish())?.ok_or("missing upload result")?; assert_eq!(info.size, 4); drop(writer); Ftp::close(); let summary = fixture.summary(); assert_eq!(summary.control_connections, 1); assert_eq!(summary.upload_commands, 1); assert_eq!(summary.target_upload_commands, 0); assert_eq!(summary.uploaded_bytes, 4); assert_eq!(summary.stage_deletes, 0); assert_eq!(summary.promotions, 1); Ok(()) } #[tokio::test] async fn dropping_an_upload_removes_only_its_private_stage() -> TestResult { let fixture = test_host::install(test_host::Scenario::AbandonedUpload); guest(Ftp::open("ftp".into(), config()))?; let writer = guest(start_writer("/target".into()))?; guest(writer.push(b"partial".to_vec()))?; drop(writer); Ftp::close(); let summary = fixture.summary(); assert_eq!(summary.control_connections, 1); assert_eq!(summary.upload_commands, 1); assert_eq!(summary.target_upload_commands, 0); assert_eq!(summary.uploaded_bytes, 7); assert_eq!(summary.stage_deletes, 1); assert_eq!(summary.promotions, 0); Ok(()) } #[tokio::test] async fn a_stage_name_collision_draws_a_fresh_random_token() -> TestResult { let fixture = test_host::install(test_host::Scenario::StageCollision); guest(Ftp::open("ftp".into(), config()))?; let mut tokens = [1, 2].into_iter(); let staged = guest(stage_path_with("/target", || { tokens.next().ok_or(ErrorCode::Internal) }))?; assert_eq!(staged, "/.memona-upload-00000000000000000000000000000002"); Ftp::close(); assert_eq!(fixture.summary().stage_probes, 2); Ok(()) }