lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }