1use chrono::{DateTime, Utc};
2use credential_exchange_format::{
3 Account as CxfAccount, AddressCredential, ApiKeyCredential, BasicAuthCredential, Credential,
4 CreditCardCredential, DriversLicenseCredential, IdentityDocumentCredential, Item,
5 NoteCredential, PasskeyCredential, PassportCredential, PersonNameCredential, SshKeyCredential,
6 TotpCredential, WifiCredential,
7};
8
9use crate::{
10 cxf::{
11 api_key::api_key_to_fields,
12 card::to_card,
13 identity::{
14 address_to_identity, drivers_license_to_identity, identity_document_to_identity,
15 passport_to_identity, person_name_to_identity,
16 },
17 login::to_login,
18 note::extract_note_content,
19 ssh::to_ssh,
20 wifi::wifi_to_fields,
21 CxfError,
22 },
23 CipherType, Field, ImportingCipher, SecureNote, SecureNoteType,
24};
25
26pub(crate) fn parse_cxf(payload: String) -> Result<Vec<ImportingCipher>, CxfError> {
30 let account: CxfAccount = serde_json::from_str(&payload)?;
31
32 let items: Vec<ImportingCipher> = account.items.into_iter().flat_map(parse_item).collect();
33
34 Ok(items)
35}
36
37fn convert_date(ts: Option<u64>) -> DateTime<Utc> {
41 ts.and_then(|ts| DateTime::from_timestamp(ts as i64, 0))
42 .unwrap_or(Utc::now())
43}
44
45pub(super) fn parse_item(value: Item) -> Vec<ImportingCipher> {
46 let grouped = group_credentials_by_type(value.credentials);
47
48 let creation_date = convert_date(value.creation_at);
49 let revision_date = convert_date(value.modified_at);
50
51 let mut output = vec![];
52
53 let scope = value.scope.as_ref();
54
55 let note_content = grouped.note.first().map(extract_note_content);
57
58 let mut add_item = |t: CipherType, fields: Vec<Field>| {
60 output.push(ImportingCipher {
61 folder_id: None, name: value.title.clone(),
63 notes: note_content.clone(),
64 r#type: t,
65 favorite: false,
66 reprompt: 0,
67 fields,
68 revision_date,
69 creation_date,
70 deleted_date: None,
71 })
72 };
73
74 if !grouped.basic_auth.is_empty() || !grouped.passkey.is_empty() || !grouped.totp.is_empty() {
76 let basic_auth = grouped.basic_auth.first();
77 let passkey = grouped.passkey.first();
78 let totp = grouped.totp.first();
79
80 let login = to_login(creation_date, basic_auth, passkey, totp, scope);
81 add_item(CipherType::Login(Box::new(login)), vec![]);
82 }
83
84 if let Some(credit_card) = grouped.credit_card.first() {
86 let (card, fields) = to_card(credit_card);
87
88 add_item(CipherType::Card(Box::new(card)), fields);
89 }
90
91 let secure_note_type = || {
93 CipherType::SecureNote(Box::new(SecureNote {
94 r#type: SecureNoteType::Generic,
95 }))
96 };
97
98 if let Some(api_key) = grouped.api_key.first() {
100 let fields = api_key_to_fields(api_key);
101 add_item(secure_note_type(), fields);
102 }
103
104 if let Some(wifi) = grouped.wifi.first() {
106 let fields = wifi_to_fields(wifi);
107 add_item(secure_note_type(), fields);
108 }
109
110 [
112 grouped
113 .address
114 .first()
115 .map(|a| address_to_identity(a.clone())),
116 grouped
117 .passport
118 .first()
119 .map(|p| passport_to_identity(p.clone())),
120 grouped
121 .person_name
122 .first()
123 .map(|p| person_name_to_identity(p.clone())),
124 grouped
125 .drivers_license
126 .first()
127 .map(|d| drivers_license_to_identity(d.clone())),
128 grouped
129 .identity_document
130 .first()
131 .map(|i| identity_document_to_identity(i.clone())),
132 ]
133 .into_iter()
134 .flatten()
135 .for_each(|(identity, custom_fields)| {
136 add_item(CipherType::Identity(Box::new(identity)), custom_fields);
137 });
138
139 if let Some(ssh) = grouped.ssh.first() {
141 match to_ssh(ssh) {
142 Ok((ssh_key, fields)) => add_item(CipherType::SshKey(Box::new(ssh_key)), fields),
143 Err(_) => {
144 }
146 }
147 }
148
149 if !grouped.note.is_empty() && output.is_empty() {
151 let standalone_note_content = grouped.note.first().map(extract_note_content);
152 output.push(ImportingCipher {
153 folder_id: None, name: value.title.clone(),
155 notes: standalone_note_content,
156 r#type: secure_note_type(),
157 favorite: false,
158 reprompt: 0,
159 fields: vec![],
160 revision_date,
161 creation_date,
162 deleted_date: None,
163 });
164 }
165
166 output
167}
168
169fn group_credentials_by_type(credentials: Vec<Credential>) -> GroupedCredentials {
176 fn filter_credentials<T>(
177 credentials: &[Credential],
178 f: impl Fn(&Credential) -> Option<&T>,
179 ) -> Vec<T>
180 where
181 T: Clone,
182 {
183 credentials.iter().filter_map(f).cloned().collect()
184 }
185
186 GroupedCredentials {
187 api_key: filter_credentials(&credentials, |c| match c {
188 Credential::ApiKey(api_key) => Some(api_key.as_ref()),
189 _ => None,
190 }),
191 basic_auth: filter_credentials(&credentials, |c| match c {
192 Credential::BasicAuth(basic_auth) => Some(basic_auth.as_ref()),
193 _ => None,
194 }),
195 credit_card: filter_credentials(&credentials, |c| match c {
196 Credential::CreditCard(credit_card) => Some(credit_card.as_ref()),
197 _ => None,
198 }),
199 passkey: filter_credentials(&credentials, |c| match c {
200 Credential::Passkey(passkey) => Some(passkey.as_ref()),
201 _ => None,
202 }),
203 ssh: filter_credentials(&credentials, |c| match c {
204 Credential::SshKey(ssh) => Some(ssh.as_ref()),
205 _ => None,
206 }),
207 totp: filter_credentials(&credentials, |c| match c {
208 Credential::Totp(totp) => Some(totp.as_ref()),
209 _ => None,
210 }),
211 wifi: filter_credentials(&credentials, |c| match c {
212 Credential::Wifi(wifi) => Some(wifi.as_ref()),
213 _ => None,
214 }),
215 address: filter_credentials(&credentials, |c| match c {
216 Credential::Address(address) => Some(address.as_ref()),
217 _ => None,
218 }),
219 passport: filter_credentials(&credentials, |c| match c {
220 Credential::Passport(passport) => Some(passport.as_ref()),
221 _ => None,
222 }),
223 person_name: filter_credentials(&credentials, |c| match c {
224 Credential::PersonName(person_name) => Some(person_name.as_ref()),
225 _ => None,
226 }),
227 drivers_license: filter_credentials(&credentials, |c| match c {
228 Credential::DriversLicense(drivers_license) => Some(drivers_license.as_ref()),
229 _ => None,
230 }),
231 identity_document: filter_credentials(&credentials, |c| match c {
232 Credential::IdentityDocument(identity_document) => Some(identity_document.as_ref()),
233 _ => None,
234 }),
235 note: filter_credentials(&credentials, |c| match c {
236 Credential::Note(note) => Some(note.as_ref()),
237 _ => None,
238 }),
239 }
240}
241
242struct GroupedCredentials {
243 address: Vec<AddressCredential>,
244 api_key: Vec<ApiKeyCredential>,
245 basic_auth: Vec<BasicAuthCredential>,
246 credit_card: Vec<CreditCardCredential>,
247 drivers_license: Vec<DriversLicenseCredential>,
248 identity_document: Vec<IdentityDocumentCredential>,
249 note: Vec<NoteCredential>,
250 passkey: Vec<PasskeyCredential>,
251 passport: Vec<PassportCredential>,
252 person_name: Vec<PersonNameCredential>,
253 ssh: Vec<SshKeyCredential>,
254 totp: Vec<TotpCredential>,
255 wifi: Vec<WifiCredential>,
256}
257
258#[cfg(test)]
259mod tests {
260 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
261 use chrono::{Duration, Month};
262 use credential_exchange_format::{CreditCardCredential, EditableFieldYearMonth};
263
264 use super::*;
265
266 #[test]
267 fn test_convert_date() {
268 let timestamp: u64 = 1706613834;
269 let datetime = convert_date(Some(timestamp));
270 assert_eq!(
271 datetime,
272 "2024-01-30T11:23:54Z".parse::<DateTime<Utc>>().unwrap()
273 );
274 }
275
276 #[test]
277 fn test_convert_date_none() {
278 let datetime = convert_date(None);
279 assert!(datetime > Utc::now() - Duration::seconds(1));
280 assert!(datetime <= Utc::now());
281 }
282
283 #[test]
284 fn test_parse_empty_item() {
285 let item = Item {
286 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
287 creation_at: Some(1706613834),
288 modified_at: Some(1706623773),
289 title: "Bitwarden".to_string(),
290 subtitle: None,
291 favorite: None,
292 credentials: vec![],
293 tags: None,
294 extensions: None,
295 scope: None,
296 };
297
298 let ciphers: Vec<ImportingCipher> = parse_item(item);
299 assert_eq!(ciphers.len(), 0);
300 }
301
302 #[test]
303 fn test_parse_passkey() {
304 let item = Item {
305 id: URL_SAFE_NO_PAD
306 .decode("Njk1RERENTItNkQ0Ny00NERBLTlFN0EtNDM1MjNEQjYzNjVF")
307 .unwrap()
308 .as_slice()
309 .into(),
310 creation_at: Some(1732181986),
311 modified_at: Some(1732182026),
312 title: "opotonniee.github.io".to_string(),
313 subtitle: None,
314 favorite: None,
315 credentials: vec![Credential::Passkey(Box::new(PasskeyCredential {
316 credential_id: URL_SAFE_NO_PAD
317 .decode("6NiHiekW4ZY8vYHa-ucbvA")
318 .unwrap()
319 .as_slice()
320 .into(),
321 rp_id: "opotonniee.github.io".to_string(),
322 username: "alex muller".to_string(),
323 user_display_name: "alex muller".to_string(),
324 user_handle: URL_SAFE_NO_PAD
325 .decode("YWxleCBtdWxsZXI")
326 .unwrap()
327 .as_slice()
328 .into(),
329 key: URL_SAFE_NO_PAD
330 .decode("MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl")
331 .unwrap()
332 .as_slice()
333 .into(),
334 fido2_extensions: None,
335 }))],
336 tags: None,
337 extensions: None,
338 scope: None,
339 };
340
341 let ciphers: Vec<ImportingCipher> = parse_item(item);
342 assert_eq!(ciphers.len(), 1);
343 let cipher = ciphers.first().unwrap();
344
345 assert_eq!(cipher.folder_id, None);
346 assert_eq!(cipher.name, "opotonniee.github.io");
347
348 let login = match &cipher.r#type {
349 CipherType::Login(login) => login,
350 _ => panic!("Expected login"),
351 };
352
353 assert_eq!(login.username, None);
354 assert_eq!(login.password, None);
355 assert_eq!(login.login_uris.len(), 0);
356 assert_eq!(login.totp, None);
357
358 let passkey = login.fido2_credentials.as_ref().unwrap().first().unwrap();
359 assert_eq!(passkey.credential_id, "b64.6NiHiekW4ZY8vYHa-ucbvA");
360 assert_eq!(passkey.key_type, "public-key");
361 assert_eq!(passkey.key_algorithm, "ECDSA");
362 assert_eq!(passkey.key_curve, "P-256");
363 assert_eq!(
364 passkey.key_value,
365 "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPzvtWYWmIsvqqr3LsZB0K-cbjuhJSGTGziL1LksHAPShRANCAAT-vqHTyEDS9QBNNi2BNLyu6TunubJT_L3G3i7KLpEDhMD15hi24IjGBH0QylJIrvlT4JN2tdRGF436XGc-VoAl"
366 );
367 assert_eq!(passkey.rp_id, "opotonniee.github.io");
368 assert_eq!(
369 passkey.user_handle.as_ref().map(|h| h.to_string()).unwrap(),
370 "YWxleCBtdWxsZXI"
371 );
372 assert_eq!(passkey.user_name, Some("alex muller".to_string()));
373 assert_eq!(passkey.counter, 0);
374 assert_eq!(passkey.rp_name, Some("opotonniee.github.io".to_string()));
375 assert_eq!(passkey.user_display_name, Some("alex muller".to_string()));
376 assert_eq!(passkey.discoverable, "true");
377 assert_eq!(
378 passkey.creation_date,
379 "2024-11-21T09:39:46Z".parse::<DateTime<Utc>>().unwrap()
380 );
381 }
382
383 #[test]
384 fn test_credit_card() {
385 let item = Item {
386 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
387 creation_at: Some(1706613834),
388 modified_at: Some(1706623773),
389 title: "My MasterCard".to_string(),
390 subtitle: None,
391 favorite: None,
392 credentials: vec![Credential::CreditCard(Box::new(CreditCardCredential {
393 number: Some("1234 5678 9012 3456".to_string().into()),
394 full_name: Some("John Doe".to_string().into()),
395 card_type: Some("MasterCard".to_string().into()),
396 verification_number: Some("123".to_string().into()),
397 pin: None,
398 expiry_date: Some(
399 EditableFieldYearMonth {
400 year: 2026,
401 month: Month::January,
402 }
403 .into(),
404 ),
405 valid_from: None,
406 }))],
407 tags: None,
408 extensions: None,
409 scope: None,
410 };
411
412 let ciphers: Vec<ImportingCipher> = parse_item(item);
413 assert_eq!(ciphers.len(), 1);
414 let cipher = ciphers.first().unwrap();
415
416 assert_eq!(cipher.folder_id, None);
417 assert_eq!(cipher.name, "My MasterCard");
418
419 let card = match &cipher.r#type {
420 CipherType::Card(card) => card,
421 _ => panic!("Expected card"),
422 };
423
424 assert_eq!(card.cardholder_name, Some("John Doe".to_string()));
425 assert_eq!(card.exp_month, Some("1".to_string()));
426 assert_eq!(card.exp_year, Some("2026".to_string()));
427 assert_eq!(card.code, Some("123".to_string()));
428 assert_eq!(card.brand, Some("Mastercard".to_string()));
429 assert_eq!(card.number, Some("1234 5678 9012 3456".to_string()));
430 }
431
432 #[test]
433 fn test_totp() {
434 use credential_exchange_format::{OTPHashAlgorithm, TotpCredential};
435
436 let item = Item {
437 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
438 creation_at: Some(1706613834),
439 modified_at: Some(1706623773),
440 title: "My TOTP".to_string(),
441 subtitle: None,
442 favorite: None,
443 credentials: vec![Credential::Totp(Box::new(TotpCredential {
444 secret: "Hello World!".as_bytes().to_vec().into(),
445 period: 30,
446 digits: 6,
447 username: Some("[email protected]".to_string()),
448 algorithm: OTPHashAlgorithm::Sha1,
449 issuer: Some("Example Service".to_string()),
450 }))],
451 tags: None,
452 extensions: None,
453 scope: None,
454 };
455
456 let ciphers: Vec<ImportingCipher> = parse_item(item);
457 assert_eq!(ciphers.len(), 1);
458 let cipher = ciphers.first().unwrap();
459
460 assert_eq!(cipher.folder_id, None);
461 assert_eq!(cipher.name, "My TOTP");
462 assert_eq!(cipher.notes, None);
463 assert!(!cipher.favorite);
464 assert_eq!(cipher.reprompt, 0);
465 assert_eq!(cipher.fields, vec![]);
466
467 let login = match &cipher.r#type {
468 CipherType::Login(login) => login,
469 _ => panic!("Expected login cipher for TOTP"),
470 };
471
472 assert!(login.totp.is_some());
474 let otpauth = login.totp.as_ref().unwrap();
475
476 assert!(
478 otpauth.starts_with("otpauth://totp/Example%20Service:test%40example%2Ecom?secret=")
479 );
480 assert!(otpauth.contains("&issuer=Example%20Service"));
481
482 assert!(!otpauth.contains("&period=30"));
484 assert!(!otpauth.contains("&digits=6"));
485 assert!(!otpauth.contains("&algorithm=SHA1"));
486
487 assert_eq!(login.username, None);
489 assert_eq!(login.password, None);
490 assert_eq!(login.login_uris, vec![]);
491 }
492
493 #[test]
494 fn test_totp_combined_with_basic_auth() {
495 use credential_exchange_format::{BasicAuthCredential, OTPHashAlgorithm, TotpCredential};
496
497 let item = Item {
498 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
499 creation_at: Some(1706613834),
500 modified_at: Some(1706623773),
501 title: "Login with TOTP".to_string(),
502 subtitle: None,
503 favorite: None,
504 credentials: vec![
505 Credential::BasicAuth(Box::new(BasicAuthCredential {
506 username: Some("myuser".to_string().into()),
507 password: Some("mypass".to_string().into()),
508 })),
509 Credential::Totp(Box::new(TotpCredential {
510 secret: "totpkey".as_bytes().to_vec().into(),
511 period: 30,
512 digits: 6,
513 username: Some("totpuser".to_string()),
514 algorithm: OTPHashAlgorithm::Sha1,
515 issuer: Some("Service".to_string()),
516 })),
517 ],
518 tags: None,
519 extensions: None,
520 scope: None,
521 };
522
523 let ciphers: Vec<ImportingCipher> = parse_item(item);
524 assert_eq!(ciphers.len(), 1);
525 let cipher = ciphers.first().unwrap();
526
527 let login = match &cipher.r#type {
528 CipherType::Login(login) => login,
529 _ => panic!("Expected login cipher"),
530 };
531
532 assert_eq!(login.username, Some("myuser".to_string()));
534 assert_eq!(login.password, Some("mypass".to_string()));
535 assert!(login.totp.is_some());
536
537 let otpauth = login.totp.as_ref().unwrap();
538 assert!(otpauth.starts_with("otpauth://totp/Service:totpuser?secret="));
539 assert!(otpauth.contains("&issuer=Service"));
540 }
541
542 #[test]
545 fn test_note_as_part_of_login() {
546 use credential_exchange_format::{BasicAuthCredential, Credential, Item, NoteCredential};
547
548 let item = Item {
549 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
550 creation_at: Some(1706613834),
551 modified_at: Some(1706623773),
552 title: "Login with Note".to_string(),
553 subtitle: None,
554 favorite: None,
555 credentials: vec![
556 Credential::BasicAuth(Box::new(BasicAuthCredential {
557 username: Some("testuser".to_string().into()),
558 password: Some("testpass".to_string().into()),
559 })),
560 Credential::Note(Box::new(NoteCredential {
561 content: "This note should be added to the login cipher."
562 .to_string()
563 .into(),
564 })),
565 ],
566 tags: None,
567 extensions: None,
568 scope: None,
569 };
570
571 let ciphers: Vec<ImportingCipher> = parse_item(item);
572 assert_eq!(ciphers.len(), 1); let cipher = ciphers.first().unwrap();
574
575 assert_eq!(cipher.name, "Login with Note");
576 assert_eq!(
577 cipher.notes,
578 Some("This note should be added to the login cipher.".to_string())
579 );
580
581 match &cipher.r#type {
582 CipherType::Login(_) => (), _ => panic!("Expected Login cipher with note content"),
584 };
585 }
586
587 #[test]
588 fn test_note_as_part_of_api_key() {
589 use credential_exchange_format::{ApiKeyCredential, Credential, Item, NoteCredential};
590
591 let item = Item {
592 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
593 creation_at: Some(1706613834),
594 modified_at: Some(1706623773),
595 title: "API Key with Note".to_string(),
596 subtitle: None,
597 favorite: None,
598 credentials: vec![
599 Credential::ApiKey(Box::new(ApiKeyCredential {
600 key: Some("api-key-12345".to_string().into()),
601 username: Some("api-user".to_string().into()),
602 key_type: Some("Bearer".to_string().into()),
603 url: None,
604 valid_from: None,
605 expiry_date: None,
606 })),
607 Credential::Note(Box::new(NoteCredential {
608 content: "This note should be added to the API key cipher."
609 .to_string()
610 .into(),
611 })),
612 ],
613 tags: None,
614 extensions: None,
615 scope: None,
616 };
617
618 let ciphers: Vec<ImportingCipher> = parse_item(item);
619 assert_eq!(ciphers.len(), 1); let cipher = ciphers.first().unwrap();
621
622 assert_eq!(cipher.name, "API Key with Note");
623 assert_eq!(
624 cipher.notes,
625 Some("This note should be added to the API key cipher.".to_string())
626 );
627
628 match &cipher.r#type {
629 CipherType::SecureNote(_) => (), _ => panic!("Expected SecureNote cipher with note content"),
631 };
632
633 assert!(!cipher.fields.is_empty());
635 }
636
637 #[test]
638 fn test_note_as_part_of_credit_card() {
639 use chrono::Month;
640 use credential_exchange_format::{Credential, CreditCardCredential, Item, NoteCredential};
641
642 let item = Item {
643 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
644 creation_at: Some(1706613834),
645 modified_at: Some(1706623773),
646 title: "Credit Card with Note".to_string(),
647 subtitle: None,
648 favorite: None,
649 credentials: vec![
650 Credential::CreditCard(Box::new(CreditCardCredential {
651 number: Some("1234 5678 9012 3456".to_string().into()),
652 full_name: Some("John Doe".to_string().into()),
653 card_type: Some("Visa".to_string().into()),
654 verification_number: Some("123".to_string().into()),
655 pin: None,
656 expiry_date: Some(
657 credential_exchange_format::EditableFieldYearMonth {
658 year: 2026,
659 month: Month::December,
660 }
661 .into(),
662 ),
663 valid_from: None,
664 })),
665 Credential::Note(Box::new(NoteCredential {
666 content: "This note should be added to the credit card cipher."
667 .to_string()
668 .into(),
669 })),
670 ],
671 tags: None,
672 extensions: None,
673 scope: None,
674 };
675
676 let ciphers: Vec<ImportingCipher> = parse_item(item);
677 assert_eq!(ciphers.len(), 1); let cipher = ciphers.first().unwrap();
679
680 assert_eq!(cipher.name, "Credit Card with Note");
681 assert_eq!(
682 cipher.notes,
683 Some("This note should be added to the credit card cipher.".to_string())
684 );
685
686 match &cipher.r#type {
687 CipherType::Card(_) => (), _ => panic!("Expected Card cipher with note content"),
689 };
690 }
691
692 #[test]
693 fn test_note_as_part_of_wifi() {
694 use credential_exchange_format::{
695 Credential, EditableFieldWifiNetworkSecurityType, Item, NoteCredential, WifiCredential,
696 };
697
698 let item = Item {
699 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
700 creation_at: Some(1706613834),
701 modified_at: Some(1706623773),
702 title: "WiFi with Note".to_string(),
703 subtitle: None,
704 favorite: None,
705 credentials: vec![
706 Credential::Wifi(Box::new(WifiCredential {
707 ssid: Some("MyNetwork".to_string().into()),
708 passphrase: Some("password123".to_string().into()),
709 network_security_type: Some(
710 EditableFieldWifiNetworkSecurityType::Wpa3Personal.into(),
711 ),
712 hidden: Some(false.into()),
713 })),
714 Credential::Note(Box::new(NoteCredential {
715 content: "This note should be added to the WiFi cipher."
716 .to_string()
717 .into(),
718 })),
719 ],
720 tags: None,
721 extensions: None,
722 scope: None,
723 };
724
725 let ciphers: Vec<ImportingCipher> = parse_item(item);
726 assert_eq!(ciphers.len(), 1); let cipher = ciphers.first().unwrap();
728
729 assert_eq!(cipher.name, "WiFi with Note");
730 assert_eq!(
731 cipher.notes,
732 Some("This note should be added to the WiFi cipher.".to_string())
733 );
734
735 match &cipher.r#type {
736 CipherType::SecureNote(_) => (), _ => panic!("Expected SecureNote cipher with note content"),
738 };
739
740 assert!(!cipher.fields.is_empty());
742 }
743
744 #[test]
745 fn test_note_as_part_of_identity() {
746 use credential_exchange_format::{AddressCredential, Credential, Item, NoteCredential};
747
748 let item = Item {
749 id: [0, 1, 2, 3, 4, 5, 6].as_ref().into(),
750 creation_at: Some(1706613834),
751 modified_at: Some(1706623773),
752 title: "Address with Note".to_string(),
753 subtitle: None,
754 favorite: None,
755 credentials: vec![
756 Credential::Address(Box::new(AddressCredential {
757 street_address: Some("123 Main St".to_string().into()),
758 city: Some("Springfield".to_string().into()),
759 territory: Some("CA".to_string().into()),
760 postal_code: Some("12345".to_string().into()),
761 country: Some("US".to_string().into()),
762 tel: Some("+1-555-123-4567".to_string().into()),
763 })),
764 Credential::Note(Box::new(NoteCredential {
765 content: "This note should be added to the address identity cipher."
766 .to_string()
767 .into(),
768 })),
769 ],
770 tags: None,
771 extensions: None,
772 scope: None,
773 };
774
775 let ciphers: Vec<ImportingCipher> = parse_item(item);
776 assert_eq!(ciphers.len(), 1); let cipher = ciphers.first().unwrap();
778
779 assert_eq!(cipher.name, "Address with Note");
780 assert_eq!(
781 cipher.notes,
782 Some("This note should be added to the address identity cipher.".to_string())
783 );
784
785 match &cipher.r#type {
786 CipherType::Identity(_) => (), _ => panic!("Expected Identity cipher"),
788 };
789 }
790}