restore_capability_tests.rs (9904B)
1 use super::*; 2 use crate::OpenOptions; 3 use radroots_storage::{ 4 authored_draft::{AuthoredDraft, AuthoredDraftId, AuthoredDraftStage, AuthoredDraftStore}, 5 event::SourceGeneration, 6 status::ShutdownState, 7 }; 8 9 fn draft(id: u8) -> AuthoredDraft { 10 AuthoredDraft::initial( 11 AuthoredDraftId::new([id; 16]).unwrap(), 12 [3; 32], 13 "fixture.restore.v1", 14 vec![id], 15 AuthoredDraftStage::Draft, 16 None, 17 100, 18 ) 19 .unwrap() 20 } 21 22 async fn fixture(root: &Path, backup: &Path) -> (SqliteStorage, RestorePlan) { 23 let store = SqliteStorage::open( 24 OpenOptions::new( 25 crate::Paths::from_directory(root).unwrap(), 26 OpenMode::Create, 27 ) 28 .with_source_generation(SourceGeneration::new([7; 32]).unwrap(), 100) 29 .unwrap() 30 .with_backup_root(backup) 31 .unwrap(), 32 ) 33 .await 34 .unwrap(); 35 store.append_authored_draft(draft(1), None).await.unwrap(); 36 let plan = BackupPlan::new( 37 BackupId::new([6; 16]).unwrap(), 38 BackupFormatVersion::V1, 39 BackupSecretPolicy::IncludeProtectedStorage, 40 100, 41 ) 42 .unwrap(); 43 let owner: &dyn StorageReliability = &store; 44 let manifest = owner.capture_backup(plan.clone()).await.unwrap(); 45 owner.finalize_backup(plan, manifest.clone()).await.unwrap(); 46 store.append_authored_draft(draft(2), None).await.unwrap(); 47 let restore = 48 RestorePlan::new(manifest, BackupSecretPolicy::IncludeProtectedStorage, 200).unwrap(); 49 (store, restore) 50 } 51 52 #[tokio::test] 53 async fn restore_spi_stages_without_live_changes_then_closes_and_reopens_exact_history() { 54 let root = tempfile::tempdir().unwrap(); 55 let backup = tempfile::tempdir().unwrap(); 56 let (store, restore) = fixture(root.path(), backup.path()).await; 57 let owner: &dyn StorageReliability = &store; 58 let paths = crate::Paths::from_directory(root.path()).unwrap(); 59 let staging = RestoreStaging::new(&paths, restore.manifest()).unwrap(); 60 drop(owner.stage_restore(restore.clone())); 61 drop(owner.finalize_restore(restore.clone())); 62 assert!(!staging.runtime.exists() && !staging.private.exists()); 63 assert_eq!( 64 store.storage_status().await.unwrap().shutdown(), 65 ShutdownState::Open 66 ); 67 let statuses = owner.stage_restore(restore.clone()).await.unwrap(); 68 assert_eq!(statuses.len(), 2); 69 assert!( 70 statuses 71 .iter() 72 .all(|v| v.verification() == MemberVerification::Verified) 73 ); 74 assert_eq!( 75 store 76 .authored_draft_head(draft(2).draft_id()) 77 .await 78 .unwrap(), 79 Some(draft(2)) 80 ); 81 assert_eq!( 82 owner.stage_restore(restore.clone()).await, 83 Err(RestoreCapabilityError::Conflict) 84 ); 85 owner.finalize_restore(restore.clone()).await.unwrap(); 86 assert_eq!( 87 store.storage_status().await.unwrap().shutdown(), 88 ShutdownState::Closed 89 ); 90 assert_eq!( 91 owner.stage_restore(restore.clone()).await, 92 Err(RestoreCapabilityError::Unavailable) 93 ); 94 assert_eq!( 95 owner.finalize_restore(restore).await, 96 Err(RestoreCapabilityError::Unavailable) 97 ); 98 let reopened = SqliteStorage::open(OpenOptions::new(paths, OpenMode::ReadWriteExisting)) 99 .await 100 .unwrap(); 101 assert_eq!( 102 reopened 103 .authored_draft_head(draft(1).draft_id()) 104 .await 105 .unwrap(), 106 Some(draft(1)) 107 ); 108 assert_eq!( 109 reopened 110 .authored_draft_head(draft(2).draft_id()) 111 .await 112 .unwrap(), 113 None 114 ); 115 reopened.close().await.unwrap(); 116 } 117 118 #[tokio::test] 119 async fn restore_spi_rejects_configuration_version_and_tampering_without_live_changes() { 120 let root = tempfile::tempdir().unwrap(); 121 let backup = tempfile::tempdir().unwrap(); 122 let (mut store, restore) = fixture(root.path(), backup.path()).await; 123 let backup_root = store.backup_root.take(); 124 assert_eq!( 125 StorageReliability::stage_restore(&store, restore.clone()).await, 126 Err(RestoreCapabilityError::InvalidConfiguration) 127 ); 128 store.backup_root = Some(std::sync::Arc::new(backup.path().join("absent"))); 129 assert_eq!( 130 StorageReliability::stage_restore(&store, restore.clone()).await, 131 Err(RestoreCapabilityError::InvalidConfiguration) 132 ); 133 store.backup_root = backup_root; 134 let original = restore.manifest(); 135 for future in [false, true] { 136 let mut members = original.members().to_vec(); 137 if !future { 138 let member = &members[0]; 139 members[0] = BackupMember::new( 140 member.relative_path(), 141 member.kind(), 142 member.byte_length() + 1, 143 member.sha256(), 144 ) 145 .unwrap(); 146 } 147 let manifest = BackupManifest::new( 148 if future { 149 BackupFormatVersion::new(2).unwrap() 150 } else { 151 BackupFormatVersion::V1 152 }, 153 original.backup_id(), 154 original.created_at_unix_ms(), 155 original.secret_policy(), 156 members, 157 ) 158 .unwrap(); 159 let bad = RestorePlan::new(manifest, original.secret_policy(), 200).unwrap(); 160 assert_eq!( 161 StorageReliability::stage_restore(&store, bad.clone()).await, 162 Err(RestoreCapabilityError::VerificationFailed) 163 ); 164 let finalization = StorageReliability::finalize_restore(&store, bad).await; 165 if future { 166 assert_eq!( 167 finalization, 168 Err(RestoreCapabilityError::UnsupportedVersion) 169 ); 170 } else { 171 assert_eq!(finalization, Err(RestoreCapabilityError::Failed)); 172 } 173 assert_eq!( 174 store.storage_status().await.unwrap().shutdown(), 175 ShutdownState::Open 176 ); 177 } 178 assert_eq!( 179 store 180 .authored_draft_head(draft(2).draft_id()) 181 .await 182 .unwrap(), 183 Some(draft(2)) 184 ); 185 store.close().await.unwrap(); 186 let reader = SqliteStorage::open( 187 OpenOptions::new( 188 crate::Paths::from_directory(root.path()).unwrap(), 189 OpenMode::ReadOnly, 190 ) 191 .with_backup_root(backup.path()) 192 .unwrap(), 193 ) 194 .await 195 .unwrap(); 196 assert_eq!( 197 StorageReliability::stage_restore(&reader, restore.clone()).await, 198 Err(RestoreCapabilityError::Unavailable) 199 ); 200 assert_eq!( 201 StorageReliability::finalize_restore(&reader, restore).await, 202 Err(RestoreCapabilityError::Unavailable) 203 ); 204 reader.close().await.unwrap(); 205 } 206 207 #[tokio::test] 208 async fn cancelled_restore_close_can_be_drained_without_releasing_a_live_writer() { 209 let root = tempfile::tempdir().unwrap(); 210 let backup = tempfile::tempdir().unwrap(); 211 let (store, restore) = fixture(root.path(), backup.path()).await; 212 StorageReliability::stage_restore(&store, restore.clone()) 213 .await 214 .unwrap(); 215 let held = store.pool.acquire().await.unwrap(); 216 let held_private = store.private_pool.acquire().await.unwrap(); 217 let mut finalization = Box::pin(StorageReliability::finalize_restore(&store, restore)); 218 tokio::time::timeout(std::time::Duration::from_secs(10), async { 219 loop { 220 std::future::poll_fn(|context| { 221 assert!(finalization.as_mut().poll(context).is_pending()); 222 std::task::Poll::Ready(()) 223 }) 224 .await; 225 if store.storage_status().await.unwrap().shutdown() == ShutdownState::Closing { 226 break; 227 } 228 tokio::task::yield_now().await; 229 } 230 }) 231 .await 232 .unwrap(); 233 drop(finalization); 234 let paths = crate::Paths::from_directory(root.path()).unwrap(); 235 assert!(matches!( 236 SqliteStorage::open(OpenOptions::new(paths.clone(), OpenMode::ReadWriteExisting)).await, 237 Err(Error::WriterAlreadyActive { .. }) 238 )); 239 drop(held); 240 let mut close = Box::pin(store.close()); 241 tokio::time::timeout(std::time::Duration::from_secs(10), async { 242 loop { 243 std::future::poll_fn(|context| { 244 assert!(close.as_mut().poll(context).is_pending()); 245 std::task::Poll::Ready(()) 246 }) 247 .await; 248 if store.private_pool.is_closed() { 249 break; 250 } 251 tokio::task::yield_now().await; 252 } 253 }) 254 .await 255 .unwrap(); 256 assert!(matches!( 257 SqliteStorage::open(OpenOptions::new(paths.clone(), OpenMode::ReadWriteExisting)).await, 258 Err(Error::WriterAlreadyActive { .. }) 259 )); 260 drop(held_private); 261 assert_eq!(close.await.unwrap().shutdown(), ShutdownState::Closed); 262 let reopened = SqliteStorage::open(OpenOptions::new(paths, OpenMode::ReadWriteExisting)) 263 .await 264 .unwrap(); 265 assert_eq!( 266 reopened 267 .authored_draft_head(draft(2).draft_id()) 268 .await 269 .unwrap(), 270 Some(draft(2)) 271 ); 272 reopened.close().await.unwrap(); 273 } 274 275 #[test] 276 fn restore_spi_errors_do_not_expose_filesystem_evidence_or_nested_causes() { 277 for raw in [ 278 Error::RestoreFilesystem { 279 operation: "/private/canary", 280 source: std::io::Error::other("credential-canary"), 281 }, 282 Error::RestoreMarkerCorrupt(PathBuf::from("/private/canary")), 283 Error::RestoreRecoveryConflict(PathBuf::from("/private/canary")), 284 Error::RestoreStagingFailed { 285 member: "credential-canary", 286 }, 287 ] { 288 let bounded = capability::map_restore_error(raw); 289 let report = format!("{bounded:?} {bounded}"); 290 assert!(!report.contains("canary") && !report.contains('/')); 291 assert!(std::error::Error::source(&bounded).is_none()); 292 } 293 }