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    cxf::editable_field::{create_editable_field, create_field},
9    Field, Identity,
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.as_ref(),
101        person_name.surname.as_ref(),
102        person_name.surname2.as_ref(),
103    ]
104    .into_iter()
105    .flatten()
106    .map(|field| field.value.0.clone())
107    .collect::<Vec<_>>()
108    .into_iter()
109    .reduce(|acc, part| format!("{acc} {part}"));
110
111    let identity = Identity {
112        title: person_name.title.map(Into::into),
113        first_name: person_name.given.map(Into::into),
114        middle_name: person_name.given2.map(Into::into),
115        last_name,
116        // Map credentials (e.g., "PhD") to company field as professional qualifications
117        company: person_name.credentials.map(Into::into),
118        ..Default::default()
119    };
120
121    // Create custom fields for unmapped data
122    let custom_fields = [
123        person_name
124            .given_informal
125            .map(|given_informal| create_field(&given_informal, Some("Informal Given Name"))),
126        person_name
127            .generation
128            .map(|generation| create_field(&generation, Some("Generation"))),
129    ]
130    .into_iter()
131    .flatten()
132    .collect();
133
134    (identity, custom_fields)
135}
136
137/// Convert drivers license credentials to Identity and custom fields
138/// According to CXF mapping document:
139/// - licenseNumber: EditableField<"string"> → Identity::license_number
140/// - fullName: EditableField<"string"> → Identity::first_name + last_name (split)
141/// - territory: EditableField<"subdivision-code"> → Identity::state
142/// - country: EditableField<"country-code"> → Identity::country
143/// - All other fields → CustomFields
144pub(super) fn drivers_license_to_identity(
145    drivers_license: DriversLicenseCredential,
146) -> (Identity, Vec<Field>) {
147    // Split full name into first and last name if available
148    let (first_name, last_name) = split_name(&drivers_license.full_name);
149
150    let identity = Identity {
151        first_name,
152        last_name,
153        // Map territory (state/province) to state field
154        state: drivers_license.territory.map(Into::into),
155        // Map country to country field
156        country: drivers_license.country.map(Into::into),
157        license_number: drivers_license.license_number.map(Into::into),
158        ..Default::default()
159    };
160
161    // Create custom fields for unmapped data according to CXF mapping document
162    let custom_fields = [
163        drivers_license
164            .birth_date
165            .map(|birth_date| create_field(&birth_date, Some("Birth Date"))),
166        drivers_license
167            .issue_date
168            .map(|issue_date| create_field(&issue_date, Some("Issue Date"))),
169        drivers_license
170            .expiry_date
171            .map(|expiry_date| create_field(&expiry_date, Some("Expiry Date"))),
172        drivers_license
173            .issuing_authority
174            .map(|issuing_authority| create_field(&issuing_authority, Some("Issuing Authority"))),
175        drivers_license
176            .license_class
177            .map(|license_class| create_field(&license_class, Some("License Class"))),
178    ]
179    .into_iter()
180    .flatten()
181    .collect();
182
183    (identity, custom_fields)
184}
185
186/// Convert identity document credentials to Identity and custom fields
187/// According to CXF mapping document: IdentityDocument ↔︎ Identity
188/// Fields are mapped similarly to passport but for general identity documents
189/// - documentNumber: EditableField<"string"> → Identity::passport_number (reusing for general
190///   document number)
191/// - identificationNumber: EditableField<"string"> → Identity::ssn
192/// - fullName: EditableField<"string"> → Identity::first_name + last_name (split)
193/// - All other fields → CustomFields
194pub(super) fn identity_document_to_identity(
195    identity_document: IdentityDocumentCredential,
196) -> (Identity, Vec<Field>) {
197    // Split full name into first and last name if available
198    let (first_name, last_name) = split_name(&identity_document.full_name);
199
200    let identity = Identity {
201        first_name,
202        last_name,
203        // Map identificationNumber to ssn
204        ssn: identity_document.identification_number.map(Into::into),
205        // Map documentNumber to passport_number (reusing for document number)
206        passport_number: identity_document.document_number.map(Into::into),
207        ..Default::default()
208    };
209
210    // Create custom fields for unmapped data according to CXF mapping document
211    let custom_fields = [
212        identity_document
213            .issuing_country
214            .map(|issuing_country| create_field(&issuing_country, Some("Issuing Country"))),
215        identity_document
216            .nationality
217            .map(|nationality| create_field(&nationality, Some("Nationality"))),
218        identity_document
219            .birth_date
220            .map(|birth_date| create_field(&birth_date, Some("Birth Date"))),
221        identity_document
222            .birth_place
223            .map(|birth_place| create_field(&birth_place, Some("Birth Place"))),
224        identity_document
225            .sex
226            .map(|sex| create_field(&sex, Some("Sex"))),
227        identity_document
228            .issue_date
229            .map(|issue_date| create_field(&issue_date, Some("Issue Date"))),
230        identity_document
231            .expiry_date
232            .map(|expiry_date| create_field(&expiry_date, Some("Expiry Date"))),
233        identity_document
234            .issuing_authority
235            .map(|issuing_authority| create_field(&issuing_authority, Some("Issuing Authority"))),
236    ]
237    .into_iter()
238    .flatten()
239    .collect();
240
241    (identity, custom_fields)
242}
243
244fn to_editable_field<T, U>(field: &Option<T>) -> Option<EditableField<U>>
245where
246    T: Clone + Into<EditableField<U>>,
247{
248    field.clone().map(|v| v.into())
249}
250
251fn split_name(
252    full_name: &Option<EditableField<EditableFieldString>>,
253) -> (Option<String>, Option<String>) {
254    full_name.as_ref().map_or((None, None), |name| {
255        let parts: Vec<&str> = name.value.0.split_whitespace().collect();
256        match parts.as_slice() {
257            [] => (None, None),
258            [first] => (Some(first.to_string()), None),
259            [first, rest @ ..] => (Some(first.to_string()), Some(rest.join(" "))),
260        }
261    })
262}
263
264impl From<&Identity> for PersonNameCredential {
265    fn from(identity: &Identity) -> Self {
266        PersonNameCredential {
267            title: to_editable_field(&identity.title),
268            given: to_editable_field(&identity.first_name),
269            given_informal: None,
270            given2: to_editable_field(&identity.middle_name),
271            surname_prefix: None,
272            surname: to_editable_field(&identity.last_name),
273            surname2: None,
274            credentials: to_editable_field(&identity.company),
275            generation: None,
276            // Note: Can't use ..Default::default() - not implemented in current CXF version
277        }
278    }
279}
280
281impl From<&Identity> for AddressCredential {
282    fn from(identity: &Identity) -> Self {
283        // Combine address lines with newlines as per CXF spec
284        let street_address = {
285            let address_lines: Vec<&str> =
286                [&identity.address1, &identity.address2, &identity.address3]
287                    .into_iter()
288                    .filter_map(|addr| addr.as_deref())
289                    .collect();
290
291            if address_lines.is_empty() {
292                None
293            } else {
294                Some(address_lines.join("\n"))
295            }
296        };
297
298        AddressCredential {
299            street_address: street_address.map(|v| v.into()),
300            city: to_editable_field(&identity.city),
301            territory: to_editable_field(&identity.state),
302            country: to_editable_field(&identity.country),
303            tel: to_editable_field(&identity.phone),
304            postal_code: to_editable_field(&identity.postal_code),
305        }
306    }
307}
308
309impl From<&Identity> for PassportCredential {
310    fn from(identity: &Identity) -> Self {
311        let full_name = combine_name(
312            &identity.first_name,
313            &identity.middle_name,
314            &identity.last_name,
315        );
316
317        PassportCredential {
318            issuing_country: to_editable_field(&identity.country),
319            nationality: None,
320            full_name: full_name.map(|v| v.into()),
321            birth_date: None,
322            birth_place: None,
323            sex: None,
324            issue_date: None,
325            expiry_date: None,
326            issuing_authority: None,
327            passport_type: None,
328            passport_number: to_editable_field(&identity.passport_number),
329            national_identification_number: to_editable_field(&identity.ssn),
330            // Note: Can't use ..Default::default() - not implemented in current CXF version
331        }
332    }
333}
334
335impl From<&Identity> for DriversLicenseCredential {
336    fn from(identity: &Identity) -> Self {
337        let full_name = combine_name(
338            &identity.first_name,
339            &identity.middle_name,
340            &identity.last_name,
341        );
342
343        DriversLicenseCredential {
344            full_name: full_name.map(|v| v.into()),
345            birth_date: None,
346            issue_date: None,
347            expiry_date: None,
348            issuing_authority: None,
349            territory: to_editable_field(&identity.state),
350            country: to_editable_field(&identity.country),
351            license_number: to_editable_field(&identity.license_number),
352            license_class: None,
353            // Note: Can't use ..Default::default() - not implemented in current CXF version
354        }
355    }
356}
357
358impl From<&Identity> for IdentityDocumentCredential {
359    fn from(identity: &Identity) -> Self {
360        let full_name = combine_name(
361            &identity.first_name,
362            &identity.middle_name,
363            &identity.last_name,
364        );
365
366        IdentityDocumentCredential {
367            issuing_country: to_editable_field(&identity.country),
368            document_number: None,
369            identification_number: to_editable_field(&identity.ssn),
370            nationality: None,
371            full_name: full_name.map(|v| v.into()),
372            birth_date: None,
373            birth_place: None,
374            sex: None,
375            issue_date: None,
376            expiry_date: None,
377            issuing_authority: None,
378            // Note: Can't use ..Default::default() - not implemented in current CXF version
379        }
380    }
381}
382
383impl From<Identity> for Vec<Credential> {
384    fn from(identity: Identity) -> Self {
385        let mut credentials = vec![];
386
387        // Helper to check if any name fields are present
388        let has_name_fields = identity.title.is_some()
389            || identity.first_name.is_some()
390            || identity.middle_name.is_some()
391            || identity.last_name.is_some()
392            || identity.company.is_some();
393
394        // Helper to check if any address fields are present
395        let has_address_fields = identity.address1.is_some()
396            || identity.city.is_some()
397            || identity.state.is_some()
398            || identity.country.is_some()
399            || identity.phone.is_some()
400            || identity.postal_code.is_some();
401
402        // Create PersonName credential only if name-related fields are present
403        if has_name_fields {
404            credentials.push(Credential::PersonName(Box::new((&identity).into())));
405        }
406
407        // Create Address credential only if address fields are present
408        if has_address_fields {
409            credentials.push(Credential::Address(Box::new((&identity).into())));
410        }
411
412        // Create Passport credential if passport number is present
413        if identity.passport_number.is_some() {
414            credentials.push(Credential::Passport(Box::new((&identity).into())));
415        }
416
417        // Create DriversLicense credential if license number is present
418        if identity.license_number.is_some() {
419            credentials.push(Credential::DriversLicense(Box::new((&identity).into())));
420        }
421
422        // Create IdentityDocument credential if SSN is present
423        if identity.ssn.is_some() {
424            credentials.push(Credential::IdentityDocument(Box::new((&identity).into())));
425        }
426
427        // Handle unmapped Identity fields as custom fields
428        let custom_fields: Vec<EditableFieldValue> = [
429            identity.email.as_ref().map(|email| {
430                EditableFieldValue::String(create_editable_field(
431                    "Email".to_string(),
432                    EditableFieldString(email.clone()),
433                ))
434            }),
435            identity.username.as_ref().map(|username| {
436                EditableFieldValue::String(create_editable_field(
437                    "Username".to_string(),
438                    EditableFieldString(username.clone()),
439                ))
440            }),
441        ]
442        .into_iter()
443        .flatten()
444        .collect();
445
446        // Add CustomFields credential if there are any unmapped fields
447        if !custom_fields.is_empty() {
448            credentials.push(Credential::CustomFields(Box::new(CustomFieldsCredential {
449                id: None,
450                label: None,
451                fields: custom_fields,
452                extensions: vec![],
453            })));
454        }
455
456        credentials
457    }
458}
459
460pub(crate) fn combine_name(
461    first: &Option<String>,
462    middle: &Option<String>,
463    last: &Option<String>,
464) -> Option<String> {
465    let parts: Vec<&str> = [first.as_deref(), middle.as_deref(), last.as_deref()]
466        .into_iter()
467        .flatten()
468        .collect();
469
470    if parts.is_empty() {
471        None
472    } else {
473        Some(parts.join(" "))
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use credential_exchange_format::Credential;
480
481    use super::*;
482
483    #[test]
484    fn test_split_name_none() {
485        let full_name = None;
486        let (first, last) = split_name(&full_name);
487        assert_eq!(first, None);
488        assert_eq!(last, None);
489    }
490
491    #[test]
492    fn test_split_name_empty_string() {
493        let full_name = Some(EditableField {
494            value: EditableFieldString("".to_string()),
495            label: None,
496            id: None,
497            extensions: None,
498        });
499        let (first, last) = split_name(&full_name);
500        assert_eq!(first, None);
501        assert_eq!(last, None);
502    }
503
504    #[test]
505    fn test_split_name_whitespace_only() {
506        let full_name = Some(EditableField {
507            value: EditableFieldString("   \t\n  ".to_string()),
508            label: None,
509            id: None,
510            extensions: None,
511        });
512        let (first, last) = split_name(&full_name);
513        assert_eq!(first, None);
514        assert_eq!(last, None);
515    }
516
517    #[test]
518    fn test_split_name_single_name() {
519        let full_name = Some(EditableField {
520            value: EditableFieldString("John".to_string()),
521            label: None,
522            id: None,
523            extensions: None,
524        });
525        let (first, last) = split_name(&full_name);
526        assert_eq!(first, Some("John".to_string()));
527        assert_eq!(last, None);
528    }
529
530    #[test]
531    fn test_split_name_single_name_with_whitespace() {
532        let full_name = Some(EditableField {
533            value: EditableFieldString("  John  ".to_string()),
534            label: None,
535            id: None,
536            extensions: None,
537        });
538        let (first, last) = split_name(&full_name);
539        assert_eq!(first, Some("John".to_string()));
540        assert_eq!(last, None);
541    }
542
543    #[test]
544    fn test_split_name_first_last() {
545        let full_name = Some(EditableField {
546            value: EditableFieldString("John Doe".to_string()),
547            label: None,
548            id: None,
549            extensions: None,
550        });
551        let (first, last) = split_name(&full_name);
552        assert_eq!(first, Some("John".to_string()));
553        assert_eq!(last, Some("Doe".to_string()));
554    }
555
556    #[test]
557    fn test_split_name_first_middle_last() {
558        let full_name = Some(EditableField {
559            value: EditableFieldString("John Michael Doe".to_string()),
560            label: None,
561            id: None,
562            extensions: None,
563        });
564        let (first, last) = split_name(&full_name);
565        assert_eq!(first, Some("John".to_string()));
566        assert_eq!(last, Some("Michael Doe".to_string()));
567    }
568
569    #[test]
570    fn test_split_name_multiple_middle_names() {
571        let full_name = Some(EditableField {
572            value: EditableFieldString("John Michael Andrew Doe".to_string()),
573            label: None,
574            id: None,
575            extensions: None,
576        });
577        let (first, last) = split_name(&full_name);
578        assert_eq!(first, Some("John".to_string()));
579        assert_eq!(last, Some("Michael Andrew Doe".to_string()));
580    }
581
582    #[test]
583    fn test_split_name_complex_surname() {
584        let full_name = Some(EditableField {
585            value: EditableFieldString("Jane van der Berg".to_string()),
586            label: None,
587            id: None,
588            extensions: None,
589        });
590        let (first, last) = split_name(&full_name);
591        assert_eq!(first, Some("Jane".to_string()));
592        assert_eq!(last, Some("van der Berg".to_string()));
593    }
594
595    #[test]
596    fn test_split_name_hyphenated_surname() {
597        let full_name = Some(EditableField {
598            value: EditableFieldString("Mary Smith-Johnson".to_string()),
599            label: None,
600            id: None,
601            extensions: None,
602        });
603        let (first, last) = split_name(&full_name);
604        assert_eq!(first, Some("Mary".to_string()));
605        assert_eq!(last, Some("Smith-Johnson".to_string()));
606    }
607
608    #[test]
609    fn test_split_name_extra_whitespace() {
610        let full_name = Some(EditableField {
611            value: EditableFieldString("  John   Michael   Doe  ".to_string()),
612            label: None,
613            id: None,
614            extensions: None,
615        });
616        let (first, last) = split_name(&full_name);
617        assert_eq!(first, Some("John".to_string()));
618        assert_eq!(last, Some("Michael Doe".to_string()));
619    }
620
621    #[test]
622    fn test_split_name_special_characters() {
623        let full_name = Some(EditableField {
624            value: EditableFieldString("José María González".to_string()),
625            label: None,
626            id: None,
627            extensions: None,
628        });
629        let (first, last) = split_name(&full_name);
630        assert_eq!(first, Some("José".to_string()));
631        assert_eq!(last, Some("María González".to_string()));
632    }
633
634    #[test]
635    fn test_split_name_single_character_names() {
636        let full_name = Some(EditableField {
637            value: EditableFieldString("A B C".to_string()),
638            label: None,
639            id: None,
640            extensions: None,
641        });
642        let (first, last) = split_name(&full_name);
643        assert_eq!(first, Some("A".to_string()));
644        assert_eq!(last, Some("B C".to_string()));
645    }
646
647    #[test]
648    fn test_identity_to_credentials() {
649        let identity = Identity {
650            title: Some("Dr.".to_string()),
651            first_name: Some("John".to_string()),
652            middle_name: Some("Michael".to_string()),
653            last_name: Some("Doe".to_string()),
654            address1: Some("123 Main St".to_string()),
655            address2: Some("Apt 456".to_string()),
656            address3: None,
657            city: Some("Anytown".to_string()),
658            state: Some("CA".to_string()),
659            postal_code: Some("12345".to_string()),
660            country: Some("US".to_string()),
661            company: Some("PhD".to_string()),
662            email: Some("[email protected]".to_string()),
663            phone: Some("+1234567890".to_string()),
664            ssn: Some("123-45-6789".to_string()),
665            username: Some("johndoe".to_string()),
666            passport_number: Some("P123456789".to_string()),
667            license_number: Some("DL123456".to_string()),
668        };
669
670        let credentials: Vec<Credential> = identity.into();
671
672        // Should create PersonName, Address, Passport, DriversLicense, IdentityDocument, and
673        // CustomFields credentials
674        assert_eq!(credentials.len(), 6);
675
676        // Check PersonName credential
677        if let Credential::PersonName(person_name) = &credentials[0] {
678            assert_eq!(person_name.title.as_ref().unwrap().value.0, "Dr.");
679            assert_eq!(person_name.given.as_ref().unwrap().value.0, "John");
680            assert_eq!(person_name.given2.as_ref().unwrap().value.0, "Michael");
681            assert_eq!(person_name.surname.as_ref().unwrap().value.0, "Doe");
682            assert_eq!(person_name.credentials.as_ref().unwrap().value.0, "PhD");
683        } else {
684            panic!("Expected PersonName credential");
685        }
686
687        // Check Address credential
688        if let Credential::Address(address) = &credentials[1] {
689            assert_eq!(
690                address.street_address.as_ref().unwrap().value.0,
691                "123 Main St\nApt 456"
692            );
693            assert_eq!(address.city.as_ref().unwrap().value.0, "Anytown");
694            assert_eq!(address.territory.as_ref().unwrap().value.0, "CA");
695            assert_eq!(address.country.as_ref().unwrap().value.0, "US");
696            assert_eq!(address.tel.as_ref().unwrap().value.0, "+1234567890");
697            assert_eq!(address.postal_code.as_ref().unwrap().value.0, "12345");
698        } else {
699            panic!("Expected Address credential");
700        }
701
702        // Check Passport credential
703        if let Credential::Passport(passport) = &credentials[2] {
704            assert_eq!(
705                passport.passport_number.as_ref().unwrap().value.0,
706                "P123456789"
707            );
708            assert_eq!(
709                passport.full_name.as_ref().unwrap().value.0,
710                "John Michael Doe"
711            );
712            assert_eq!(
713                passport
714                    .national_identification_number
715                    .as_ref()
716                    .unwrap()
717                    .value
718                    .0,
719                "123-45-6789"
720            );
721            assert_eq!(passport.issuing_country.as_ref().unwrap().value.0, "US");
722        } else {
723            panic!("Expected Passport credential");
724        }
725
726        // Check DriversLicense credential
727        if let Credential::DriversLicense(license) = &credentials[3] {
728            assert_eq!(license.license_number.as_ref().unwrap().value.0, "DL123456");
729            assert_eq!(
730                license.full_name.as_ref().unwrap().value.0,
731                "John Michael Doe"
732            );
733            assert_eq!(license.territory.as_ref().unwrap().value.0, "CA");
734            assert_eq!(license.country.as_ref().unwrap().value.0, "US");
735        } else {
736            panic!("Expected DriversLicense credential");
737        }
738
739        // Check IdentityDocument credential
740        if let Credential::IdentityDocument(identity_doc) = &credentials[4] {
741            assert_eq!(
742                identity_doc.identification_number.as_ref().unwrap().value.0,
743                "123-45-6789"
744            );
745            assert_eq!(
746                identity_doc.full_name.as_ref().unwrap().value.0,
747                "John Michael Doe"
748            );
749        } else {
750            panic!("Expected IdentityDocument credential");
751        }
752
753        // Check CustomFields credential
754        if let Credential::CustomFields(custom_fields) = &credentials[5] {
755            assert_eq!(custom_fields.fields.len(), 2); // email, username
756
757            // Check email field
758            let email_field = &custom_fields.fields[0];
759            if let EditableFieldValue::String(email_field) = email_field {
760                assert_eq!(email_field.label.as_ref().unwrap(), "Email");
761                assert_eq!(email_field.value.0, "[email protected]");
762            } else {
763                panic!("Expected email field to be of type String");
764            }
765
766            // Check username field
767            let username_field = &custom_fields.fields[1];
768            if let EditableFieldValue::String(username_field) = username_field {
769                assert_eq!(username_field.label.as_ref().unwrap(), "Username");
770                assert_eq!(username_field.value.0, "johndoe");
771            } else {
772                panic!("Expected username field to be of type String");
773            }
774        } else {
775            panic!("Expected CustomFields credential");
776        }
777    }
778
779    #[test]
780    fn test_identity_minimal_fields() {
781        let identity = Identity {
782            first_name: Some("Jane".to_string()),
783            last_name: Some("Smith".to_string()),
784            ..Default::default()
785        };
786
787        let credentials: Vec<Credential> = identity.into();
788
789        // Should only create PersonName credential
790        assert_eq!(credentials.len(), 1);
791
792        if let Credential::PersonName(person_name) = &credentials[0] {
793            assert_eq!(person_name.given.as_ref().unwrap().value.0, "Jane");
794            assert_eq!(person_name.surname.as_ref().unwrap().value.0, "Smith");
795            assert!(person_name.title.is_none());
796            assert!(person_name.given2.is_none());
797        } else {
798            panic!("Expected PersonName credential");
799        }
800    }
801
802    #[test]
803    fn test_identity_license_only() {
804        let identity = Identity {
805            first_name: Some("Alice".to_string()),
806            license_number: Some("LIC123456".to_string()),
807            state: Some("NY".to_string()),
808            ..Default::default()
809        };
810
811        let credentials: Vec<Credential> = identity.into();
812
813        // Should create PersonName, Address (due to state), and DriversLicense credentials
814        assert_eq!(credentials.len(), 3);
815
816        // Check PersonName credential
817        if let Credential::PersonName(person_name) = &credentials[0] {
818            assert_eq!(person_name.given.as_ref().unwrap().value.0, "Alice");
819        } else {
820            panic!("Expected PersonName credential");
821        }
822
823        // Check Address credential
824        if let Credential::Address(address) = &credentials[1] {
825            assert_eq!(address.territory.as_ref().unwrap().value.0, "NY");
826        } else {
827            panic!("Expected Address credential");
828        }
829
830        // Check DriversLicense credential
831        if let Credential::DriversLicense(license) = &credentials[2] {
832            assert_eq!(
833                license.license_number.as_ref().unwrap().value.0,
834                "LIC123456"
835            );
836            assert_eq!(license.full_name.as_ref().unwrap().value.0, "Alice");
837            assert_eq!(license.territory.as_ref().unwrap().value.0, "NY");
838        } else {
839            panic!("Expected DriversLicense credential");
840        }
841    }
842
843    #[test]
844    fn test_identity_ssn_only() {
845        let identity = Identity {
846            first_name: Some("Bob".to_string()),
847            ssn: Some("987-65-4321".to_string()),
848            ..Default::default()
849        };
850
851        let credentials: Vec<Credential> = identity.into();
852
853        // Should create PersonName and IdentityDocument credentials
854        assert_eq!(credentials.len(), 2);
855
856        if let Credential::IdentityDocument(identity_doc) = &credentials[1] {
857            assert_eq!(
858                identity_doc.identification_number.as_ref().unwrap().value.0,
859                "987-65-4321"
860            );
861            assert_eq!(identity_doc.full_name.as_ref().unwrap().value.0, "Bob");
862        } else {
863            panic!("Expected IdentityDocument credential");
864        }
865    }
866
867    #[test]
868    fn test_identity_empty() {
869        let identity = Identity::default();
870
871        let credentials: Vec<Credential> = identity.into();
872
873        // Should create no credentials for completely empty identity
874        assert_eq!(credentials.len(), 0);
875    }
876
877    #[test]
878    fn test_combine_name_helper() {
879        assert_eq!(
880            combine_name(
881                &Some("John".to_string()),
882                &Some("Michael".to_string()),
883                &Some("Doe".to_string())
884            ),
885            Some("John Michael Doe".to_string())
886        );
887
888        assert_eq!(
889            combine_name(&Some("Jane".to_string()), &None, &Some("Smith".to_string())),
890            Some("Jane Smith".to_string())
891        );
892
893        assert_eq!(
894            combine_name(&Some("Bob".to_string()), &None, &None),
895            Some("Bob".to_string())
896        );
897
898        assert_eq!(combine_name(&None, &None, &None), None);
899    }
900}