1#![forbid(unsafe_code)]
100
101mod addressbook;
102mod backend;
103mod capability;
104mod card;
105mod collision;
106
107pub use addressbook::{AddressBook, AddressBookRights};
108pub use backend::{AddressBookProperty, ContactCardProperty};
109pub use capability::{ContactsAccountCapability, ContactsCapability, JMAP_CONTACTS_URI};
110pub use card::{
111 ContactCard, ContactCardComparator, ContactCardComparatorProperty, ContactCardFilter,
112 ContactCardFilterCondition,
113};
114pub use collision::CollisionError;
115
116pub use jmap_types::{Filter, FilterOperator, Operator};
123
124pub use jmap_jscontact_types::{
131 Address, AddressComponent, Anniversary, AnniversaryDate, Author, Calendar, CryptoKey,
132 Directory, EmailAddress, LanguagePref, Link, Media, Name, NameComponent, Nickname, Note,
133 OnlineService, OrgUnit, Organization, PartialDate, PersonalInfo, Phone, Pronouns, Relation,
134 SchedulingAddress, SpeakToAs, Timestamp, Title,
135};
136
137#[cfg(test)]
138mod tests {
139 use std::collections::BTreeMap;
140
141 use jmap_types::Id;
142 use serde_json::json;
143
144 use super::*;
145
146 #[test]
151 fn address_book_rights_default_is_all_false() {
152 let r = AddressBookRights::default();
153 assert!(!r.may_read, "may_read default should be false");
154 assert!(!r.may_write, "may_write default should be false");
155 assert!(!r.may_share, "may_share default should be false");
156 assert!(!r.may_delete, "may_delete default should be false");
157 }
158
159 #[test]
162 fn address_book_rights_deserialize() {
163 let json = r#"{
164 "mayRead": true,
165 "mayWrite": false,
166 "mayShare": false,
167 "mayDelete": false
168 }"#;
169 let r: AddressBookRights = serde_json::from_str(json).unwrap();
170 assert!(r.may_read);
171 assert!(!r.may_write);
172 assert!(!r.may_share);
173 assert!(!r.may_delete);
174 }
175
176 #[test]
179 fn address_book_rights_round_trip() {
180 let original = AddressBookRights {
181 may_read: true,
182 may_write: true,
183 may_share: false,
184 may_delete: false,
185 may_set_metadata: Some(true),
186 extra: serde_json::Map::new(),
187 };
188 let json = serde_json::to_string(&original).unwrap();
189 let decoded: AddressBookRights = serde_json::from_str(&json).unwrap();
190 assert_eq!(original, decoded);
191 }
192
193 #[test]
198 fn address_book_deserialize_spec_example() {
199 let json = r#"{
201 "id": "062adcfa-105d-455c-bc60-6db68b69c3f3",
202 "name": "Personal",
203 "description": null,
204 "sortOrder": 0,
205 "isDefault": true,
206 "isSubscribed": true,
207 "shareWith": {
208 "3f1502e0-63fe-4335-9ff3-e739c188f5dd": {
209 "mayRead": true,
210 "mayWrite": false,
211 "mayShare": false,
212 "mayDelete": false
213 }
214 },
215 "myRights": {
216 "mayRead": true,
217 "mayWrite": true,
218 "mayShare": true,
219 "mayDelete": false
220 }
221 }"#;
222 let ab: AddressBook = serde_json::from_str(json).unwrap();
223 assert_eq!(ab.name, "Personal");
224 assert!(ab.is_default);
225 assert!(ab.is_subscribed);
226 assert_eq!(ab.sort_order, 0);
227 assert!(ab.description.is_none());
228
229 let share_with = ab.share_with.unwrap();
230 let principal_id: Id = Id::from("3f1502e0-63fe-4335-9ff3-e739c188f5dd");
231 let shared_rights = &share_with[&principal_id];
232 assert!(shared_rights.may_read);
233 assert!(!shared_rights.may_write);
234
235 assert!(ab.my_rights.may_read);
236 assert!(ab.my_rights.may_write);
237 assert!(ab.my_rights.may_share);
238 assert!(!ab.my_rights.may_delete);
239 }
240
241 #[test]
244 fn address_book_share_with_null() {
245 let json = r#"{
246 "id": "cd40089d-35f9-4fd7-980b-ba3a9f1d74fe",
247 "name": "Autosaved",
248 "description": null,
249 "sortOrder": 1,
250 "isDefault": false,
251 "isSubscribed": true,
252 "shareWith": null,
253 "myRights": {
254 "mayRead": true,
255 "mayWrite": true,
256 "mayShare": true,
257 "mayDelete": false
258 }
259 }"#;
260 let ab: AddressBook = serde_json::from_str(json).unwrap();
261 assert_eq!(ab.name, "Autosaved");
262 assert!(ab.share_with.is_none());
263 }
264
265 #[test]
269 fn address_book_share_with_null_serializes() {
270 let json = r#"{
272 "id": "ab-null-sw",
273 "name": "Private",
274 "sortOrder": 0,
275 "isDefault": false,
276 "isSubscribed": true,
277 "shareWith": null,
278 "myRights": {
279 "mayRead": true,
280 "mayWrite": true,
281 "mayShare": false,
282 "mayDelete": false
283 }
284 }"#;
285 let ab: AddressBook = serde_json::from_str(json).expect("deserialize");
286 assert!(ab.share_with.is_none());
287
288 let serialized = serde_json::to_value(&ab).expect("serialize");
289 let obj = serialized.as_object().expect("object");
290 assert!(
291 obj.contains_key("shareWith"),
292 "shareWith must be present in wire JSON (required-nullable)"
293 );
294 assert!(
295 obj["shareWith"].is_null(),
296 "shareWith must serialize as null when None"
297 );
298 }
299
300 #[test]
303 fn address_book_share_with_some_serializes() {
304 let json = r#"{
306 "id": "062adcfa-105d-455c-bc60-6db68b69c3f3",
307 "name": "Personal",
308 "description": null,
309 "sortOrder": 0,
310 "isDefault": true,
311 "isSubscribed": true,
312 "shareWith": {
313 "3f1502e0-63fe-4335-9ff3-e739c188f5dd": {
314 "mayRead": true,
315 "mayWrite": false,
316 "mayShare": false,
317 "mayDelete": false
318 }
319 },
320 "myRights": {
321 "mayRead": true,
322 "mayWrite": true,
323 "mayShare": true,
324 "mayDelete": false
325 }
326 }"#;
327 let ab: AddressBook = serde_json::from_str(json).expect("deserialize");
328 assert!(ab.share_with.is_some());
329
330 let serialized = serde_json::to_value(&ab).expect("serialize");
331 let obj = serialized.as_object().expect("object");
332 assert!(
333 obj.contains_key("shareWith"),
334 "shareWith must be present when set"
335 );
336 assert!(
337 !obj["shareWith"].is_null(),
338 "shareWith must not be null when populated"
339 );
340 let back: AddressBook = serde_json::from_value(serialized).expect("round-trip deserialize");
342 assert_eq!(ab.share_with, back.share_with);
343 }
344
345 #[test]
347 fn address_book_round_trip_with_description() {
348 let original_json = json!({
350 "id": "ab1",
351 "name": "Work",
352 "description": "Work contacts",
353 "sortOrder": 5,
354 "isDefault": false,
355 "isSubscribed": true,
356 "myRights": {
357 "mayRead": true,
358 "mayWrite": false,
359 "mayShare": false,
360 "mayDelete": false
361 }
362 });
363 let ab: AddressBook = serde_json::from_value(original_json).unwrap();
364 assert_eq!(ab.description.as_deref(), Some("Work contacts"));
365 let rt: AddressBook = serde_json::from_str(&serde_json::to_string(&ab).unwrap()).unwrap();
366 assert_eq!(rt.description.as_deref(), Some("Work contacts"));
367 }
368
369 #[test]
375 fn address_book_description_null_serializes() {
376 let json = r#"{
377 "id": "AB1",
378 "name": "Personal",
379 "description": null,
380 "sortOrder": 0,
381 "isDefault": false,
382 "isSubscribed": false,
383 "shareWith": null,
384 "myRights": {
385 "mayRead": true,
386 "mayWrite": true,
387 "mayShare": false,
388 "mayDelete": false
389 }
390 }"#;
391 let ab: AddressBook = serde_json::from_str(json).expect("deserialize");
392 assert!(ab.description.is_none(), "description should be None");
393
394 let out = serde_json::to_value(&ab).expect("serialize");
395 assert!(
396 out.as_object().expect("object").contains_key("description"),
397 "description key must be present in wire JSON"
398 );
399 assert!(
400 out["description"].is_null(),
401 "description must serialize as null, not be absent"
402 );
403 }
404
405 #[test]
410 fn contact_card_deserialize_spec_example() {
411 let json = r#"{
413 "id": "3",
414 "addressBookIds": {
415 "062adcfa-105d-455c-bc60-6db68b69c3f3": true
416 },
417 "name": {
418 "components": [
419 { "kind": "given", "value": "Joe" },
420 { "kind": "surname", "value": "Bloggs" }
421 ],
422 "isOrdered": true
423 },
424 "emails": {
425 "0": {
426 "contexts": {
427 "private": true
428 },
429 "address": "joe.bloggs@example.com"
430 }
431 }
432 }"#;
433 let card: ContactCard = serde_json::from_str(json).unwrap();
434
435 let id = card.id.as_ref().unwrap();
436 assert_eq!(id.as_ref(), "3");
437
438 let ab_ids = card.address_book_ids.as_ref().unwrap();
439 let ab_key: Id = Id::from("062adcfa-105d-455c-bc60-6db68b69c3f3");
440 assert!(ab_ids[&ab_key]);
441
442 let name = card.name.as_ref().unwrap();
444 assert_eq!(name["isOrdered"], serde_json::Value::Bool(true));
445 assert_eq!(name["components"][0]["kind"], "given");
446 assert_eq!(name["components"][0]["value"], "Joe");
447
448 let emails = card.emails.as_ref().unwrap();
450 assert_eq!(emails["0"]["address"], "joe.bloggs@example.com");
451 }
452
453 #[test]
456 fn contact_card_group_with_members() {
457 let json = r#"{
458 "version": "1.0",
459 "uid": "urn:uuid:ab4310aa-fa43-11e9-8f0b-362b9e155667",
460 "kind": "group",
461 "name": { "full": "The Doe family" },
462 "members": {
463 "urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af": true,
464 "urn:uuid:b8767877-b4a1-4c70-9acc-505d3819e519": true
465 }
466 }"#;
467 let card: ContactCard = serde_json::from_str(json).unwrap();
468 assert_eq!(card.kind.as_deref(), Some("group"));
469 let members = card.members.as_ref().unwrap();
470 assert!(members["urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af"]);
471 assert_eq!(members.len(), 2);
472 }
473
474 #[test]
476 fn contact_card_minimal_fields() {
477 let json = r#"{
478 "id": "minimal1",
479 "addressBookIds": { "ab1": true }
480 }"#;
481 let card: ContactCard = serde_json::from_str(json).unwrap();
482 assert!(card.version.is_none());
483 assert!(card.uid.is_none());
484 assert!(card.kind.is_none());
485 assert!(card.emails.is_none());
486 assert!(card.phones.is_none());
487 assert!(card.addresses.is_none());
488 }
489
490 #[test]
492 fn contact_card_serializes_only_present_fields() {
493 let card: ContactCard = serde_json::from_value(json!({
495 "id": "x1",
496 "addressBookIds": { "ab1": true },
497 "version": "1.0",
498 "uid": "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
499 }))
500 .unwrap();
501
502 let serialized = serde_json::to_value(&card).unwrap();
503 assert!(serialized.get("id").is_some());
505 assert!(serialized.get("version").is_some());
506 assert!(serialized.get("uid").is_some());
507 assert!(serialized.get("emails").is_none());
509 assert!(serialized.get("phones").is_none());
510 assert!(serialized.get("addresses").is_none());
511 assert!(serialized.get("kind").is_none());
512 }
513
514 #[test]
521 fn contact_card_localizations_patch_object_transparent() {
522 let json = r#"{"localizations":{"de":{"name/full":"Joe Bloggs (DE)"}}}"#;
523 let card: ContactCard = serde_json::from_str(json).expect("deserialize");
524
525 let locs = card.localizations.as_ref().expect("localizations present");
526 let de = locs.get("de").expect("de locale present");
527 assert_eq!(
528 de.as_map().get("name/full").and_then(|v| v.as_str()),
529 Some("Joe Bloggs (DE)"),
530 );
531
532 let serialized = serde_json::to_string(&card).expect("serialize");
533 assert_eq!(serialized, json);
534 }
535
536 #[test]
542 fn filter_condition_slash_keys_serialize_correctly() {
543 let filter: ContactCardFilterCondition = serde_json::from_value(json!({
544 "name/given": "John",
545 "name/surname": "Smith",
546 "name/surname2": "Doe"
547 }))
548 .unwrap();
549
550 assert_eq!(filter.name_given.as_deref(), Some("John"));
551 assert_eq!(filter.name_surname.as_deref(), Some("Smith"));
552 assert_eq!(filter.name_surname2.as_deref(), Some("Doe"));
553
554 let serialized = serde_json::to_value(&filter).unwrap();
555 assert_eq!(serialized["name/given"], "John");
556 assert_eq!(serialized["name/surname"], "Smith");
557 assert_eq!(serialized["name/surname2"], "Doe");
558 }
559
560 #[test]
563 fn filter_condition_all_fields() {
564 let json = r#"{
565 "inAddressBook": "ab1",
566 "uid": "urn:uuid:abc123",
567 "hasMember": "urn:uuid:def456",
568 "kind": "individual",
569 "createdBefore": "2024-01-01T00:00:00Z",
570 "createdAfter": "2020-01-01T00:00:00Z",
571 "updatedBefore": "2024-06-01T00:00:00Z",
572 "updatedAfter": "2021-06-01T00:00:00Z",
573 "text": "John",
574 "name": "Doe",
575 "name/given": "Jane",
576 "name/surname": "Smith",
577 "name/surname2": "Gomez",
578 "nickname": "Johnny",
579 "organization": "ACME",
580 "email": "john@example.com",
581 "phone": "+1-555-1234",
582 "onlineService": "twitter",
583 "address": "Main St",
584 "note": "VIP"
585 }"#;
586 let f: ContactCardFilterCondition = serde_json::from_str(json).unwrap();
587 assert_eq!(
588 f.in_address_book.as_ref().map(|id| id.as_ref()),
589 Some("ab1")
590 );
591 assert_eq!(f.uid.as_deref(), Some("urn:uuid:abc123"));
592 assert_eq!(f.has_member.as_deref(), Some("urn:uuid:def456"));
593 assert_eq!(f.kind.as_deref(), Some("individual"));
594 assert_eq!(
595 f.created_before.as_ref().map(AsRef::as_ref),
596 Some("2024-01-01T00:00:00Z")
597 );
598 assert_eq!(
599 f.created_after.as_ref().map(AsRef::as_ref),
600 Some("2020-01-01T00:00:00Z")
601 );
602 assert_eq!(
603 f.updated_before.as_ref().map(AsRef::as_ref),
604 Some("2024-06-01T00:00:00Z")
605 );
606 assert_eq!(
607 f.updated_after.as_ref().map(AsRef::as_ref),
608 Some("2021-06-01T00:00:00Z")
609 );
610 assert_eq!(f.text.as_deref(), Some("John"));
611 assert_eq!(f.name.as_deref(), Some("Doe"));
612 assert_eq!(f.name_given.as_deref(), Some("Jane"));
613 assert_eq!(f.name_surname.as_deref(), Some("Smith"));
614 assert_eq!(f.name_surname2.as_deref(), Some("Gomez"));
615 assert_eq!(f.nickname.as_deref(), Some("Johnny"));
616 assert_eq!(f.organization.as_deref(), Some("ACME"));
617 assert_eq!(f.email.as_deref(), Some("john@example.com"));
618 assert_eq!(f.phone.as_deref(), Some("+1-555-1234"));
619 assert_eq!(f.online_service.as_deref(), Some("twitter"));
620 assert_eq!(f.address.as_deref(), Some("Main St"));
621 assert_eq!(f.note.as_deref(), Some("VIP"));
622 }
623
624 #[test]
626 fn filter_condition_empty_serializes_to_empty_object() {
627 let f = ContactCardFilterCondition::default();
628 let v = serde_json::to_value(&f).unwrap();
629 assert_eq!(v, json!({}));
630 }
631
632 #[test]
637 fn contacts_capability_is_empty_object() {
638 let cap = ContactsCapability::default();
639 let v = serde_json::to_value(&cap).unwrap();
640 assert_eq!(v, json!({}));
641 }
642
643 #[test]
646 fn contacts_account_capability_deserialize() {
647 let json = r#"{
648 "maxAddressBooksPerCard": 5,
649 "mayCreateAddressBook": true
650 }"#;
651 let cap: ContactsAccountCapability = serde_json::from_str(json).unwrap();
652 assert_eq!(cap.max_address_books_per_card, Some(5));
653 assert!(cap.may_create_address_book);
654 }
655
656 #[test]
659 fn contacts_account_capability_null_max() {
660 let json = r#"{
661 "maxAddressBooksPerCard": null,
662 "mayCreateAddressBook": false
663 }"#;
664 let cap: ContactsAccountCapability = serde_json::from_str(json).unwrap();
665 assert!(cap.max_address_books_per_card.is_none());
666 assert!(!cap.may_create_address_book);
667 }
668
669 #[test]
673 fn contacts_account_capability_max_unsigned_int() {
674 let json = r#"{
675 "maxAddressBooksPerCard": 9007199254740991,
676 "mayCreateAddressBook": true
677 }"#;
678 let cap: ContactsAccountCapability = serde_json::from_str(json).unwrap();
679 assert_eq!(cap.max_address_books_per_card, Some(9_007_199_254_740_991));
680 }
681
682 #[test]
684 fn jmap_contacts_uri_value() {
685 assert_eq!(JMAP_CONTACTS_URI, "urn:ietf:params:jmap:contacts");
686 }
687
688 #[test]
693 fn comparator_is_ascending_defaults_to_true() {
694 let json = r#"{ "property": "created" }"#;
695 let c: ContactCardComparator = serde_json::from_str(json).unwrap();
696 assert!(c.is_ascending);
697 assert!(c.collation.is_none());
698 }
699
700 #[test]
702 fn comparator_round_trip() {
703 let json = r#"{ "property": "name/given", "isAscending": false, "collation": "i;unicode-casemap" }"#;
704 let c: ContactCardComparator = serde_json::from_str(json).unwrap();
705 assert_eq!(c.property, ContactCardComparatorProperty::NameGiven);
706 assert!(!c.is_ascending);
707 assert_eq!(c.collation.as_deref(), Some("i;unicode-casemap"));
708 let rt: ContactCardComparator =
709 serde_json::from_str(&serde_json::to_string(&c).unwrap()).unwrap();
710 assert_eq!(rt.property, ContactCardComparatorProperty::NameGiven);
711 assert!(!rt.is_ascending);
712 }
713
714 #[test]
719 fn comparator_sort_properties() {
720 use ContactCardComparatorProperty as P;
721 for (wire, want) in [
722 ("created", P::Created),
723 ("updated", P::Updated),
724 ("name/given", P::NameGiven),
725 ("name/surname", P::NameSurname),
726 ("name/surname2", P::NameSurname2),
727 ] {
728 let c: ContactCardComparator =
729 serde_json::from_value(serde_json::json!({ "property": wire })).unwrap();
730 assert_eq!(c.property, want);
731 }
732 let wire = serde_json::json!({ "property": "uid", "isAscending": true });
733 let c: ContactCardComparator = serde_json::from_value(wire).unwrap();
734 assert_eq!(c.property, P::Other("uid".to_owned()));
735 assert_eq!(
737 serde_json::to_value(&c).unwrap(),
738 serde_json::json!({ "property": "uid" })
739 );
740 }
741
742 #[test]
757 fn sloppy_field_name_roundtrips_through_jscontact_name() {
758 let card_json = json!({
759 "name": {
760 "components": [
761 { "kind": "given", "value": "Vincent" },
762 { "kind": "surname", "value": "van Gogh" }
763 ],
764 "isOrdered": true
765 }
766 });
767 let card: ContactCard = serde_json::from_value(card_json).unwrap();
768 let name: Name = serde_json::from_value(card.name.clone().unwrap()).unwrap();
769 let back = serde_json::to_value(&name).unwrap();
770 assert_eq!(back, card.name.unwrap());
771 }
772
773 #[test]
776 fn sloppy_field_nicknames_roundtrips_through_jscontact_nickname_map() {
777 let card_json = json!({
778 "nicknames": {
779 "k391": { "name": "Johnny" }
780 }
781 });
782 let card: ContactCard = serde_json::from_value(card_json).unwrap();
783 let nicks: BTreeMap<String, Nickname> =
784 serde_json::from_value(card.nicknames.clone().unwrap()).unwrap();
785 assert_eq!(nicks["k391"].name, "Johnny");
786 let back = serde_json::to_value(&nicks).unwrap();
787 assert_eq!(back, card.nicknames.unwrap());
788 }
789
790 #[test]
793 fn sloppy_field_organizations_roundtrips_through_jscontact_organization_map() {
794 let card_json = json!({
795 "organizations": {
796 "o1": {
797 "name": "ABC, Inc.",
798 "units": [
799 { "name": "North American Division" },
800 { "name": "Marketing" }
801 ],
802 "sortAs": "ABC"
803 }
804 }
805 });
806 let card: ContactCard = serde_json::from_value(card_json).unwrap();
807 let orgs: BTreeMap<String, Organization> =
808 serde_json::from_value(card.organizations.clone().unwrap()).unwrap();
809 assert_eq!(orgs["o1"].name.as_deref(), Some("ABC, Inc."));
810 let back = serde_json::to_value(&orgs).unwrap();
811 assert_eq!(back, card.organizations.unwrap());
812 }
813
814 #[test]
817 fn sloppy_field_speak_to_as_roundtrips_through_jscontact_speak_to_as() {
818 let card_json = json!({
819 "speakToAs": {
820 "grammaticalGender": "neuter",
821 "pronouns": {
822 "k19": { "pronouns": "they/them", "pref": 2 }
823 }
824 }
825 });
826 let card: ContactCard = serde_json::from_value(card_json).unwrap();
827 let speak: SpeakToAs = serde_json::from_value(card.speak_to_as.clone().unwrap()).unwrap();
828 let back = serde_json::to_value(&speak).unwrap();
829 assert_eq!(back, card.speak_to_as.unwrap());
830 }
831
832 #[test]
835 fn sloppy_field_titles_roundtrips_through_jscontact_title_map() {
836 let card_json = json!({
837 "titles": {
838 "le9": { "kind": "title", "name": "Research Scientist" }
839 }
840 });
841 let card: ContactCard = serde_json::from_value(card_json).unwrap();
842 let titles: BTreeMap<String, Title> =
843 serde_json::from_value(card.titles.clone().unwrap()).unwrap();
844 assert_eq!(titles["le9"].name, "Research Scientist");
845 let back = serde_json::to_value(&titles).unwrap();
846 assert_eq!(back, card.titles.unwrap());
847 }
848
849 #[test]
852 fn sloppy_field_emails_roundtrips_through_jscontact_email_address_map() {
853 let card_json = json!({
854 "emails": {
855 "e1": { "contexts": { "work": true }, "address": "jqpublic@xyz.example.com" }
856 }
857 });
858 let card: ContactCard = serde_json::from_value(card_json).unwrap();
859 let emails: BTreeMap<String, EmailAddress> =
860 serde_json::from_value(card.emails.clone().unwrap()).unwrap();
861 assert_eq!(emails["e1"].address, "jqpublic@xyz.example.com");
862 let back = serde_json::to_value(&emails).unwrap();
863 assert_eq!(back, card.emails.unwrap());
864 }
865
866 #[test]
869 fn sloppy_field_online_services_roundtrips_through_jscontact_online_service_map() {
870 let card_json = json!({
871 "onlineServices": {
872 "x1": { "uri": "xmpp:alice@example.com" }
873 }
874 });
875 let card: ContactCard = serde_json::from_value(card_json).unwrap();
876 let svcs: BTreeMap<String, OnlineService> =
877 serde_json::from_value(card.online_services.clone().unwrap()).unwrap();
878 let back = serde_json::to_value(&svcs).unwrap();
879 assert_eq!(back, card.online_services.unwrap());
880 }
881
882 #[test]
885 fn sloppy_field_phones_roundtrips_through_jscontact_phone_map() {
886 let card_json = json!({
887 "phones": {
888 "tel0": {
889 "contexts": { "private": true },
890 "features": { "voice": true },
891 "number": "tel:+1-555-555-5555;ext=5555",
892 "pref": 1
893 }
894 }
895 });
896 let card: ContactCard = serde_json::from_value(card_json).unwrap();
897 let phones: BTreeMap<String, Phone> =
898 serde_json::from_value(card.phones.clone().unwrap()).unwrap();
899 assert_eq!(phones["tel0"].number, "tel:+1-555-555-5555;ext=5555");
900 let back = serde_json::to_value(&phones).unwrap();
901 assert_eq!(back, card.phones.unwrap());
902 }
903
904 #[test]
907 fn sloppy_field_preferred_languages_roundtrips_through_jscontact_language_pref_map() {
908 let card_json = json!({
909 "preferredLanguages": {
910 "l1": { "language": "en", "contexts": { "work": true }, "pref": 1 }
911 }
912 });
913 let card: ContactCard = serde_json::from_value(card_json).unwrap();
914 let langs: BTreeMap<String, LanguagePref> =
915 serde_json::from_value(card.preferred_languages.clone().unwrap()).unwrap();
916 assert_eq!(langs["l1"].language, "en");
917 let back = serde_json::to_value(&langs).unwrap();
918 assert_eq!(back, card.preferred_languages.unwrap());
919 }
920
921 #[test]
924 fn sloppy_field_calendars_roundtrips_through_jscontact_calendar_map() {
925 let card_json = json!({
926 "calendars": {
927 "calA": { "kind": "calendar", "uri": "webcal://calendar.example.com/calA.ics" }
928 }
929 });
930 let card: ContactCard = serde_json::from_value(card_json).unwrap();
931 let cals: BTreeMap<String, Calendar> =
932 serde_json::from_value(card.calendars.clone().unwrap()).unwrap();
933 let back = serde_json::to_value(&cals).unwrap();
934 assert_eq!(back, card.calendars.unwrap());
935 }
936
937 #[test]
940 fn sloppy_field_scheduling_addresses_roundtrips_through_jscontact_scheduling_address_map() {
941 let card_json = json!({
942 "schedulingAddresses": {
943 "sched1": { "uri": "mailto:janedoe@example.com" }
944 }
945 });
946 let card: ContactCard = serde_json::from_value(card_json).unwrap();
947 let scheds: BTreeMap<String, SchedulingAddress> =
948 serde_json::from_value(card.scheduling_addresses.clone().unwrap()).unwrap();
949 assert_eq!(scheds["sched1"].uri, "mailto:janedoe@example.com");
950 let back = serde_json::to_value(&scheds).unwrap();
951 assert_eq!(back, card.scheduling_addresses.unwrap());
952 }
953
954 #[test]
957 fn sloppy_field_addresses_roundtrips_through_jscontact_address_map() {
958 let card_json = json!({
959 "addresses": {
960 "k23": {
961 "contexts": { "work": true },
962 "components": [
963 { "kind": "number", "value": "54321" },
964 { "kind": "name", "value": "Oak St" }
965 ],
966 "countryCode": "US",
967 "isOrdered": true
968 }
969 }
970 });
971 let card: ContactCard = serde_json::from_value(card_json).unwrap();
972 let addrs: BTreeMap<String, Address> =
973 serde_json::from_value(card.addresses.clone().unwrap()).unwrap();
974 assert_eq!(addrs["k23"].country_code.as_deref(), Some("US"));
975 let back = serde_json::to_value(&addrs).unwrap();
976 assert_eq!(back, card.addresses.unwrap());
977 }
978
979 #[test]
982 fn sloppy_field_crypto_keys_roundtrips_through_jscontact_crypto_key_map() {
983 let card_json = json!({
984 "cryptoKeys": {
985 "mykey1": { "uri": "https://www.example.com/keys/jdoe.cer" }
986 }
987 });
988 let card: ContactCard = serde_json::from_value(card_json).unwrap();
989 let keys: BTreeMap<String, CryptoKey> =
990 serde_json::from_value(card.crypto_keys.clone().unwrap()).unwrap();
991 let back = serde_json::to_value(&keys).unwrap();
992 assert_eq!(back, card.crypto_keys.unwrap());
993 }
994
995 #[test]
998 fn sloppy_field_directories_roundtrips_through_jscontact_directory_map() {
999 let card_json = json!({
1000 "directories": {
1001 "dir1": {
1002 "kind": "entry",
1003 "uri": "https://dir.example.com/addrbook/jdoe/Jean%20Dupont.vcf"
1004 }
1005 }
1006 });
1007 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1008 let dirs: BTreeMap<String, Directory> =
1009 serde_json::from_value(card.directories.clone().unwrap()).unwrap();
1010 assert_eq!(dirs["dir1"].kind.as_deref(), Some("entry"));
1011 let back = serde_json::to_value(&dirs).unwrap();
1012 assert_eq!(back, card.directories.unwrap());
1013 }
1014
1015 #[test]
1018 fn sloppy_field_links_roundtrips_through_jscontact_link_map() {
1019 let card_json = json!({
1020 "links": {
1021 "link3": { "kind": "contact", "uri": "mailto:contact@example.com", "pref": 1 }
1022 }
1023 });
1024 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1025 let links: BTreeMap<String, Link> =
1026 serde_json::from_value(card.links.clone().unwrap()).unwrap();
1027 let back = serde_json::to_value(&links).unwrap();
1028 assert_eq!(back, card.links.unwrap());
1029 }
1030
1031 #[test]
1034 fn sloppy_field_media_roundtrips_through_jscontact_media_map() {
1035 let card_json = json!({
1036 "media": {
1037 "res47": { "kind": "logo", "uri": "https://www.example.com/pub/logos/abccorp.jpg" }
1038 }
1039 });
1040 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1041 let media: BTreeMap<String, Media> =
1042 serde_json::from_value(card.media.clone().unwrap()).unwrap();
1043 assert_eq!(media["res47"].kind.as_deref(), Some("logo"));
1044 let back = serde_json::to_value(&media).unwrap();
1045 assert_eq!(back, card.media.unwrap());
1046 }
1047
1048 #[test]
1051 fn sloppy_field_anniversaries_roundtrips_through_jscontact_anniversary_map() {
1052 let card_json = json!({
1053 "anniversaries": {
1054 "k8": {
1055 "kind": "birth",
1056 "date": { "year": 1953, "month": 4, "day": 15 }
1057 }
1058 }
1059 });
1060 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1061 let annivs: BTreeMap<String, Anniversary> =
1062 serde_json::from_value(card.anniversaries.clone().unwrap()).unwrap();
1063 assert_eq!(annivs["k8"].kind, "birth");
1064 let back = serde_json::to_value(&annivs).unwrap();
1065 assert_eq!(back, card.anniversaries.unwrap());
1066 }
1067
1068 #[test]
1071 fn sloppy_field_notes_roundtrips_through_jscontact_note_map() {
1072 let card_json = json!({
1073 "notes": {
1074 "n1": {
1075 "note": "Open office hours are 1600 to 1715 EST, Mon-Fri",
1076 "created": "2022-11-23T15:01:32Z",
1077 "author": { "name": "John" }
1078 }
1079 }
1080 });
1081 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1082 let notes: BTreeMap<String, Note> =
1083 serde_json::from_value(card.notes.clone().unwrap()).unwrap();
1084 assert_eq!(
1085 notes["n1"].author.as_ref().unwrap().name.as_deref(),
1086 Some("John")
1087 );
1088 let back = serde_json::to_value(¬es).unwrap();
1089 assert_eq!(back, card.notes.unwrap());
1090 }
1091
1092 #[test]
1095 fn sloppy_field_personal_info_roundtrips_through_jscontact_personal_info_map() {
1096 let card_json = json!({
1097 "personalInfo": {
1098 "pi2": { "kind": "expertise", "value": "chemistry", "level": "high" }
1099 }
1100 });
1101 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1102 let info: BTreeMap<String, PersonalInfo> =
1103 serde_json::from_value(card.personal_info.clone().unwrap()).unwrap();
1104 assert_eq!(info["pi2"].kind, "expertise");
1105 let back = serde_json::to_value(&info).unwrap();
1106 assert_eq!(back, card.personal_info.unwrap());
1107 }
1108
1109 #[test]
1112 fn sloppy_field_related_to_roundtrips_through_jscontact_relation_map() {
1113 let card_json = json!({
1114 "relatedTo": {
1115 "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6": {
1116 "relation": { "friend": true }
1117 }
1118 }
1119 });
1120 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1121 let rels: BTreeMap<String, Relation> =
1122 serde_json::from_value(card.related_to.clone().unwrap()).unwrap();
1123 let back = serde_json::to_value(&rels).unwrap();
1124 assert_eq!(back, card.related_to.unwrap());
1125 }
1126
1127 #[test]
1136 fn keywords_roundtrips_as_string_bool_map() {
1137 let card_json = json!({
1138 "keywords": {
1139 "internet": true,
1140 "IETF": true
1141 }
1142 });
1143 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1144 let kws = card.keywords.as_ref().expect("keywords present");
1145 assert!(kws["internet"]);
1146 assert!(kws["IETF"]);
1147 assert_eq!(kws.len(), 2);
1148 }
1149
1150 #[test]
1159 fn top_level_jscontact_reexport_is_jmap_jscontact_types() {
1160 const _ASSERT_TOP_LEVEL_SAME_AS_DIRECT: fn(jmap_jscontact_types::Name) -> Name = |x| x;
1161
1162 let v = json!({ "full": "Vincent van Gogh" });
1166 let _direct: jmap_jscontact_types::Name = serde_json::from_value(v.clone()).unwrap();
1167 let _top_level: Name = serde_json::from_value(v).unwrap();
1168 }
1169
1170 #[test]
1174 fn address_book_rights_preserves_vendor_extras() {
1175 let raw = json!({
1176 "mayRead": true,
1177 "mayWrite": false,
1178 "mayShare": false,
1179 "mayDelete": false,
1180 "acmeCorpMayMerge": true
1181 });
1182 let r: AddressBookRights = serde_json::from_value(raw).unwrap();
1183 assert_eq!(
1184 r.extra.get("acmeCorpMayMerge").and_then(|v| v.as_bool()),
1185 Some(true)
1186 );
1187 let back = serde_json::to_value(&r).unwrap();
1188 assert_eq!(back["acmeCorpMayMerge"], true);
1189 }
1190
1191 #[test]
1193 fn address_book_preserves_vendor_extras() {
1194 let raw = json!({
1195 "id": "ab1",
1196 "name": "Personal",
1197 "description": null,
1198 "sortOrder": 0,
1199 "isDefault": true,
1200 "isSubscribed": true,
1201 "shareWith": null,
1202 "myRights": {
1203 "mayRead": true, "mayWrite": true,
1204 "mayShare": false, "mayDelete": false
1205 },
1206 "acmeCorpRetentionDays": 365
1207 });
1208 let ab: AddressBook = serde_json::from_value(raw).unwrap();
1209 assert_eq!(
1210 ab.extra
1211 .get("acmeCorpRetentionDays")
1212 .and_then(|v| v.as_u64()),
1213 Some(365)
1214 );
1215 let back = serde_json::to_value(&ab).unwrap();
1216 assert_eq!(back["acmeCorpRetentionDays"], 365);
1217 }
1218
1219 #[test]
1221 fn contact_card_preserves_vendor_extras() {
1222 let raw = json!({
1223 "uid": "card-1",
1224 "version": "1.0",
1225 "acmeCorpExternalId": "ldap-42"
1226 });
1227 let c: ContactCard = serde_json::from_value(raw).unwrap();
1228 assert_eq!(
1229 c.extra.get("acmeCorpExternalId").and_then(|v| v.as_str()),
1230 Some("ldap-42")
1231 );
1232 let back = serde_json::to_value(&c).unwrap();
1233 assert_eq!(back["acmeCorpExternalId"], "ldap-42");
1234 }
1235
1236 #[test]
1250 fn contact_card_address_book_ids_accepts_false_deserialise() {
1251 let raw = json!({
1252 "id": "card-1",
1253 "addressBookIds": { "ab1": false }
1254 });
1255 let c: ContactCard = serde_json::from_value(raw).unwrap();
1256 let ids = c.address_book_ids.as_ref().unwrap();
1257 let ab1: Id = Id::from("ab1");
1258 assert_eq!(ids.get(&ab1).copied(), Some(false));
1260 }
1261
1262 #[test]
1265 fn contact_card_members_accepts_false_deserialise() {
1266 let raw = json!({
1267 "id": "card-1",
1268 "kind": "group",
1269 "members": { "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6": false }
1270 });
1271 let c: ContactCard = serde_json::from_value(raw).unwrap();
1272 let mems = c.members.as_ref().unwrap();
1273 assert_eq!(
1274 mems.get("urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6")
1275 .copied(),
1276 Some(false),
1277 );
1278 }
1279
1280 #[test]
1283 fn contact_card_keywords_accepts_false_deserialise() {
1284 let raw = json!({
1285 "id": "card-1",
1286 "keywords": { "IETF": false }
1287 });
1288 let c: ContactCard = serde_json::from_value(raw).unwrap();
1289 let kws = c.keywords.as_ref().unwrap();
1290 assert_eq!(kws.get("IETF").copied(), Some(false));
1291 }
1292
1293 #[test]
1311 fn malformed_json_returns_error() {
1312 let truncated = r#"{"id": "card-1""#;
1314 assert!(serde_json::from_str::<ContactCard>(truncated).is_err());
1315
1316 let mismatched = r#"{"id": "card-1"]"#;
1318 assert!(serde_json::from_str::<ContactCard>(mismatched).is_err());
1319
1320 let unterminated = r#"{"id": "card-1}"#;
1322 assert!(serde_json::from_str::<ContactCard>(unterminated).is_err());
1323 }
1324
1325 #[test]
1333 fn oversized_name_deserialises_to_typed_field() {
1334 let big_name = "x".repeat(1000);
1335 let raw = json!({
1336 "id": "ab-big-name",
1337 "name": big_name,
1338 "description": null,
1339 "sortOrder": 0,
1340 "isDefault": false,
1341 "isSubscribed": true,
1342 "shareWith": null,
1343 "myRights": {
1344 "mayRead": true, "mayWrite": false,
1345 "mayShare": false, "mayDelete": false
1346 }
1347 });
1348 let ab: AddressBook = serde_json::from_value(raw).unwrap();
1349 assert_eq!(ab.name.len(), 1000);
1350 }
1351
1352 #[test]
1359 fn sort_order_overflow_outside_spec_range_accepts() {
1360 let raw = json!({
1361 "id": "ab-overflow",
1362 "name": "Overflow",
1363 "description": null,
1364 "sortOrder": 2_147_483_648u64,
1365 "isDefault": false,
1366 "isSubscribed": true,
1367 "shareWith": null,
1368 "myRights": {
1369 "mayRead": true, "mayWrite": false,
1370 "mayShare": false, "mayDelete": false
1371 }
1372 });
1373 let ab: AddressBook = serde_json::from_value(raw).unwrap();
1374 assert_eq!(ab.sort_order, 2_147_483_648);
1375 }
1376
1377 #[test]
1385 fn sloppy_field_accepts_non_object_value() {
1386 let raw = json!({
1387 "id": "card-1",
1388 "name": "vincent"
1389 });
1390 let c: ContactCard = serde_json::from_value(raw).unwrap();
1391 let name_value = c.name.as_ref().unwrap();
1392 assert!(name_value.is_string(), "sloppy field preserved the string");
1393
1394 let typed = serde_json::from_value::<Name>(name_value.clone());
1397 assert!(typed.is_err(), "Name expects an object, got a string");
1398 }
1399
1400 #[test]
1405 fn deeply_nested_sloppy_field_within_serde_json_limit_succeeds() {
1406 let mut nested = String::from("1");
1409 for _ in 0..100 {
1410 nested = format!("{{\"x\":{nested}}}");
1411 }
1412 let payload = format!("{{\"id\":\"c\",\"name\":{nested}}}");
1413 let c: ContactCard = serde_json::from_str(&payload)
1414 .expect("100-deep nesting must be within serde_json's default 128 recursion limit");
1415 assert!(c.name.is_some());
1416 }
1417
1418 #[test]
1425 fn deeply_nested_sloppy_field_beyond_serde_json_limit_errors() {
1426 let mut nested = String::from("1");
1427 for _ in 0..200 {
1428 nested = format!("{{\"x\":{nested}}}");
1429 }
1430 let payload = format!("{{\"id\":\"c\",\"name\":{nested}}}");
1431 let result: Result<ContactCard, _> = serde_json::from_str(&payload);
1432 assert!(
1433 result.is_err(),
1434 "200-deep nesting must exceed serde_json's default 128 recursion limit"
1435 );
1436 }
1437
1438 #[test]
1447 fn duplicate_typed_field_keys_rejected() {
1448 let raw = r#"{"id":"card-1","uid":"first","uid":"second"}"#;
1449 let err = serde_json::from_str::<ContactCard>(raw)
1450 .expect_err("typed field duplicate must be rejected");
1451 let msg = err.to_string();
1452 assert!(
1453 msg.contains("duplicate field"),
1454 "expected duplicate-field error, got: {msg}"
1455 );
1456 }
1457
1458 #[test]
1467 fn duplicate_vendor_extra_keys_last_wins() {
1468 let raw = r#"{"id":"card-1","acmeCorpExternalId":"first","acmeCorpExternalId":"second"}"#;
1469 let c: ContactCard = serde_json::from_str(raw)
1470 .expect("vendor-extra duplicates fold into extra map, last-wins");
1471 assert_eq!(
1472 c.extra.get("acmeCorpExternalId").and_then(|v| v.as_str()),
1473 Some("second"),
1474 "extra map last-wins on duplicate keys"
1475 );
1476 }
1477
1478 #[test]
1489 fn contact_card_validate_extras_ok_on_clean() {
1490 let c = ContactCard::default();
1491 assert!(c.validate_extras().is_ok(), "empty extras must pass");
1492
1493 let mut c = ContactCard::default();
1494 c.extra.insert("acmeCorpFoo".into(), json!("bar"));
1495 assert!(c.validate_extras().is_ok(), "vendor-only key must pass");
1496 }
1497
1498 #[test]
1504 fn contact_card_validate_extras_detects_every_typed_field_collision() {
1505 let typed_fields = [
1508 "id",
1509 "addressBookIds",
1510 "version",
1511 "created",
1512 "kind",
1513 "language",
1514 "members",
1515 "prodId",
1516 "relatedTo",
1517 "uid",
1518 "updated",
1519 "name",
1520 "nicknames",
1521 "organizations",
1522 "speakToAs",
1523 "titles",
1524 "emails",
1525 "onlineServices",
1526 "phones",
1527 "preferredLanguages",
1528 "calendars",
1529 "schedulingAddresses",
1530 "addresses",
1531 "cryptoKeys",
1532 "directories",
1533 "links",
1534 "media",
1535 "localizations",
1536 "anniversaries",
1537 "keywords",
1538 "notes",
1539 "personalInfo",
1540 ];
1541 for field in typed_fields {
1542 let mut c = ContactCard::default();
1543 c.extra.insert(field.into(), json!("collision"));
1544 let err = c
1545 .validate_extras()
1546 .expect_err(&format!("{field} collision must be detected"));
1547 assert_eq!(err.keys(), &[field.to_string()]);
1548 }
1549 }
1550
1551 #[test]
1554 fn contact_card_validate_extras_reports_multiple_collisions() {
1555 let mut c = ContactCard::default();
1556 c.extra.insert("uid".into(), json!("a"));
1557 c.extra.insert("id".into(), json!("b"));
1558 c.extra.insert("acmeCorpFoo".into(), json!("c"));
1559 let err = c.validate_extras().expect_err("multi-collision must error");
1560 assert_eq!(err.keys(), &["id".to_string(), "uid".to_string()]);
1561 let msg = err.to_string();
1562 assert!(msg.contains("id, uid"), "got: {msg}");
1563 }
1564
1565 #[test]
1568 fn address_book_validate_extras_detects_every_typed_field_collision() {
1569 let typed_fields = [
1570 "id",
1571 "name",
1572 "description",
1573 "sortOrder",
1574 "isDefault",
1575 "isSubscribed",
1576 "shareWith",
1577 "myRights",
1578 ];
1579 let template = json!({
1582 "id": "ab-test",
1583 "name": "Test",
1584 "description": null,
1585 "sortOrder": 0,
1586 "isDefault": false,
1587 "isSubscribed": true,
1588 "shareWith": null,
1589 "myRights": {
1590 "mayRead": true, "mayWrite": false,
1591 "mayShare": false, "mayDelete": false
1592 }
1593 });
1594 let mut ab: AddressBook = serde_json::from_value(template).unwrap();
1595 assert!(ab.validate_extras().is_ok(), "clean extras pass");
1596
1597 for field in typed_fields {
1598 ab.extra.clear();
1599 ab.extra.insert(field.into(), json!("collision"));
1600 let err = ab
1601 .validate_extras()
1602 .expect_err(&format!("{field} collision must be detected"));
1603 assert_eq!(err.keys(), &[field.to_string()]);
1604 }
1605 }
1606
1607 #[test]
1610 fn address_book_rights_validate_extras_detects_every_typed_field_collision() {
1611 let typed_fields = ["mayRead", "mayWrite", "mayShare", "mayDelete"];
1612 let mut r = AddressBookRights::default();
1613 assert!(r.validate_extras().is_ok(), "clean extras pass");
1614
1615 for field in typed_fields {
1616 r.extra.clear();
1617 r.extra.insert(field.into(), json!(true));
1618 let err = r
1619 .validate_extras()
1620 .expect_err(&format!("{field} collision must be detected"));
1621 assert_eq!(err.keys(), &[field.to_string()]);
1622 }
1623 }
1624
1625 #[test]
1629 fn contact_card_maps_serialize_in_sorted_key_order() {
1630 let raw = r#"{"addressBookIds":{"ab2":true,"ab1":true},"localizations":{"fr":{"name/full":"Jean"},"de":{"name/full":"Johann"}}}"#;
1631 let card: ContactCard = serde_json::from_str(raw).expect("deserialize");
1632 let j = serde_json::to_string(&card).expect("serialize");
1633 assert_eq!(
1634 j,
1635 r#"{"addressBookIds":{"ab1":true,"ab2":true},"localizations":{"de":{"name/full":"Johann"},"fr":{"name/full":"Jean"}}}"#
1636 );
1637 }
1638
1639 #[test]
1643 fn address_book_rights_may_set_metadata_typed_and_optional() {
1644 let with = serde_json::json!({
1645 "mayRead": true, "mayWrite": false, "mayShare": false, "mayDelete": false,
1646 "maySetMetadata": true
1647 });
1648 let r: AddressBookRights = serde_json::from_value(with.clone()).unwrap();
1649 assert_eq!(r.may_set_metadata, Some(true));
1650 assert!(!r.extra.contains_key("maySetMetadata"));
1651 assert_eq!(serde_json::to_value(&r).unwrap(), with);
1652
1653 let without = serde_json::json!({
1654 "mayRead": true, "mayWrite": false, "mayShare": false, "mayDelete": false
1655 });
1656 let r: AddressBookRights = serde_json::from_value(without.clone()).unwrap();
1657 assert_eq!(r.may_set_metadata, None);
1658 assert_eq!(serde_json::to_value(&r).unwrap(), without);
1659 }
1660
1661 #[test]
1664 fn address_book_rights_extra_may_set_metadata_collides() {
1665 let mut r = AddressBookRights::default();
1666 r.extra
1667 .insert("maySetMetadata".to_owned(), serde_json::Value::Bool(true));
1668 assert!(r.validate_extras().is_err());
1669 }
1670}