Implement confirmed Android tree mutation workflows (#108)
This commit is contained in:
1 parent
6a864a8d7f
commit
316a875d63
22 files changed
+2170
-220
No files matched your search
@@ -16,6 +16,249 @@ use ironstorage::{
|
||||
};
|
||||
use support::compatibility::{FixtureSet, TestResult};
|
||||
|
||||
#[test]
|
||||
fn mobile_tree_plans_confirm_folders_and_preserve_pass_recipient_semantics() -> TestResult {
|
||||
use ironstorage::mobile_mutation::{
|
||||
MobileMutationAction as Action, MobileMutationRequest, MobileMutationService,
|
||||
};
|
||||
let fixture = FixtureSet::load()?;
|
||||
let store = fixture.materialize_store("basic")?;
|
||||
fs::create_dir(store.path().join("archive"))?;
|
||||
let repository = Repository::open(store.path())?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let service = MobileMutationService::new(&repository);
|
||||
let mut secrets = Secrets::all(&fixture);
|
||||
let mut committer = Committer::default();
|
||||
let original = repository.read_entry(&EntryPath::parse("email/personal")?)?;
|
||||
let plan = service.prepare("email/", Action::Copy, false, false)?;
|
||||
assert!(plan.is_directory());
|
||||
assert_eq!(plan.entries(), 1);
|
||||
assert!(
|
||||
plan.destinations()
|
||||
.iter()
|
||||
.all(|destination| destination.path() != "email")
|
||||
);
|
||||
let mut request = MobileMutationRequest {
|
||||
action: Action::Copy,
|
||||
source: plan.source().into(),
|
||||
revision: plan.revision().into(),
|
||||
destination: Some("team".into()),
|
||||
confirmed: false,
|
||||
overwrite: false,
|
||||
discard_editor: false,
|
||||
};
|
||||
assert!(
|
||||
service
|
||||
.perform(&request, false, &keys, &mut secrets, &mut committer)
|
||||
.is_err()
|
||||
);
|
||||
assert!(committer.changes.is_empty());
|
||||
assert!(!store.path().join("team/email").exists());
|
||||
request.confirmed = true;
|
||||
service.perform(&request, false, &keys, &mut secrets, &mut committer)?;
|
||||
assert_eq!(
|
||||
repository.read_entry(&EntryPath::parse("email/personal")?)?,
|
||||
original
|
||||
);
|
||||
let copied = repository.read_entry(&EntryPath::parse("team/email/personal")?)?;
|
||||
assert_ne!(copied, original);
|
||||
assert!(keys.is_encrypted_for(
|
||||
&copied,
|
||||
&[keys.resolve(&fixture.key("bob")?.primary_fingerprint)?]
|
||||
)?);
|
||||
assert_eq!(
|
||||
keys.decrypt(&copied, &mut secrets)?.expose(),
|
||||
fixture.read("expected/basic/email/personal.txt")?
|
||||
);
|
||||
let plan = service.prepare("team/", Action::Move, false, false)?;
|
||||
let policy = fs::read(store.path().join("team/.gpg-id"))?;
|
||||
service.perform(
|
||||
&MobileMutationRequest {
|
||||
action: Action::Move,
|
||||
source: plan.source().into(),
|
||||
revision: plan.revision().into(),
|
||||
destination: Some("archive".into()),
|
||||
confirmed: true,
|
||||
overwrite: false,
|
||||
discard_editor: false,
|
||||
},
|
||||
false,
|
||||
&keys,
|
||||
&mut secrets,
|
||||
&mut committer,
|
||||
)?;
|
||||
assert!(!store.path().join("team").exists());
|
||||
assert_eq!(fs::read(store.path().join("archive/team/.gpg-id"))?, policy);
|
||||
assert_eq!(
|
||||
repository.read_entry(&EntryPath::parse("archive/team/email/personal")?)?,
|
||||
copied
|
||||
);
|
||||
let plan = service.prepare("archive/team/", Action::Delete, false, false)?;
|
||||
let mut request = MobileMutationRequest {
|
||||
action: Action::Delete,
|
||||
source: plan.source().into(),
|
||||
revision: plan.revision().into(),
|
||||
destination: None,
|
||||
confirmed: false,
|
||||
overwrite: false,
|
||||
discard_editor: false,
|
||||
};
|
||||
assert!(
|
||||
service
|
||||
.perform(&request, false, &keys, &mut secrets, &mut committer)
|
||||
.is_err()
|
||||
);
|
||||
assert!(store.path().join("archive/team").exists());
|
||||
request.confirmed = true;
|
||||
service.perform(&request, false, &keys, &mut secrets, &mut committer)?;
|
||||
assert!(!store.path().join("archive/team").exists());
|
||||
assert_eq!(
|
||||
repository.read_entry(&EntryPath::parse("email/personal")?)?,
|
||||
original
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mobile_overwrite_is_explicit_and_destination_replacement_invalidates_the_plan() -> TestResult {
|
||||
use ironstorage::mobile_mutation::{
|
||||
MobileMutationAction as Action, MobileMutationError, MobileMutationRequest,
|
||||
MobileMutationService,
|
||||
};
|
||||
let fixture = FixtureSet::load()?;
|
||||
let store = fixture.materialize_store("basic")?;
|
||||
let repository = Repository::open(store.path())?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let path = EntryPath::parse("team/personal")?;
|
||||
let original = repository.read_entry(&EntryPath::parse("email/personal")?)?;
|
||||
repository.write_entry(&path, &original)?;
|
||||
let service = MobileMutationService::new(&repository);
|
||||
let plan = service.prepare("email/personal", Action::Copy, false, false)?;
|
||||
let target = plan
|
||||
.destinations()
|
||||
.iter()
|
||||
.find(|target| target.path() == "team")
|
||||
.unwrap();
|
||||
assert!(target.requires_overwrite());
|
||||
assert_eq!(target.collision_targets(), &["team/personal"]);
|
||||
let mut request = MobileMutationRequest {
|
||||
action: Action::Copy,
|
||||
source: plan.source().into(),
|
||||
revision: plan.revision().into(),
|
||||
destination: Some("team".into()),
|
||||
confirmed: true,
|
||||
overwrite: false,
|
||||
discard_editor: false,
|
||||
};
|
||||
let mut secrets = Secrets::all(&fixture);
|
||||
let mut committer = Committer::default();
|
||||
assert!(matches!(
|
||||
service.perform(&request, false, &keys, &mut secrets, &mut committer),
|
||||
Err(MobileMutationError::OverwriteRequired { .. })
|
||||
));
|
||||
repository.write_entry(&path, &original)?;
|
||||
request.overwrite = true;
|
||||
assert!(matches!(
|
||||
service.perform(&request, false, &keys, &mut secrets, &mut committer),
|
||||
Err(MobileMutationError::StaleEntry { .. })
|
||||
));
|
||||
assert_eq!(repository.read_entry(&path)?, original);
|
||||
assert!(committer.changes.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mobile_rechecks_revisions_after_reencryption_and_rolls_back_failed_commits() -> TestResult {
|
||||
use ironstorage::mobile_mutation::{
|
||||
MobileMutationAction as Action, MobileMutationRequest, MobileMutationService,
|
||||
};
|
||||
struct ReplacingSecrets<'a> {
|
||||
inner: Secrets,
|
||||
repository: &'a Repository,
|
||||
path: EntryPath,
|
||||
}
|
||||
impl SecretProvider for ReplacingSecrets<'_> {
|
||||
fn secret_for(&mut self, key: &KeyInfo) -> Result<SecretBytes, SecretProviderError> {
|
||||
self.repository
|
||||
.write_entry(
|
||||
&self.path,
|
||||
&ironstorage::repository::EncryptedEntry::new(b"replacement".to_vec()),
|
||||
)
|
||||
.map_err(|_| SecretProviderError::Unavailable)?;
|
||||
self.inner.secret_for(key)
|
||||
}
|
||||
}
|
||||
let fixture = FixtureSet::load()?;
|
||||
let store = fixture.materialize_store("basic")?;
|
||||
fs::create_dir(store.path().join("archive"))?;
|
||||
let repository = Repository::open(store.path())?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let service = MobileMutationService::new(&repository);
|
||||
let original = repository.read_entry(&EntryPath::parse("email/personal")?)?;
|
||||
let plan = service.prepare("email/personal", Action::Move, false, false)?;
|
||||
let mut provider = ReplacingSecrets {
|
||||
inner: Secrets::all(&fixture),
|
||||
repository: &repository,
|
||||
path: EntryPath::parse("email/personal")?,
|
||||
};
|
||||
let mut committer = Committer::default();
|
||||
assert!(
|
||||
service
|
||||
.perform(
|
||||
&MobileMutationRequest {
|
||||
action: Action::Move,
|
||||
source: plan.source().into(),
|
||||
revision: plan.revision().into(),
|
||||
destination: Some("team".into()),
|
||||
confirmed: true,
|
||||
overwrite: false,
|
||||
discard_editor: false,
|
||||
},
|
||||
false,
|
||||
&keys,
|
||||
&mut provider,
|
||||
&mut committer
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
repository
|
||||
.read_entry(&EntryPath::parse("email/personal")?)?
|
||||
.as_bytes(),
|
||||
b"replacement"
|
||||
);
|
||||
assert!(!store.path().join("team/personal.gpg").exists());
|
||||
assert!(committer.changes.is_empty());
|
||||
repository.write_entry(&EntryPath::parse("email/personal")?, &original)?;
|
||||
let plan = service.prepare("email/", Action::Move, false, false)?;
|
||||
committer.fail = true;
|
||||
assert!(
|
||||
service
|
||||
.perform(
|
||||
&MobileMutationRequest {
|
||||
action: Action::Move,
|
||||
source: plan.source().into(),
|
||||
revision: plan.revision().into(),
|
||||
destination: Some("archive".into()),
|
||||
confirmed: true,
|
||||
overwrite: false,
|
||||
discard_editor: false,
|
||||
},
|
||||
false,
|
||||
&keys,
|
||||
&mut Secrets::all(&fixture),
|
||||
&mut committer
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
repository.read_entry(&EntryPath::parse("email/personal")?)?,
|
||||
original
|
||||
);
|
||||
assert!(!store.path().join("archive/email").exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Secrets(BTreeMap<String, Vec<u8>>);
|
||||
impl Secrets {
|
||||
fn all(fixture: &FixtureSet) -> Self {
|
||||
|
||||
Reference in new issue
Block a user