Implement native iPhone entry editor

This commit is contained in:
2026-08-11 20:27:14 +02:00
parent 900e62e523
commit ae64ce47a3
8 changed files with 2516 additions and 76 deletions

View File

@@ -9,7 +9,10 @@ use ironstorage::{
document::{
DocumentError, EntryDocumentService, EntryFieldDraft, EntryFieldKind, EntrySensitivity,
},
mobile_entry::{MobileEntryPage, MobileEntrySectionKind, field_value},
mobile_entry::{
MobileEntryDraft, MobileEntryEditorError, MobileEntryEditorFieldKind,
MobileEntryEditorInput, MobileEntryPage, MobileEntrySectionKind, field_value,
},
recipient::RecipientPolicyManager,
repository::{EntryPath, Repository, SecretBytes},
write::{EntryCommit, EntryCommitError, EntryCommitter, WriteError},
@@ -624,6 +627,133 @@ fn atomic_save_creates_updates_and_rejects_stale_documents() -> TestResult {
Ok(())
}
#[test]
fn mobile_editor_creates_edits_generates_and_preserves_dynamic_fields() -> TestResult {
let fixture = FixtureSet::load()?;
let store = fixture.materialize_store("basic")?;
let repository = Repository::open(store.path())?;
let keys = KeyStore::load(fixture.path("keys"))?;
let mut secrets = FixtureSecrets::all(&fixture);
write_plaintext(
&repository,
&keys,
"documents/mobile-editor",
b"old password\nlogin: alice\ncomments: first\nsecond\nnote: repeated one\nnote: repeated two\nbinary: \xff\n",
)?;
let service = EntryDocumentService::new(&repository, &keys);
let document = service.open("documents/mobile-editor", &mut secrets)?;
let mut draft = MobileEntryDraft::new(document, false);
let page = draft.page();
assert!(!page.dirty());
assert!(!page.creating());
assert_eq!(page.default_password_length(), 25);
assert_eq!(page.fields().len(), 6);
assert!(page.fields()[2].multiline());
assert_eq!(page.fields()[2].value(), Some("first\nsecond"));
assert!(page.fields()[5].value().is_none());
let password = page.fields()[0].id();
assert!(matches!(
draft.remove(password),
Err(MobileEntryEditorError::ProtectedPasswordField)
));
assert!(matches!(
draft.reorder(password, 1),
Err(MobileEntryEditorError::ProtectedPasswordField)
));
assert!(draft.generate_password(Some(0), false).is_err());
let repeated_one = page.fields()[3].id();
let inputs = page
.fields()
.iter()
.map(|field| {
let (name, value) = if field.label() == "Login" {
(Some("username".to_owned()), Some("bob".to_owned()))
} else if field.label() == "Comments" {
(
field.name().map(str::to_owned),
Some("heading\nline: preserved\n\nend".to_owned()),
)
} else {
(
field.name().map(str::to_owned),
field.value().map(str::to_owned),
)
};
MobileEntryEditorInput::new(field.id(), name, value)
})
.collect();
draft.apply(inputs)?;
draft.add(
MobileEntryEditorFieldKind::Field,
Some("tags".to_owned()),
"work, mobile".to_owned(),
)?;
draft.add(
MobileEntryEditorFieldKind::Note,
None,
"open note\nsecond line".to_owned(),
)?;
draft.remove(repeated_one)?;
let open_note = draft.page().fields().last().expect("open note").id();
draft.reorder(open_note, 2)?;
draft.generate_password(Some(32), true)?;
let edited = draft.page();
assert!(edited.dirty());
let password = edited.fields().first().expect("password").value().unwrap();
assert_eq!(password.len(), 32);
assert!(
password
.chars()
.all(|character| character.is_ascii_alphanumeric())
);
assert_eq!(edited.fields()[2].value(), Some("open note\nsecond line"));
let mut committer = Committer::default();
service.save_recoverable(draft.document(), None, &mut committer)?;
let reopened = service.open("documents/mobile-editor", &mut secrets)?;
assert_eq!(
reopened
.fields()
.iter()
.filter(|field| field.metadata().name() == Some("note"))
.count(),
1
);
assert!(
reopened.fields().iter().any(|field| {
field.metadata().name() == Some("username") && field.value() == b"bob"
})
);
assert!(reopened.fields().iter().any(|field| {
field.metadata().name() == Some("comments")
&& field.value() == b"heading\nline: preserved\n\nend"
}));
assert!(reopened.fields().iter().any(|field| {
field.metadata().name() == Some("tags") && field.value() == b"work, mobile"
}));
assert!(
reopened
.fields()
.iter()
.any(|field| { field.metadata().name() == Some("binary") && field.value() == b"\xff" })
);
let mut created = service.open("documents/cancelled-mobile", &mut secrets)?;
created.add(0, EntryFieldDraft::line(Vec::new())?)?;
let created = MobileEntryDraft::new(created, true);
assert!(created.page().creating());
assert!(created.page().dirty());
drop(created);
assert!(matches!(
repository.read_entry(&EntryPath::parse("documents/cancelled-mobile")?),
Err(ironstorage::repository::RepositoryError::NotFound { .. })
));
Ok(())
}
#[test]
fn every_compatibility_entry_round_trips_as_a_document() -> TestResult {
let fixture = FixtureSet::load()?;