capability_tests.rs (7203B)
1 use super::*; 2 use crate::OpenOptions; 3 use radroots_storage::authored_draft::{ 4 AuthoredDraft, AuthoredDraftId, AuthoredDraftStage, AuthoredDraftStore, 5 }; 6 use radroots_storage::event::SourceGeneration; 7 8 fn plan(id: u8) -> BackupPlan { 9 BackupPlan::new( 10 BackupId::new([id; 16]).unwrap(), 11 BackupFormatVersion::V1, 12 BackupSecretPolicy::IncludeProtectedStorage, 13 100, 14 ) 15 .unwrap() 16 } 17 18 async fn fixture(root: &Path, backup: &Path) -> SqliteStorage { 19 SqliteStorage::open( 20 OpenOptions::new( 21 crate::Paths::from_directory(root).unwrap(), 22 OpenMode::Create, 23 ) 24 .with_source_generation(SourceGeneration::new([7; 32]).unwrap(), 100) 25 .unwrap() 26 .with_backup_root(backup) 27 .unwrap(), 28 ) 29 .await 30 .unwrap() 31 } 32 33 #[tokio::test] 34 async fn canonical_capability_captures_wal_and_finalizes_exact_members() { 35 let root = tempfile::tempdir().unwrap(); 36 let backup = tempfile::tempdir().unwrap(); 37 let store = fixture(root.path(), backup.path()).await; 38 let draft = AuthoredDraft::initial( 39 AuthoredDraftId::new([8; 16]).unwrap(), 40 [3; 32], 41 "fixture.backup.v1", 42 b"latest committed WAL work".to_vec(), 43 AuthoredDraftStage::Draft, 44 None, 45 7777, 46 ) 47 .unwrap(); 48 store 49 .append_authored_draft(draft.clone(), None) 50 .await 51 .unwrap(); 52 assert!( 53 fs::metadata(root.path().join("runtime.sqlite-wal")) 54 .unwrap() 55 .len() 56 > 0 57 ); 58 let plan = plan(1); 59 let owner: &dyn StorageReliability = &store; 60 drop(owner.capture_backup(plan.clone())); 61 assert_eq!(fs::read_dir(backup.path()).unwrap().count(), 0); 62 let manifest = owner.capture_backup(plan.clone()).await.unwrap(); 63 assert_eq!(manifest.members().len(), 2); 64 let layout = BackupLayout::new(backup.path(), &plan); 65 let mut connection = SqliteConnection::connect_with( 66 &SqliteConnectOptions::new() 67 .filename(&layout.runtime_file) 68 .read_only(true), 69 ) 70 .await 71 .unwrap(); 72 let value: Vec<u8> = 73 sqlx::query_scalar("SELECT snapshot FROM radroots_runtime_authored_draft_revisions") 74 .fetch_one(&mut connection) 75 .await 76 .unwrap(); 77 assert_eq!( 78 serde_json::from_slice::<AuthoredDraft>(&value).unwrap(), 79 draft 80 ); 81 connection.close().await.unwrap(); 82 owner 83 .verify_backup(plan.clone(), manifest.clone()) 84 .await 85 .unwrap(); 86 for _ in 0..2 { 87 owner 88 .finalize_backup(plan.clone(), manifest.clone()) 89 .await 90 .unwrap(); 91 } 92 assert!(!layout.staging.exists()); 93 assert!(layout.finalized.is_dir()); 94 assert_eq!( 95 owner.capture_backup(plan.clone()).await, 96 Err(BackupCapabilityError::Conflict) 97 ); 98 store.close().await.unwrap(); 99 assert_eq!( 100 owner.capture_backup(plan.clone()).await, 101 Err(BackupCapabilityError::Unavailable) 102 ); 103 assert_eq!( 104 owner.verify_backup(plan.clone(), manifest.clone()).await, 105 Err(BackupCapabilityError::Unavailable) 106 ); 107 assert_eq!( 108 owner.finalize_backup(plan, manifest).await, 109 Err(BackupCapabilityError::Unavailable) 110 ); 111 } 112 113 #[tokio::test] 114 async fn canonical_capability_refuses_tampered_inventory_and_retains_staging() { 115 let root = tempfile::tempdir().unwrap(); 116 let backup = tempfile::tempdir().unwrap(); 117 let store = fixture(root.path(), backup.path()).await; 118 let owner: &dyn StorageReliability = &store; 119 let plan = plan(2); 120 let manifest = owner.capture_backup(plan.clone()).await.unwrap(); 121 let layout = BackupLayout::new(backup.path(), &plan); 122 let bytes = fs::read(&layout.runtime_file).unwrap(); 123 for bad_size in [false, true] { 124 let mut members = manifest.members().to_vec(); 125 let original = &members[0]; 126 members[0] = BackupMember::new( 127 original.relative_path(), 128 original.kind(), 129 original.byte_length() + u64::from(bad_size), 130 if bad_size { 131 original.sha256() 132 } else { 133 MemberDigest::new([0; 32]) 134 }, 135 ) 136 .unwrap(); 137 let bad = BackupManifest::new( 138 manifest.format_version(), 139 manifest.backup_id(), 140 manifest.created_at_unix_ms(), 141 manifest.secret_policy(), 142 members, 143 ) 144 .unwrap(); 145 assert_eq!( 146 owner.verify_backup(plan.clone(), bad.clone()).await, 147 Err(BackupCapabilityError::VerificationFailed) 148 ); 149 assert_eq!( 150 owner.finalize_backup(plan.clone(), bad).await, 151 Err(BackupCapabilityError::VerificationFailed) 152 ); 153 } 154 let wrong_plan = BackupPlan::new( 155 plan.backup_id(), 156 plan.format_version(), 157 plan.secret_policy(), 158 101, 159 ) 160 .unwrap(); 161 assert_eq!( 162 owner.verify_backup(wrong_plan, manifest.clone()).await, 163 Err(BackupCapabilityError::VerificationFailed) 164 ); 165 fs::write(layout.staging.join("unexpected"), b"retained").unwrap(); 166 assert_eq!( 167 owner.verify_backup(plan.clone(), manifest.clone()).await, 168 Err(BackupCapabilityError::VerificationFailed) 169 ); 170 assert_eq!(fs::read(&layout.runtime_file).unwrap(), bytes); 171 assert!(!layout.finalized.exists()); 172 assert_eq!( 173 owner.capture_backup(plan.clone()).await, 174 Err(BackupCapabilityError::Conflict) 175 ); 176 let missing = self::plan(3); 177 assert_eq!( 178 owner.verify_backup(missing, manifest).await, 179 Err(BackupCapabilityError::VerificationFailed) 180 ); 181 store.close().await.unwrap(); 182 } 183 184 #[tokio::test] 185 async fn canonical_capability_classifies_configuration_version_and_io_without_paths() { 186 let root = tempfile::tempdir().unwrap(); 187 let backup = tempfile::tempdir().unwrap(); 188 let mut store = fixture(root.path(), backup.path()).await; 189 let future = BackupPlan::new( 190 BackupId::new([4; 16]).unwrap(), 191 BackupFormatVersion::new(2).unwrap(), 192 BackupSecretPolicy::IncludeProtectedStorage, 193 100, 194 ) 195 .unwrap(); 196 assert_eq!( 197 StorageReliability::capture_backup(&store, future).await, 198 Err(BackupCapabilityError::UnsupportedVersion) 199 ); 200 store.backup_root = None; 201 assert_eq!( 202 StorageReliability::capture_backup(&store, plan(4)).await, 203 Err(BackupCapabilityError::InvalidConfiguration) 204 ); 205 store.backup_root = Some(std::sync::Arc::new(backup.path().join("absent"))); 206 assert_eq!( 207 StorageReliability::capture_backup(&store, plan(4)).await, 208 Err(BackupCapabilityError::InvalidConfiguration) 209 ); 210 // The public SPI must never forward backend paths or nested I/O details. 211 let error = capability::map_error(Error::BackupFilesystem { 212 operation: "capture /private/canary", 213 source: std::io::Error::other("credential-canary"), 214 }); 215 assert_eq!(error, BackupCapabilityError::Failed); 216 assert!(!format!("{error:?} {error}").contains("canary")); 217 store.close().await.unwrap(); 218 }