lib

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

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 }