Skip to main content

bitwarden_exporters/cxf/
identity.rs

1use credential_exchange_format::{
2    AddressCredential, Credential, CustomFieldsCredential, DriversLicenseCredential, EditableField,
3    EditableFieldString, EditableFieldValue, IdentityDocumentCredential, PassportCredential,
4    PersonNameCredential,
5};
6
7use crate::{
8    Field, Identity,
9    cxf::editable_field::{create_editable_field, create_field},
10};
11
12/// Convert address credentials to Identity (no custom fields needed for address)
13/// According to the mapping specification:
14/// - streetAddress: EditableField<"string"> → Identity::address1
15/// - city: EditableField<"string"> → Identity::city
16/// - territory: EditableField<"subdivision-code"> → Identity::state
17/// - country: EditableField<"country-code"> → Identity::country
18/// - tel: EditableField<"string"> → Identity::phone
19/// - postalCode: EditableField<"string"> → Identity::postal_code
20pub(super) fn address_to_identity(address: AddressCredential) -> (Identity, Vec<Field>) {
21    let identity = Identity {
22        address1: address.street_address.map(Into::into),
23        city: address.city.map(Into::into),
24        state: address.territory.map(Into::into),
25        postal_code: address.postal_code.map(Into::into),
26        country: address.country.map(Into::into),
27        phone: address.tel.map(Into::into),
28        ..Default::default()
29    };
30
31    (identity, vec![])
32}
33
34/// Convert passport credentials to Identity and custom fields
35/// According to CXF mapping document:
36/// - passportNumber: EditableField<"string"> → Identity::passport_number
37/// - nationalIdentificationNumber: EditableField<"string"> → Identity::ssn
38/// - fullName: EditableField<"string"> → Identity::first_name + last_name (split)
39/// - All other fields → CustomFields
40pub(super) fn passport_to_identity(passport: PassportCredential) -> (Identity, Vec<Field>) {
41    // Split full name into first and last name if available
42    let (first_name, last_name) = split_name(&passport.full_name);
43
44    let identity = Identity {
45        first_name,
46        last_name,
47        // Map nationalIdentificationNumber to ssn as closest available field
48        ssn: passport.national_identification_number.map(Into::into),
49        passport_number: passport.passport_number.map(Into::into),
50        ..Default::default()
51    };
52
53    // Create custom fields for unmapped data according to CXF mapping document
54    let custom_fields = [
55        passport
56            .issuing_country
57            .map(|issuing_country| create_field(&issuing_country, Some("Issuing Country"))),
58        passport
59            .nationality
60            .map(|nationality| create_field(&nationality, Some("Nationality"))),
61        passport
62            .birth_date
63            .map(|birth_date| create_field(&birth_date, Some("Birth Date"))),
64        passport
65            .birth_place
66            .map(|birth_place| create_field(&birth_place, Some("Birth Place"))),
67        passport.sex.map(|sex| create_field(&sex, Some("Sex"))),
68        passport
69            .issue_date
70            .map(|issue_date| create_field(&issue_date, Some("Issue Date"))),
71        passport
72            .expiry_date
73            .map(|expiry_date| create_field(&expiry_date, Some("Expiry Date"))),
74        passport
75            .issuing_authority
76            .map(|issuing_authority| create_field(&issuing_authority, Some("Issuing Authority"))),
77        passport
78            .passport_type
79            .map(|passport_type| create_field(&passport_type, Some("Passport Type"))),
80    ]
81    .into_iter()
82    .flatten()
83    .collect();
84
85    (identity, custom_fields)
86}
87
88/// Convert person name credentials to Identity and custom fields
89/// According to CXF mapping:
90/// - title: EditableField<"string"> → Identity::title
91/// - given: EditableField<"string"> → Identity::first_name
92/// - given2: EditableField<"string"> → Identity::middle_name
93/// - surname: EditableField<"string"> → Identity::last_name
94/// - surnamePrefix + surname + surname2: combine for complete last name
95/// - credentials: EditableField<"string"> → Identity::company (as professional credentials)
96/// - Other fields → CustomFields
97pub(super) fn person_name_to_identity(person_name: PersonNameCredential) -> (Identity, Vec<Field>) {
98    // Construct complete last name from surnamePrefix, surname, and surname2
99    let last_name = [
100        person_name.surname_prefix.clone(),
101        person_name.surname.clone(),
102        person_name.surname2.clone(),
103    ]
104    .into_iter()
105    .flatten()
106    .map(String::from)
107    .reduce(|acc, part| format!("{acc} {part}"));
108
109    let identity = Identity {
110        title: person_name.title.map(Into::into),
111        first_name: person_name.given.map(Into::into),
112        middle_name: person_name.given2.map(Into::into),
113        last_name,
114        // Map credentials (e.g., "PhD") to company field as professional qualifications
115        company: person_name.credentials.map(Into::into),
116        ..Default::default()
117    };
118
119    // Create custom fields for unmapped data
120    let custom_fields = [
121        person_name
122            .given_informal
123            .map(|given_informal| create_field(&given_informal, Some("Informal Given Name"))),
124        person_name
125            .generation
126            .map(|generation| create_field(&generation, Some("Generation"))),
127    ]
128    .into_iter()
129    .flatten()
130    .collect();
131
132    (identity, custom_fields)
133}
134
135/// Convert drivers license credentials to Identity and custom fields
136/// According to CXF mapping document:
137/// - licenseNumber: EditableField<"string"> → Identity::license_number
138/// - fullName: EditableField<"string"> → Identity::first_name + last_name (split)
139/// - territory: EditableField<"subdivision-code"> → Identity::state
140/// - country: EditableField<"country-code"> → Identity::country
141/// - All other fields → CustomFields
142pub(super) fn drivers_license_to_identity(
143    drivers_license: DriversLicenseCredential,
144) -> (Identity, Vec<Field>) {
145    // Split full name into first and last name if available
146    let (first_name, last_name) = split_name(&drivers_license.full_name);
147
148    let identity = Identity {
149        first_name,
150        last_name,
151        // Map territory (state/province) to state field
152        state: drivers_license.territory.map(Into::into),
153        // Map country to country field
154        country: drivers_license.country.map(Into::into),
155        license_number: drivers_license.license_number.map(Into::into),
156        ..Default::default()
157    };
158
159    // Create custom fields for unmapped data according to CXF mapping document
160    let custom_fields = [
161        drivers_license
162            .birth_date
163            .map(|birth_date| create_field(&birth_date, Some("Birth Date"))),
164        drivers_license
165            .issue_date
166            .map(|issue_date| create_field(&issue_date, Some("Issue Date"))),
167        drivers_license
168            .expiry_date
169            .map(|expiry_date| create_field(&expiry_date, Some("Expiry Date"))),
170        drivers_license
171            .issuing_authority
172            .map(|issuing_authority| create_field(&issuing_authority, Some("Issuing Authority"))),
173        drivers_license
174            .license_class
175            .map(|license_class| create_field(&license_class, Some("License Class"))),
176    ]
177    .into_iter()
178    .flatten()
179    .collect();
180
181    (identity, custom_fields)
182}
183
184/// Convert identity document credentials to Identity and custom fields
185/// According to CXF mapping document: IdentityDocument ↔︎ Identity
186/// Fields are mapped similarly to passport but for general identity documents
187/// - documentNumber: EditableField<"string"> → Identity::passport_number (reusing for general
188///   document number)
189/// - identificationNumber: EditableField<"string"> → Identity::ssn
190/// - fullName: EditableField<"string"> → Identity::first_name + last_name (split)
191/// - All other fields → CustomFields
192pub(super) fn identity_document_to_identity(
193    identity_document: IdentityDocumentCredential,
194) -> (Identity, Vec<Field>) {
195    // Split full name into first and last name if available
196    let (first_name, last_name) = split_name(&identity_document.full_name);
197
198    let identity = Identity {
199        first_name,
200        last_name,
201        // Map identificationNumber to ssn
202        ssn: identity_document.identification_number.map(Into::into),
203        // Map documentNumber to passport_number (reusing for document number)
204        passport_number: identity_document.document_number.map(Into::into),
205        ..Default::default()
206    };
207
208    // Create custom fields for unmapped data according to CXF mapping document
209    let custom_fields = [
210        identity_document
211            .issuing_country
212            .map(|issuing_country| create_field(&issuing_country, Some("Issuing Country"))),
213        identity_document
214            .nationality
215            .map(|nationality| create_field(&nationality, Some("Nationality"))),
216        identity_document
217            .birth_date
218            .map(|birth_date| create_field(&birth_date, Some("Birth Date"))),
219        identity_document
220            .birth_place
221            .map(|birth_place| create_field(&birth_place, Some("Birth Place"))),
222        identity_document
223            .sex
224            .map(|sex| create_field(&sex, Some("Sex"))),
225        identity_document
226            .issue_date
227            .map(|issue_date| create_field(&issue_date, Some("Issue Date"))),
228        identity_document
229            .expiry_date
230            .map(|expiry_date| create_field(&expiry_date, Some("Expiry Date"))),
231        identity_document
232            .issuing_authority
233            .map(|issuing_authority| create_field(&issuing_authority, Some("Issuing Authority"))),
234    ]
235    .into_iter()
236    .flatten()
237    .collect();
238
239    (identity, custom_fields)
240}
241
242fn to_editable_field<T, U>(field: &Option<T>) -> Option<EditableField<U>>
243where
244    T: Clone + Into<EditableField<U>>,
245{
246    field.clone().map(|v| v.into())
247}
248
249fn split_name(
250    full_name: &Option<EditableField<EditableFieldString>>,
251) -> (Option<String>, Option<String>) {
252    full_name.clone().map_or((None, None), |name| {
253        let name = String::from(name);
254        let parts: Vec<&str> = name.split_whitespace().collect();
255        match parts.as_slice() {
256            [] => (None, None),
257            [first] => (Some(first.to_string()), None),
258            [first, rest @ ..] => (Some(first.to_string()), Some(rest.join(" "))),
259        }
260    })
261}
262
263impl From<&Identity> for PersonNameCredential {
264    fn from(identity: &Identity) -> Self {
265        PersonNameCredential {
266            title: to_editable_field(&identity.title),
267            given: to_editable_field(&identity.first_name),
268            given_informal: None,
269            given2: to_editable_field(&identity.middle_name),
270            surname_prefix: None,
271            surname: to_editable_field(&identity.last_name),
272            surname2: None,
273            credentials: to_editable_field(&identity.company),
274            generation: None,
275            // Note: Can't use ..Default::default() - not implemented in current CXF version
276        }
277    }
278}
279
280impl From<&Identity> for AddressCredential {
281    fn from(identity: &Identity) -> Self {
282        // Combine address lines with newlines as per CXF spec
283        let street_address = {
284            let address_lines: Vec<&str> =
285                [&identity.address1, &identity.address2, &identity.address3]
286                    .into_iter()
287                    .filter_map(|addr| addr.as_deref())
288                    .collect();
289
290            if address_lines.is_empty() {
291                None
292            } else {
293                Some(address_lines.join("\n"))
294            }
295        };
296
297        AddressCredential {
298            street_address: street_address.map(|v| v.into()),
299            city: to_editable_field(&identity.city),
300            territory: to_editable_field(&identity.state),
301            country: to_editable_field(&identity.country),
302            tel: to_editable_field(&identity.phone),
303            postal_code: to_editable_field(&identity.postal_code),
304        }
305    }
306}
307
308impl From<&Identity> for PassportCredential {
309    fn from(identity: &Identity) -> Self {
310        let full_name = combine_name(
311            &identity.first_name,
312            &identity.middle_name,
313            &identity.last_name,
314        );
315
316        PassportCredential {
317            issuing_country: to_editable_field(&identity.country),
318            nationality: None,
319            full_name: full_name.map(|v| v.into()),
320            birth_date: None,
321            birth_place: None,
322            sex: None,
323            issue_date: None,
324            expiry_date: None,
325            issuing_authority: None,
326            passport_type: None,
327            passport_number: to_editable_field(&identity.passport_number),
328            national_identification_number: to_editable_field(&identity.ssn),
329            // Note: Can't use ..Default::default() - not implemented in current CXF version
330        }
331    }
332}
333
334impl From<&Identity> for DriversLicenseCredential {
335    fn from(identity: &Identity) -> Self {
336        let full_name = combine_name(
337            &identity.first_name,
338            &identity.middle_name,
339            &identity.last_name,
340        );
341
342        DriversLicenseCredential {
343            full_name: full_name.map(|v| v.into()),
344            birth_date: None,
345            issue_date: None,
346            expiry_date: None,
347            issuing_authority: None,
348            territory: to_editable_field(&identity.state),
349            country: to_editable_field(&identity.country),
350            license_number: to_editable_field(&identity.license_number),
351            license_class: None,
352            // Note: Can't use ..Default::default() - not implemented in current CXF version
353        }
354    }
355}
356
357impl From<&Identity> for IdentityDocumentCredential {
358    fn from(identity: &Identity) -> Self {
359        let full_name = combine_name(
360            &identity.first_name,
361            &identity.middle_name,
362            &identity.last_name,
363        );
364
365        IdentityDocumentCredential {
366            issuing_country: to_editable_field(&identity.country),
367            document_number: None,
368            identification_number: to_editable_field(&identity.ssn),
369            nationality: None,
370            full_name: full_name.map(|v| v.into()),
371            birth_date: None,
372            birth_place: None,
373            sex: None,
374            issue_date: None,
375            expiry_date: None,
376            issuing_authority: None,
377            // Note: Can't use ..Default::default() - not implemented in current CXF version
378        }
379    }
380}
381
382impl From<Identity> for Vec<Credential> {
383    fn from(identity: Identity) -> Self {
384        let mut credentials = vec![];
385
386        // Helper to check if any name fields are present
387        let has_name_fields = identity.title.is_some()
388            || identity.first_name.is_some()
389            || identity.middle_name.is_some()
390            || identity.last_name.is_some()
391            || identity.company.is_some();
392
393        // Helper to check if any address fields are present
394        let has_address_fields = identity.address1.is_some()
395            || identity.city.is_some()
396            || identity.state.is_some()
397            || identity.country.is_some()
398            || identity.phone.is_some()
399            || identity.postal_code.is_some();
400
401        // Create PersonName credential only if name-related fields are present
402        if has_name_fields {
403            credentials.push(Credential::PersonName(Box::new((&identity).into())));
404        }
405
406        // Create Address credential only if address fields are present
407        if has_address_fields {
408            credentials.push(Credential::Address(Box::new((&identity).into())));
409        }
410
411        // Create Passport credential if passport number is present
412        if identity.passport_number.is_some() {
413            credentials.push(Credential::Passport(Box::new((&identity).into())));
414        }
415
416        // Create DriversLicense credential if license number is present
417        if identity.license_number.is_some() {
418            credentials.push(Credential::DriversLicense(Box::new((&identity).into())));
419        }
420
421        // Create IdentityDocument credential if SSN is present
422        if identity.ssn.is_some() {
423            credentials.push(Credential::IdentityDocument(Box::new((&identity).into())));
424        }
425
426        // Handle unmapped Identity fields as custom fields
427        let custom_fields: Vec<EditableFieldValue> = [
428            identity.email.as_ref().map(|email| {
429                EditableFieldValue::String(create_editable_field(
430                    "Email".to_string(),
431                    EditableFieldString(email.clone()),
432                ))
433            }),
434            identity.username.as_ref().map(|username| {
435                EditableFieldValue::String(create_editable_field(
436                    "Username".to_string(),
437                    EditableFieldString(username.clone()),
438                ))
439            }),
440        ]
441        .into_iter()
442        .flatten()
443        .collect();
444
445        // Add CustomFields credential if there are any unmapped fields
446        if !custom_fields.is_empty() {
447            credentials.push(Credential::CustomFields(Box::new(CustomFieldsCredential {
448                id: None,
449                label: None,
450                fields: custom_fields,
451                extensions: vec![],
452            })));
453        }
454
455        credentials
456    }
457}
458
459pub(crate) fn combine_name(
460    first: &Option<String>,
461    middle: &Option<String>,
462    last: &Option<String>,
463) -> Option<String> {
464    let parts: Vec<&str> = [first.as_deref(), middle.as_deref(), last.as_deref()]
465        .into_iter()
466        .flatten()
467        .collect();
468
469    if parts.is_empty() {
470        None
471    } else {
472        Some(parts.join(" "))
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use credential_exchange_format::Credential;
479
480    use super::*;
481
482    #[test]
483    fn test_split_name_none() {
484        let full_name = None;
485        let (first, last) = split_name(&full_name);
486        assert_eq!(first, None);
487        assert_eq!(last, None);
488    }
489
490    #[test]
491    fn test_split_name_empty_string() {
492        let full_name = Some(EditableField {
493            value: EditableFieldString("".to_string()).into(),
494            label: None,
495            id: None,
496            extensions: None,
497        });
498        let (first, last) = split_name(&full_name);
499        assert_eq!(first, None);
500        assert_eq!(last, None);
501    }
502
503    #[test]
504    fn test_split_name_whitespace_only() {
505        let full_name = Some(EditableField {
506            value: EditableFieldString("   \t\n  ".to_string()).into(),
507            label: None,
508            id: None,
509            extensions: None,
510        });
511        let (first, last) = split_name(&full_name);
512        assert_eq!(first, None);
513        assert_eq!(last, None);
514    }
515
516    #[test]
517    fn test_split_name_single_name() {
518        let full_name = Some(EditableField {
519            value: EditableFieldString("John".to_string()).into(),
520            label: None,
521            id: None,
522            extensions: None,
523        });
524        let (first, last) = split_name(&full_name);
525        assert_eq!(first, Some("John".to_string()));
526        assert_eq!(last, None);
527    }
528
529    #[test]
530    fn test_split_name_single_name_with_whitespace() {
531        let full_name = Some(EditableField {
532            value: EditableFieldString("  John  ".to_string()).into(),
533            label: None,
534            id: None,
535            extensions: None,
536        });
537        let (first, last) = split_name(&full_name);
538        assert_eq!(first, Some("John".to_string()));
539        assert_eq!(last, None);
540    }
541
542    #[test]
543    fn test_split_name_first_last() {
544        let full_name = Some(EditableField {
545            value: EditableFieldString("John Doe".to_string()).into(),
546            label: None,
547            id: None,
548            extensions: None,
549        });
550        let (first, last) = split_name(&full_name);
551        assert_eq!(first, Some("John".to_string()));
552        assert_eq!(last, Some("Doe".to_string()));
553    }
554
555    #[test]
556    fn test_split_name_first_middle_last() {
557        let full_name = Some(EditableField {
558            value: EditableFieldString("John Michael Doe".to_string()).into(),
559            label: None,
560            id: None,
561            extensions: None,
562        });
563        let (first, last) = split_name(&full_name);
564        assert_eq!(first, Some("John".to_string()));
565        assert_eq!(last, Some("Michael Doe".to_string()));
566    }
567
568    #[test]
569    fn test_split_name_multiple_middle_names() {
570        let full_name = Some(EditableField {
571            value: EditableFieldString("John Michael Andrew Doe".to_string()).into(),
572            label: None,
573            id: None,
574            extensions: None,
575        });
576        let (first, last) = split_name(&full_name);
577        assert_eq!(first, Some("John".to_string()));
578        assert_eq!(last, Some("Michael Andrew Doe".to_string()));
579    }
580
581    #[test]
582    fn test_split_name_complex_surname() {
583        let full_name = Some(EditableField {
584            value: EditableFieldString("Jane van der Berg".to_string()).into(),
585            label: None,
586            id: None,
587            extensions: None,
588        });
589        let (first, last) = split_name(&full_name);
590        assert_eq!(first, Some("Jane".to_string()));
591        assert_eq!(last, Some("van der Berg".to_string()));
592    }
593
594    #[test]
595    fn test_split_name_hyphenated_surname() {
596        let full_name = Some(EditableField {
597            value: EditableFieldString("Mary Smith-Johnson".to_string()).into(),
598            label: None,
599            id: None,
600            extensions: None,
601        });
602        let (first, last) = split_name(&full_name);
603        assert_eq!(first, Some("Mary".to_string()));
604        assert_eq!(last, Some("Smith-Johnson".to_string()));
605    }
606
607    #[test]
608    fn test_split_name_extra_whitespace() {
609        let full_name = Some(EditableField {
610            value: EditableFieldString("  John   Michael   Doe  ".to_string()).into(),
611            label: None,
612            id: None,
613            extensions: None,
614        });
615        let (first, last) = split_name(&full_name);
616        assert_eq!(first, Some("John".to_string()));
617        assert_eq!(last, Some("Michael Doe".to_string()));
618    }
619
620    #[test]
621    fn test_split_name_special_characters() {
622        let full_name = Some(EditableField {
623            value: EditableFieldString("José María González".to_string()).into(),
624            label: None,
625            id: None,
626            extensions: None,
627        });
628        let (first, last) = split_name(&full_name);
629        assert_eq!(first, Some("José".to_string()));
630        assert_eq!(last, Some("María González".to_string()));
631    }
632
633    #[test]
634    fn test_split_name_single_character_names() {
635        let full_name = Some(EditableField {
636            value: EditableFieldString("A B C".to_string()).into(),
637            label: None,
638            id: None,
639            extensions: None,
640        });
641        let (first, last) = split_name(&full_name);
642        assert_eq!(first, Some("A".to_string()));
643        assert_eq!(last, Some("B C".to_string()));
644    }
645
646    #[test]
647    fn test_identity_to_credentials() {
648        let identity = Identity {
649            title: Some("Dr.".to_string()),
650            first_name: Some("John".to_string()),
651            middle_name: Some("Michael".to_string()),
652            last_name: Some("Doe".to_string()),
653            address1: Some("123 Main St".to_string()),
654            address2: Some("Apt 456".to_string()),
655            address3: None,
656            city: Some("Anytown".to_string()),
657            state: Some("CA".to_string()),
658            postal_code: Some("12345".to_string()),
659            country: Some("US".to_string()),
660            company: Some("PhD".to_string()),
661            email: Some("[email protected]".to_string()),
662            phone: Some("+1234567890".to_string()),
663            ssn: Some("123-45-6789".to_string()),
664            username: Some("johndoe".to_string()),
665            passport_number: Some("P123456789".to_string()),
666            license_number: Some("DL123456".to_string()),
667        };
668
669        let credentials: Vec<Credential> = identity.into();
670
671        // Should create PersonName, Address, Passport, DriversLicense, IdentityDocument, and
672        // CustomFields credentials
673        assert_eq!(credentials.len(), 6);
674
675        // Check PersonName credential
676        if let Credential::PersonName(person_name) = &credentials[0] {
677            assert_eq!(
678                person_name
679                    .title
680                    .as_ref()
681                    .unwrap()
682                    .value
683                    .as_expected()
684                    .unwrap()
685                    .0,
686                "Dr."
687            );
688            assert_eq!(
689                person_name
690                    .given
691                    .as_ref()
692                    .unwrap()
693                    .value
694                    .as_expected()
695                    .unwrap()
696                    .0,
697                "John"
698            );
699            assert_eq!(
700                person_name
701                    .given2
702                    .as_ref()
703                    .unwrap()
704                    .value
705                    .as_expected()
706                    .unwrap()
707                    .0,
708                "Michael"
709            );
710            assert_eq!(
711                person_name
712                    .surname
713                    .as_ref()
714                    .unwrap()
715                    .value
716                    .as_expected()
717                    .unwrap()
718                    .0,
719                "Doe"
720            );
721            assert_eq!(
722                person_name
723                    .credentials
724                    .as_ref()
725                    .unwrap()
726                    .value
727                    .as_expected()
728                    .unwrap()
729                    .0,
730                "PhD"
731            );
732        } else {
733            panic!("Expected PersonName credential");
734        }
735
736        // Check Address credential
737        if let Credential::Address(address) = &credentials[1] {
738            assert_eq!(
739                address
740                    .street_address
741                    .as_ref()
742                    .unwrap()
743                    .value
744                    .as_expected()
745                    .unwrap()
746                    .0,
747                "123 Main St\nApt 456"
748            );
749            assert_eq!(
750                address
751                    .city
752                    .as_ref()
753                    .unwrap()
754                    .value
755                    .as_expected()
756                    .unwrap()
757                    .0,
758                "Anytown"
759            );
760            assert_eq!(
761                address
762                    .territory
763                    .as_ref()
764                    .unwrap()
765                    .value
766                    .as_expected()
767                    .unwrap()
768                    .0,
769                "CA"
770            );
771            assert_eq!(
772                address
773                    .country
774                    .as_ref()
775                    .unwrap()
776                    .value
777                    .as_expected()
778                    .unwrap()
779                    .0,
780                "US"
781            );
782            assert_eq!(
783                address.tel.as_ref().unwrap().value.as_expected().unwrap().0,
784                "+1234567890"
785            );
786            assert_eq!(
787                address
788                    .postal_code
789                    .as_ref()
790                    .unwrap()
791                    .value
792                    .as_expected()
793                    .unwrap()
794                    .0,
795                "12345"
796            );
797        } else {
798            panic!("Expected Address credential");
799        }
800
801        // Check Passport credential
802        if let Credential::Passport(passport) = &credentials[2] {
803            assert_eq!(
804                passport
805                    .passport_number
806                    .as_ref()
807                    .unwrap()
808                    .value
809                    .as_expected()
810                    .unwrap()
811                    .0,
812                "P123456789"
813            );
814            assert_eq!(
815                passport
816                    .full_name
817                    .as_ref()
818                    .unwrap()
819                    .value
820                    .as_expected()
821                    .unwrap()
822                    .0,
823                "John Michael Doe"
824            );
825            assert_eq!(
826                passport
827                    .national_identification_number
828                    .as_ref()
829                    .unwrap()
830                    .value
831                    .as_expected()
832                    .unwrap()
833                    .0,
834                "123-45-6789"
835            );
836            assert_eq!(
837                passport
838                    .issuing_country
839                    .as_ref()
840                    .unwrap()
841                    .value
842                    .as_expected()
843                    .unwrap()
844                    .0,
845                "US"
846            );
847        } else {
848            panic!("Expected Passport credential");
849        }
850
851        // Check DriversLicense credential
852        if let Credential::DriversLicense(license) = &credentials[3] {
853            assert_eq!(
854                license
855                    .license_number
856                    .as_ref()
857                    .unwrap()
858                    .value
859                    .as_expected()
860                    .unwrap()
861                    .0,
862                "DL123456"
863            );
864            assert_eq!(
865                license
866                    .full_name
867                    .as_ref()
868                    .unwrap()
869                    .value
870                    .as_expected()
871                    .unwrap()
872                    .0,
873                "John Michael Doe"
874            );
875            assert_eq!(
876                license
877                    .territory
878                    .as_ref()
879                    .unwrap()
880                    .value
881                    .as_expected()
882                    .unwrap()
883                    .0,
884                "CA"
885            );
886            assert_eq!(
887                license
888                    .country
889                    .as_ref()
890                    .unwrap()
891                    .value
892                    .as_expected()
893                    .unwrap()
894                    .0,
895                "US"
896            );
897        } else {
898            panic!("Expected DriversLicense credential");
899        }
900
901        // Check IdentityDocument credential
902        if let Credential::IdentityDocument(identity_doc) = &credentials[4] {
903            assert_eq!(
904                identity_doc
905                    .identification_number
906                    .as_ref()
907                    .unwrap()
908                    .value
909                    .as_expected()
910                    .unwrap()
911                    .0,
912                "123-45-6789"
913            );
914            assert_eq!(
915                identity_doc
916                    .full_name
917                    .as_ref()
918                    .unwrap()
919                    .value
920                    .as_expected()
921                    .unwrap()
922                    .0,
923                "John Michael Doe"
924            );
925        } else {
926            panic!("Expected IdentityDocument credential");
927        }
928
929        // Check CustomFields credential
930        if let Credential::CustomFields(custom_fields) = &credentials[5] {
931            assert_eq!(custom_fields.fields.len(), 2); // email, username
932
933            // Check email field
934            let email_field = &custom_fields.fields[0];
935            if let EditableFieldValue::String(email_field) = email_field {
936                assert_eq!(email_field.label.as_ref().unwrap(), "Email");
937                assert_eq!(
938                    email_field.value.as_expected().unwrap().0,
939                    "[email protected]"
940                );
941            } else {
942                panic!("Expected email field to be of type String");
943            }
944
945            // Check username field
946            let username_field = &custom_fields.fields[1];
947            if let EditableFieldValue::String(username_field) = username_field {
948                assert_eq!(username_field.label.as_ref().unwrap(), "Username");
949                assert_eq!(username_field.value.as_expected().unwrap().0, "johndoe");
950            } else {
951                panic!("Expected username field to be of type String");
952            }
953        } else {
954            panic!("Expected CustomFields credential");
955        }
956    }
957
958    #[test]
959    fn test_identity_minimal_fields() {
960        let identity = Identity {
961            first_name: Some("Jane".to_string()),
962            last_name: Some("Smith".to_string()),
963            ..Default::default()
964        };
965
966        let credentials: Vec<Credential> = identity.into();
967
968        // Should only create PersonName credential
969        assert_eq!(credentials.len(), 1);
970
971        if let Credential::PersonName(person_name) = &credentials[0] {
972            assert_eq!(
973                person_name
974                    .given
975                    .as_ref()
976                    .unwrap()
977                    .value
978                    .as_expected()
979                    .unwrap()
980                    .0,
981                "Jane"
982            );
983            assert_eq!(
984                person_name
985                    .surname
986                    .as_ref()
987                    .unwrap()
988                    .value
989                    .as_expected()
990                    .unwrap()
991                    .0,
992                "Smith"
993            );
994            assert!(person_name.title.is_none());
995            assert!(person_name.given2.is_none());
996        } else {
997            panic!("Expected PersonName credential");
998        }
999    }
1000
1001    #[test]
1002    fn test_identity_license_only() {
1003        let identity = Identity {
1004            first_name: Some("Alice".to_string()),
1005            license_number: Some("LIC123456".to_string()),
1006            state: Some("NY".to_string()),
1007            ..Default::default()
1008        };
1009
1010        let credentials: Vec<Credential> = identity.into();
1011
1012        // Should create PersonName, Address (due to state), and DriversLicense credentials
1013        assert_eq!(credentials.len(), 3);
1014
1015        // Check PersonName credential
1016        if let Credential::PersonName(person_name) = &credentials[0] {
1017            assert_eq!(
1018                person_name
1019                    .given
1020                    .as_ref()
1021                    .unwrap()
1022                    .value
1023                    .as_expected()
1024                    .unwrap()
1025                    .0,
1026                "Alice"
1027            );
1028        } else {
1029            panic!("Expected PersonName credential");
1030        }
1031
1032        // Check Address credential
1033        if let Credential::Address(address) = &credentials[1] {
1034            assert_eq!(
1035                address
1036                    .territory
1037                    .as_ref()
1038                    .unwrap()
1039                    .value
1040                    .as_expected()
1041                    .unwrap()
1042                    .0,
1043                "NY"
1044            );
1045        } else {
1046            panic!("Expected Address credential");
1047        }
1048
1049        // Check DriversLicense credential
1050        if let Credential::DriversLicense(license) = &credentials[2] {
1051            assert_eq!(
1052                license
1053                    .license_number
1054                    .as_ref()
1055                    .unwrap()
1056                    .value
1057                    .as_expected()
1058                    .unwrap()
1059                    .0,
1060                "LIC123456"
1061            );
1062            assert_eq!(
1063                license
1064                    .full_name
1065                    .as_ref()
1066                    .unwrap()
1067                    .value
1068                    .as_expected()
1069                    .unwrap()
1070                    .0,
1071                "Alice"
1072            );
1073            assert_eq!(
1074                license
1075                    .territory
1076                    .as_ref()
1077                    .unwrap()
1078                    .value
1079                    .as_expected()
1080                    .unwrap()
1081                    .0,
1082                "NY"
1083            );
1084        } else {
1085            panic!("Expected DriversLicense credential");
1086        }
1087    }
1088
1089    #[test]
1090    fn test_identity_ssn_only() {
1091        let identity = Identity {
1092            first_name: Some("Bob".to_string()),
1093            ssn: Some("987-65-4321".to_string()),
1094            ..Default::default()
1095        };
1096
1097        let credentials: Vec<Credential> = identity.into();
1098
1099        // Should create PersonName and IdentityDocument credentials
1100        assert_eq!(credentials.len(), 2);
1101
1102        if let Credential::IdentityDocument(identity_doc) = &credentials[1] {
1103            assert_eq!(
1104                identity_doc
1105                    .identification_number
1106                    .as_ref()
1107                    .unwrap()
1108                    .value
1109                    .as_expected()
1110                    .unwrap()
1111                    .0,
1112                "987-65-4321"
1113            );
1114            assert_eq!(
1115                identity_doc
1116                    .full_name
1117                    .as_ref()
1118                    .unwrap()
1119                    .value
1120                    .as_expected()
1121                    .unwrap()
1122                    .0,
1123                "Bob"
1124            );
1125        } else {
1126            panic!("Expected IdentityDocument credential");
1127        }
1128    }
1129
1130    #[test]
1131    fn test_identity_empty() {
1132        let identity = Identity::default();
1133
1134        let credentials: Vec<Credential> = identity.into();
1135
1136        // Should create no credentials for completely empty identity
1137        assert_eq!(credentials.len(), 0);
1138    }
1139
1140    #[test]
1141    fn test_combine_name_helper() {
1142        assert_eq!(
1143            combine_name(
1144                &Some("John".to_string()),
1145                &Some("Michael".to_string()),
1146                &Some("Doe".to_string())
1147            ),
1148            Some("John Michael Doe".to_string())
1149        );
1150
1151        assert_eq!(
1152            combine_name(&Some("Jane".to_string()), &None, &Some("Smith".to_string())),
1153            Some("Jane Smith".to_string())
1154        );
1155
1156        assert_eq!(
1157            combine_name(&Some("Bob".to_string()), &None, &None),
1158            Some("Bob".to_string())
1159        );
1160
1161        assert_eq!(combine_name(&None, &None, &None), None);
1162    }
1163}