feat(core): import_lastpass — URL/header robustness
Bad URLs in login rows downgrade to url: None with a warning rather than skipping the row. Header mismatches (extra columns, wrong order) surface ImportCsvHeader. Quoted commas, multi-line extra, unicode all parse cleanly via the csv crate's defaults.
This commit is contained in:
@@ -207,3 +207,70 @@ fn secure_note_preserves_structured_extra_verbatim() {
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_with_unparseable_url_imports_with_url_none_and_warns() {
|
||||
let csv = format!(
|
||||
"{HEADER}\n\
|
||||
not-a-real-url,alice,hunter2,,,Site,,",
|
||||
);
|
||||
let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
|
||||
assert_eq!(items.len(), 1);
|
||||
match &items[0].core {
|
||||
ItemCore::Login(l) => assert!(l.url.is_none()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert_eq!(warnings.len(), 1);
|
||||
assert!(warnings[0].message.contains("URL"), "msg: {}", warnings[0].message);
|
||||
assert_eq!(warnings[0].title.as_deref(), Some("Site"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_with_extra_column_is_rejected() {
|
||||
let bad = "url,username,password,totp,extra,name,grouping,fav,EXTRA\nhttps://x,u,p,,,T,,";
|
||||
let err = parse_lastpass_csv(bad.as_bytes()).unwrap_err();
|
||||
let msg = format!("{err}");
|
||||
assert!(msg.contains("LastPass") || msg.contains("expected"), "msg: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_with_wrong_column_order_is_rejected() {
|
||||
let swapped = "name,url,username,password,totp,extra,grouping,fav\nT,https://x,u,p,,,,";
|
||||
let err = parse_lastpass_csv(swapped.as_bytes()).unwrap_err();
|
||||
assert!(format!("{err}").contains("expected"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_comma_in_extra_parses() {
|
||||
let csv = format!(
|
||||
"{HEADER}\n\
|
||||
https://x,u,p,,\"hint with, a comma\",Site,,",
|
||||
);
|
||||
let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
|
||||
assert!(warnings.is_empty());
|
||||
assert_eq!(items[0].notes.as_deref(), Some("hint with, a comma"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_title_round_trips() {
|
||||
let csv = format!("{HEADER}\nhttps://x,u,p,,,Müllerstraße — café ☕,,");
|
||||
let (items, _) = parse_lastpass_csv(csv.as_bytes()).unwrap();
|
||||
assert_eq!(items[0].title, "Müllerstraße — café ☕");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_csv_after_header_returns_empty_vecs() {
|
||||
let (items, warnings) = parse_lastpass_csv(HEADER.as_bytes()).unwrap();
|
||||
assert!(items.is_empty());
|
||||
assert!(warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_header_is_rejected() {
|
||||
// Empty input — csv reader treats first row as header (which doesn't exist).
|
||||
let err = parse_lastpass_csv(b"").unwrap_err();
|
||||
let msg = format!("{err}");
|
||||
// Either ImportCsvHeader (header didn't match) or ImportCsvFormat (read
|
||||
// failed). Both are acceptable; we just need a clear error.
|
||||
assert!(msg.contains("LastPass") || msg.contains("CSV"), "msg: {msg}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user