feat(core): import_lastpass — SecureNote rows

Rows with url == "http://sn" map to SecureNoteCore with extra
copied verbatim into the body. LastPass-packed structured data
(credit cards, addresses) flows through unparsed — users can
re-categorize manually post-import.

SecureNote rows skip the password-required check that applies
to Logins.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-29 23:06:03 -04:00
parent c4905c5ee7
commit 0841bddcb5
2 changed files with 67 additions and 1 deletions

View File

@@ -155,3 +155,55 @@ fn login_with_lowercase_base32_totp_is_accepted() {
_ => unreachable!(),
}
}
#[test]
fn url_http_sn_maps_to_secure_note() {
let csv = format!(
"{HEADER}\n\
http://sn,,,,The body of the note,My Note,,",
);
let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
assert!(warnings.is_empty());
assert_eq!(items.len(), 1);
assert_eq!(items[0].title, "My Note");
match &items[0].core {
ItemCore::SecureNote(sn) => assert_eq!(sn.body.as_str(), "The body of the note"),
other => panic!("expected SecureNote, got {:?}", other),
}
}
#[test]
fn secure_note_does_not_require_password() {
// SecureNote rows have empty password; that must not trigger the
// `missing password` skip path (which is Login-only).
let csv = format!("{HEADER}\nhttp://sn,,,,note text,Title,,");
let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
assert!(warnings.is_empty(), "{:?}", warnings);
assert_eq!(items.len(), 1);
}
#[test]
fn secure_note_passes_through_grouping_and_favorite() {
let csv = format!("{HEADER}\nhttp://sn,,,,body,Title,Personal,1");
let (items, _) = parse_lastpass_csv(csv.as_bytes()).unwrap();
assert_eq!(items[0].group.as_deref(), Some("Personal"));
assert!(items[0].favorite);
}
#[test]
fn secure_note_preserves_structured_extra_verbatim() {
// LastPass packs structured note data (e.g. credit cards) into `extra`
// using their own key:value format. We do NOT auto-parse it — verbatim
// pass-through, per spec D10.
let csv_body = "NoteType:Credit Card\nNumber:4111111111111111\nCVV:123";
let csv = format!(
"{HEADER}\n\
http://sn,,,,\"{csv_body}\",Visa,,",
csv_body = csv_body,
);
let (items, _) = parse_lastpass_csv(csv.as_bytes()).unwrap();
match &items[0].core {
ItemCore::SecureNote(sn) => assert_eq!(sn.body.as_str(), csv_body),
_ => unreachable!(),
}
}