1#![forbid(unsafe_code)]
100
101pub mod addressbook;
102pub mod backend;
103pub mod capability;
104pub mod card;
105pub mod collision;
106
107pub use jmap_jscontact_types as jscontact;
115
116pub use addressbook::{AddressBook, AddressBookRights};
117pub use backend::{AddressBookProperty, ContactCardProperty};
118pub use capability::{ContactsAccountCapability, ContactsCapability, JMAP_CONTACTS_URI};
119pub use card::{ContactCard, ContactCardComparator, ContactCardFilter, ContactCardFilterCondition};
120pub use collision::CollisionError;
121
122pub use card::prop as comparator_prop;
125
126pub use jmap_types::query::{Filter, FilterOperator, Operator};
135
136pub use jmap_jscontact_types::{
143 Address, AddressComponent, Anniversary, AnniversaryDate, Author, Calendar, CryptoKey,
144 Directory, EmailAddress, LanguagePref, Link, Media, Name, NameComponent, Nickname, Note,
145 OnlineService, OrgUnit, Organization, PartialDate, PersonalInfo, Phone, Pronouns, Relation,
146 SchedulingAddress, SpeakToAs, Timestamp, Title,
147};
148
149#[cfg(test)]
150mod tests {
151 use std::collections::HashMap;
152
153 use jmap_types::Id;
154 use serde_json::json;
155
156 use super::*;
157
158 #[test]
163 fn address_book_rights_default_is_all_false() {
164 let r = AddressBookRights::default();
165 assert!(!r.may_read, "may_read default should be false");
166 assert!(!r.may_write, "may_write default should be false");
167 assert!(!r.may_share, "may_share default should be false");
168 assert!(!r.may_delete, "may_delete default should be false");
169 }
170
171 #[test]
174 fn address_book_rights_deserialize() {
175 let json = r#"{
176 "mayRead": true,
177 "mayWrite": false,
178 "mayShare": false,
179 "mayDelete": false
180 }"#;
181 let r: AddressBookRights = serde_json::from_str(json).unwrap();
182 assert!(r.may_read);
183 assert!(!r.may_write);
184 assert!(!r.may_share);
185 assert!(!r.may_delete);
186 }
187
188 #[test]
191 fn address_book_rights_round_trip() {
192 let original = AddressBookRights {
193 may_read: true,
194 may_write: true,
195 may_share: false,
196 may_delete: false,
197 extra: serde_json::Map::new(),
198 };
199 let json = serde_json::to_string(&original).unwrap();
200 let decoded: AddressBookRights = serde_json::from_str(&json).unwrap();
201 assert_eq!(original, decoded);
202 }
203
204 #[test]
209 fn address_book_deserialize_spec_example() {
210 let json = r#"{
212 "id": "062adcfa-105d-455c-bc60-6db68b69c3f3",
213 "name": "Personal",
214 "description": null,
215 "sortOrder": 0,
216 "isDefault": true,
217 "isSubscribed": true,
218 "shareWith": {
219 "3f1502e0-63fe-4335-9ff3-e739c188f5dd": {
220 "mayRead": true,
221 "mayWrite": false,
222 "mayShare": false,
223 "mayDelete": false
224 }
225 },
226 "myRights": {
227 "mayRead": true,
228 "mayWrite": true,
229 "mayShare": true,
230 "mayDelete": false
231 }
232 }"#;
233 let ab: AddressBook = serde_json::from_str(json).unwrap();
234 assert_eq!(ab.name, "Personal");
235 assert!(ab.is_default);
236 assert!(ab.is_subscribed);
237 assert_eq!(ab.sort_order, 0);
238 assert!(ab.description.is_none());
239
240 let share_with = ab.share_with.unwrap();
241 let principal_id: Id = Id::from("3f1502e0-63fe-4335-9ff3-e739c188f5dd");
242 let shared_rights = &share_with[&principal_id];
243 assert!(shared_rights.may_read);
244 assert!(!shared_rights.may_write);
245
246 assert!(ab.my_rights.may_read);
247 assert!(ab.my_rights.may_write);
248 assert!(ab.my_rights.may_share);
249 assert!(!ab.my_rights.may_delete);
250 }
251
252 #[test]
255 fn address_book_share_with_null() {
256 let json = r#"{
257 "id": "cd40089d-35f9-4fd7-980b-ba3a9f1d74fe",
258 "name": "Autosaved",
259 "description": null,
260 "sortOrder": 1,
261 "isDefault": false,
262 "isSubscribed": true,
263 "shareWith": null,
264 "myRights": {
265 "mayRead": true,
266 "mayWrite": true,
267 "mayShare": true,
268 "mayDelete": false
269 }
270 }"#;
271 let ab: AddressBook = serde_json::from_str(json).unwrap();
272 assert_eq!(ab.name, "Autosaved");
273 assert!(ab.share_with.is_none());
274 }
275
276 #[test]
280 fn address_book_share_with_null_serializes() {
281 let json = r#"{
283 "id": "ab-null-sw",
284 "name": "Private",
285 "sortOrder": 0,
286 "isDefault": false,
287 "isSubscribed": true,
288 "shareWith": null,
289 "myRights": {
290 "mayRead": true,
291 "mayWrite": true,
292 "mayShare": false,
293 "mayDelete": false
294 }
295 }"#;
296 let ab: AddressBook = serde_json::from_str(json).expect("deserialize");
297 assert!(ab.share_with.is_none());
298
299 let serialized = serde_json::to_value(&ab).expect("serialize");
300 let obj = serialized.as_object().expect("object");
301 assert!(
302 obj.contains_key("shareWith"),
303 "shareWith must be present in wire JSON (required-nullable)"
304 );
305 assert!(
306 obj["shareWith"].is_null(),
307 "shareWith must serialize as null when None"
308 );
309 }
310
311 #[test]
314 fn address_book_share_with_some_serializes() {
315 let json = r#"{
317 "id": "062adcfa-105d-455c-bc60-6db68b69c3f3",
318 "name": "Personal",
319 "description": null,
320 "sortOrder": 0,
321 "isDefault": true,
322 "isSubscribed": true,
323 "shareWith": {
324 "3f1502e0-63fe-4335-9ff3-e739c188f5dd": {
325 "mayRead": true,
326 "mayWrite": false,
327 "mayShare": false,
328 "mayDelete": false
329 }
330 },
331 "myRights": {
332 "mayRead": true,
333 "mayWrite": true,
334 "mayShare": true,
335 "mayDelete": false
336 }
337 }"#;
338 let ab: AddressBook = serde_json::from_str(json).expect("deserialize");
339 assert!(ab.share_with.is_some());
340
341 let serialized = serde_json::to_value(&ab).expect("serialize");
342 let obj = serialized.as_object().expect("object");
343 assert!(
344 obj.contains_key("shareWith"),
345 "shareWith must be present when set"
346 );
347 assert!(
348 !obj["shareWith"].is_null(),
349 "shareWith must not be null when populated"
350 );
351 let back: AddressBook = serde_json::from_value(serialized).expect("round-trip deserialize");
353 assert_eq!(ab.share_with, back.share_with);
354 }
355
356 #[test]
358 fn address_book_round_trip_with_description() {
359 let original_json = json!({
361 "id": "ab1",
362 "name": "Work",
363 "description": "Work contacts",
364 "sortOrder": 5,
365 "isDefault": false,
366 "isSubscribed": true,
367 "myRights": {
368 "mayRead": true,
369 "mayWrite": false,
370 "mayShare": false,
371 "mayDelete": false
372 }
373 });
374 let ab: AddressBook = serde_json::from_value(original_json).unwrap();
375 assert_eq!(ab.description.as_deref(), Some("Work contacts"));
376 let rt: AddressBook = serde_json::from_str(&serde_json::to_string(&ab).unwrap()).unwrap();
377 assert_eq!(rt.description.as_deref(), Some("Work contacts"));
378 }
379
380 #[test]
386 fn address_book_description_null_serializes() {
387 let json = r#"{
388 "id": "AB1",
389 "name": "Personal",
390 "description": null,
391 "sortOrder": 0,
392 "isDefault": false,
393 "isSubscribed": false,
394 "shareWith": null,
395 "myRights": {
396 "mayRead": true,
397 "mayWrite": true,
398 "mayShare": false,
399 "mayDelete": false
400 }
401 }"#;
402 let ab: AddressBook = serde_json::from_str(json).expect("deserialize");
403 assert!(ab.description.is_none(), "description should be None");
404
405 let out = serde_json::to_value(&ab).expect("serialize");
406 assert!(
407 out.as_object().expect("object").contains_key("description"),
408 "description key must be present in wire JSON"
409 );
410 assert!(
411 out["description"].is_null(),
412 "description must serialize as null, not be absent"
413 );
414 }
415
416 #[test]
421 fn contact_card_deserialize_spec_example() {
422 let json = r#"{
424 "id": "3",
425 "addressBookIds": {
426 "062adcfa-105d-455c-bc60-6db68b69c3f3": true
427 },
428 "name": {
429 "components": [
430 { "kind": "given", "value": "Joe" },
431 { "kind": "surname", "value": "Bloggs" }
432 ],
433 "isOrdered": true
434 },
435 "emails": {
436 "0": {
437 "contexts": {
438 "private": true
439 },
440 "address": "joe.bloggs@example.com"
441 }
442 }
443 }"#;
444 let card: ContactCard = serde_json::from_str(json).unwrap();
445
446 let id = card.id.as_ref().unwrap();
447 assert_eq!(id.as_ref(), "3");
448
449 let ab_ids = card.address_book_ids.as_ref().unwrap();
450 let ab_key: Id = Id::from("062adcfa-105d-455c-bc60-6db68b69c3f3");
451 assert!(ab_ids[&ab_key]);
452
453 let name = card.name.as_ref().unwrap();
455 assert_eq!(name["isOrdered"], serde_json::Value::Bool(true));
456 assert_eq!(name["components"][0]["kind"], "given");
457 assert_eq!(name["components"][0]["value"], "Joe");
458
459 let emails = card.emails.as_ref().unwrap();
461 assert_eq!(emails["0"]["address"], "joe.bloggs@example.com");
462 }
463
464 #[test]
467 fn contact_card_group_with_members() {
468 let json = r#"{
469 "version": "1.0",
470 "uid": "urn:uuid:ab4310aa-fa43-11e9-8f0b-362b9e155667",
471 "kind": "group",
472 "name": { "full": "The Doe family" },
473 "members": {
474 "urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af": true,
475 "urn:uuid:b8767877-b4a1-4c70-9acc-505d3819e519": true
476 }
477 }"#;
478 let card: ContactCard = serde_json::from_str(json).unwrap();
479 assert_eq!(card.kind.as_deref(), Some("group"));
480 let members = card.members.as_ref().unwrap();
481 assert!(members["urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af"]);
482 assert_eq!(members.len(), 2);
483 }
484
485 #[test]
487 fn contact_card_minimal_fields() {
488 let json = r#"{
489 "id": "minimal1",
490 "addressBookIds": { "ab1": true }
491 }"#;
492 let card: ContactCard = serde_json::from_str(json).unwrap();
493 assert!(card.version.is_none());
494 assert!(card.uid.is_none());
495 assert!(card.kind.is_none());
496 assert!(card.emails.is_none());
497 assert!(card.phones.is_none());
498 assert!(card.addresses.is_none());
499 }
500
501 #[test]
503 fn contact_card_serializes_only_present_fields() {
504 let card: ContactCard = serde_json::from_value(json!({
506 "id": "x1",
507 "addressBookIds": { "ab1": true },
508 "version": "1.0",
509 "uid": "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
510 }))
511 .unwrap();
512
513 let serialized = serde_json::to_value(&card).unwrap();
514 assert!(serialized.get("id").is_some());
516 assert!(serialized.get("version").is_some());
517 assert!(serialized.get("uid").is_some());
518 assert!(serialized.get("emails").is_none());
520 assert!(serialized.get("phones").is_none());
521 assert!(serialized.get("addresses").is_none());
522 assert!(serialized.get("kind").is_none());
523 }
524
525 #[test]
532 fn contact_card_localizations_patch_object_transparent() {
533 let json = r#"{"localizations":{"de":{"name/full":"Joe Bloggs (DE)"}}}"#;
534 let card: ContactCard = serde_json::from_str(json).expect("deserialize");
535
536 let locs = card.localizations.as_ref().expect("localizations present");
537 let de = locs.get("de").expect("de locale present");
538 assert_eq!(
539 de.as_map().get("name/full").and_then(|v| v.as_str()),
540 Some("Joe Bloggs (DE)"),
541 );
542
543 let serialized = serde_json::to_string(&card).expect("serialize");
544 assert_eq!(serialized, json);
545 }
546
547 #[test]
553 fn filter_condition_slash_keys_serialize_correctly() {
554 let filter: ContactCardFilterCondition = serde_json::from_value(json!({
555 "name/given": "John",
556 "name/surname": "Smith",
557 "name/surname2": "Doe"
558 }))
559 .unwrap();
560
561 assert_eq!(filter.name_given.as_deref(), Some("John"));
562 assert_eq!(filter.name_surname.as_deref(), Some("Smith"));
563 assert_eq!(filter.name_surname2.as_deref(), Some("Doe"));
564
565 let serialized = serde_json::to_value(&filter).unwrap();
566 assert_eq!(serialized["name/given"], "John");
567 assert_eq!(serialized["name/surname"], "Smith");
568 assert_eq!(serialized["name/surname2"], "Doe");
569 }
570
571 #[test]
574 fn filter_condition_all_fields() {
575 let json = r#"{
576 "inAddressBook": "ab1",
577 "uid": "urn:uuid:abc123",
578 "hasMember": "urn:uuid:def456",
579 "kind": "individual",
580 "createdBefore": "2024-01-01T00:00:00Z",
581 "createdAfter": "2020-01-01T00:00:00Z",
582 "updatedBefore": "2024-06-01T00:00:00Z",
583 "updatedAfter": "2021-06-01T00:00:00Z",
584 "text": "John",
585 "name": "Doe",
586 "name/given": "Jane",
587 "name/surname": "Smith",
588 "name/surname2": "Gomez",
589 "nickname": "Johnny",
590 "organization": "ACME",
591 "email": "john@example.com",
592 "phone": "+1-555-1234",
593 "onlineService": "twitter",
594 "address": "Main St",
595 "note": "VIP"
596 }"#;
597 let f: ContactCardFilterCondition = serde_json::from_str(json).unwrap();
598 assert_eq!(
599 f.in_address_book.as_ref().map(|id| id.as_ref()),
600 Some("ab1")
601 );
602 assert_eq!(f.uid.as_deref(), Some("urn:uuid:abc123"));
603 assert_eq!(f.has_member.as_deref(), Some("urn:uuid:def456"));
604 assert_eq!(f.kind.as_deref(), Some("individual"));
605 assert_eq!(
606 f.created_before.as_ref().map(AsRef::as_ref),
607 Some("2024-01-01T00:00:00Z")
608 );
609 assert_eq!(
610 f.created_after.as_ref().map(AsRef::as_ref),
611 Some("2020-01-01T00:00:00Z")
612 );
613 assert_eq!(
614 f.updated_before.as_ref().map(AsRef::as_ref),
615 Some("2024-06-01T00:00:00Z")
616 );
617 assert_eq!(
618 f.updated_after.as_ref().map(AsRef::as_ref),
619 Some("2021-06-01T00:00:00Z")
620 );
621 assert_eq!(f.text.as_deref(), Some("John"));
622 assert_eq!(f.name.as_deref(), Some("Doe"));
623 assert_eq!(f.name_given.as_deref(), Some("Jane"));
624 assert_eq!(f.name_surname.as_deref(), Some("Smith"));
625 assert_eq!(f.name_surname2.as_deref(), Some("Gomez"));
626 assert_eq!(f.nickname.as_deref(), Some("Johnny"));
627 assert_eq!(f.organization.as_deref(), Some("ACME"));
628 assert_eq!(f.email.as_deref(), Some("john@example.com"));
629 assert_eq!(f.phone.as_deref(), Some("+1-555-1234"));
630 assert_eq!(f.online_service.as_deref(), Some("twitter"));
631 assert_eq!(f.address.as_deref(), Some("Main St"));
632 assert_eq!(f.note.as_deref(), Some("VIP"));
633 }
634
635 #[test]
637 fn filter_condition_empty_serializes_to_empty_object() {
638 let f = ContactCardFilterCondition::default();
639 let v = serde_json::to_value(&f).unwrap();
640 assert_eq!(v, json!({}));
641 }
642
643 #[test]
648 fn contacts_capability_is_empty_object() {
649 let cap = ContactsCapability::default();
650 let v = serde_json::to_value(&cap).unwrap();
651 assert_eq!(v, json!({}));
652 }
653
654 #[test]
657 fn contacts_account_capability_deserialize() {
658 let json = r#"{
659 "maxAddressBooksPerCard": 5,
660 "mayCreateAddressBook": true
661 }"#;
662 let cap: ContactsAccountCapability = serde_json::from_str(json).unwrap();
663 assert_eq!(cap.max_address_books_per_card, Some(5));
664 assert!(cap.may_create_address_book);
665 }
666
667 #[test]
670 fn contacts_account_capability_null_max() {
671 let json = r#"{
672 "maxAddressBooksPerCard": null,
673 "mayCreateAddressBook": false
674 }"#;
675 let cap: ContactsAccountCapability = serde_json::from_str(json).unwrap();
676 assert!(cap.max_address_books_per_card.is_none());
677 assert!(!cap.may_create_address_book);
678 }
679
680 #[test]
682 fn jmap_contacts_uri_value() {
683 assert_eq!(JMAP_CONTACTS_URI, "urn:ietf:params:jmap:contacts");
684 }
685
686 #[test]
691 fn comparator_is_ascending_defaults_to_true() {
692 let json = r#"{ "property": "created" }"#;
693 let c: ContactCardComparator = serde_json::from_str(json).unwrap();
694 assert!(c.is_ascending);
695 assert!(c.collation.is_none());
696 }
697
698 #[test]
700 fn comparator_round_trip() {
701 let json = r#"{ "property": "name/given", "isAscending": false, "collation": "i;unicode-casemap" }"#;
702 let c: ContactCardComparator = serde_json::from_str(json).unwrap();
703 assert_eq!(c.property, "name/given");
704 assert!(!c.is_ascending);
705 assert_eq!(c.collation.as_deref(), Some("i;unicode-casemap"));
706 let rt: ContactCardComparator =
707 serde_json::from_str(&serde_json::to_string(&c).unwrap()).unwrap();
708 assert_eq!(rt.property, "name/given");
709 assert!(!rt.is_ascending);
710 }
711
712 #[test]
727 fn sloppy_field_name_roundtrips_through_jscontact_name() {
728 let card_json = json!({
729 "name": {
730 "components": [
731 { "kind": "given", "value": "Vincent" },
732 { "kind": "surname", "value": "van Gogh" }
733 ],
734 "isOrdered": true
735 }
736 });
737 let card: ContactCard = serde_json::from_value(card_json).unwrap();
738 let name: Name = serde_json::from_value(card.name.clone().unwrap()).unwrap();
739 let back = serde_json::to_value(&name).unwrap();
740 assert_eq!(back, card.name.unwrap());
741 }
742
743 #[test]
746 fn sloppy_field_nicknames_roundtrips_through_jscontact_nickname_map() {
747 let card_json = json!({
748 "nicknames": {
749 "k391": { "name": "Johnny" }
750 }
751 });
752 let card: ContactCard = serde_json::from_value(card_json).unwrap();
753 let nicks: HashMap<String, Nickname> =
754 serde_json::from_value(card.nicknames.clone().unwrap()).unwrap();
755 assert_eq!(nicks["k391"].name, "Johnny");
756 let back = serde_json::to_value(&nicks).unwrap();
757 assert_eq!(back, card.nicknames.unwrap());
758 }
759
760 #[test]
763 fn sloppy_field_organizations_roundtrips_through_jscontact_organization_map() {
764 let card_json = json!({
765 "organizations": {
766 "o1": {
767 "name": "ABC, Inc.",
768 "units": [
769 { "name": "North American Division" },
770 { "name": "Marketing" }
771 ],
772 "sortAs": "ABC"
773 }
774 }
775 });
776 let card: ContactCard = serde_json::from_value(card_json).unwrap();
777 let orgs: HashMap<String, Organization> =
778 serde_json::from_value(card.organizations.clone().unwrap()).unwrap();
779 assert_eq!(orgs["o1"].name.as_deref(), Some("ABC, Inc."));
780 let back = serde_json::to_value(&orgs).unwrap();
781 assert_eq!(back, card.organizations.unwrap());
782 }
783
784 #[test]
787 fn sloppy_field_speak_to_as_roundtrips_through_jscontact_speak_to_as() {
788 let card_json = json!({
789 "speakToAs": {
790 "grammaticalGender": "neuter",
791 "pronouns": {
792 "k19": { "pronouns": "they/them", "pref": 2 }
793 }
794 }
795 });
796 let card: ContactCard = serde_json::from_value(card_json).unwrap();
797 let speak: SpeakToAs = serde_json::from_value(card.speak_to_as.clone().unwrap()).unwrap();
798 let back = serde_json::to_value(&speak).unwrap();
799 assert_eq!(back, card.speak_to_as.unwrap());
800 }
801
802 #[test]
805 fn sloppy_field_titles_roundtrips_through_jscontact_title_map() {
806 let card_json = json!({
807 "titles": {
808 "le9": { "kind": "title", "name": "Research Scientist" }
809 }
810 });
811 let card: ContactCard = serde_json::from_value(card_json).unwrap();
812 let titles: HashMap<String, Title> =
813 serde_json::from_value(card.titles.clone().unwrap()).unwrap();
814 assert_eq!(titles["le9"].name, "Research Scientist");
815 let back = serde_json::to_value(&titles).unwrap();
816 assert_eq!(back, card.titles.unwrap());
817 }
818
819 #[test]
822 fn sloppy_field_emails_roundtrips_through_jscontact_email_address_map() {
823 let card_json = json!({
824 "emails": {
825 "e1": { "contexts": { "work": true }, "address": "jqpublic@xyz.example.com" }
826 }
827 });
828 let card: ContactCard = serde_json::from_value(card_json).unwrap();
829 let emails: HashMap<String, EmailAddress> =
830 serde_json::from_value(card.emails.clone().unwrap()).unwrap();
831 assert_eq!(emails["e1"].address, "jqpublic@xyz.example.com");
832 let back = serde_json::to_value(&emails).unwrap();
833 assert_eq!(back, card.emails.unwrap());
834 }
835
836 #[test]
839 fn sloppy_field_online_services_roundtrips_through_jscontact_online_service_map() {
840 let card_json = json!({
841 "onlineServices": {
842 "x1": { "uri": "xmpp:alice@example.com" }
843 }
844 });
845 let card: ContactCard = serde_json::from_value(card_json).unwrap();
846 let svcs: HashMap<String, OnlineService> =
847 serde_json::from_value(card.online_services.clone().unwrap()).unwrap();
848 let back = serde_json::to_value(&svcs).unwrap();
849 assert_eq!(back, card.online_services.unwrap());
850 }
851
852 #[test]
855 fn sloppy_field_phones_roundtrips_through_jscontact_phone_map() {
856 let card_json = json!({
857 "phones": {
858 "tel0": {
859 "contexts": { "private": true },
860 "features": { "voice": true },
861 "number": "tel:+1-555-555-5555;ext=5555",
862 "pref": 1
863 }
864 }
865 });
866 let card: ContactCard = serde_json::from_value(card_json).unwrap();
867 let phones: HashMap<String, Phone> =
868 serde_json::from_value(card.phones.clone().unwrap()).unwrap();
869 assert_eq!(phones["tel0"].number, "tel:+1-555-555-5555;ext=5555");
870 let back = serde_json::to_value(&phones).unwrap();
871 assert_eq!(back, card.phones.unwrap());
872 }
873
874 #[test]
877 fn sloppy_field_preferred_languages_roundtrips_through_jscontact_language_pref_map() {
878 let card_json = json!({
879 "preferredLanguages": {
880 "l1": { "language": "en", "contexts": { "work": true }, "pref": 1 }
881 }
882 });
883 let card: ContactCard = serde_json::from_value(card_json).unwrap();
884 let langs: HashMap<String, LanguagePref> =
885 serde_json::from_value(card.preferred_languages.clone().unwrap()).unwrap();
886 assert_eq!(langs["l1"].language, "en");
887 let back = serde_json::to_value(&langs).unwrap();
888 assert_eq!(back, card.preferred_languages.unwrap());
889 }
890
891 #[test]
894 fn sloppy_field_calendars_roundtrips_through_jscontact_calendar_map() {
895 let card_json = json!({
896 "calendars": {
897 "calA": { "kind": "calendar", "uri": "webcal://calendar.example.com/calA.ics" }
898 }
899 });
900 let card: ContactCard = serde_json::from_value(card_json).unwrap();
901 let cals: HashMap<String, Calendar> =
902 serde_json::from_value(card.calendars.clone().unwrap()).unwrap();
903 let back = serde_json::to_value(&cals).unwrap();
904 assert_eq!(back, card.calendars.unwrap());
905 }
906
907 #[test]
910 fn sloppy_field_scheduling_addresses_roundtrips_through_jscontact_scheduling_address_map() {
911 let card_json = json!({
912 "schedulingAddresses": {
913 "sched1": { "uri": "mailto:janedoe@example.com" }
914 }
915 });
916 let card: ContactCard = serde_json::from_value(card_json).unwrap();
917 let scheds: HashMap<String, SchedulingAddress> =
918 serde_json::from_value(card.scheduling_addresses.clone().unwrap()).unwrap();
919 assert_eq!(scheds["sched1"].uri, "mailto:janedoe@example.com");
920 let back = serde_json::to_value(&scheds).unwrap();
921 assert_eq!(back, card.scheduling_addresses.unwrap());
922 }
923
924 #[test]
927 fn sloppy_field_addresses_roundtrips_through_jscontact_address_map() {
928 let card_json = json!({
929 "addresses": {
930 "k23": {
931 "contexts": { "work": true },
932 "components": [
933 { "kind": "number", "value": "54321" },
934 { "kind": "name", "value": "Oak St" }
935 ],
936 "countryCode": "US",
937 "isOrdered": true
938 }
939 }
940 });
941 let card: ContactCard = serde_json::from_value(card_json).unwrap();
942 let addrs: HashMap<String, Address> =
943 serde_json::from_value(card.addresses.clone().unwrap()).unwrap();
944 assert_eq!(addrs["k23"].country_code.as_deref(), Some("US"));
945 let back = serde_json::to_value(&addrs).unwrap();
946 assert_eq!(back, card.addresses.unwrap());
947 }
948
949 #[test]
952 fn sloppy_field_crypto_keys_roundtrips_through_jscontact_crypto_key_map() {
953 let card_json = json!({
954 "cryptoKeys": {
955 "mykey1": { "uri": "https://www.example.com/keys/jdoe.cer" }
956 }
957 });
958 let card: ContactCard = serde_json::from_value(card_json).unwrap();
959 let keys: HashMap<String, CryptoKey> =
960 serde_json::from_value(card.crypto_keys.clone().unwrap()).unwrap();
961 let back = serde_json::to_value(&keys).unwrap();
962 assert_eq!(back, card.crypto_keys.unwrap());
963 }
964
965 #[test]
968 fn sloppy_field_directories_roundtrips_through_jscontact_directory_map() {
969 let card_json = json!({
970 "directories": {
971 "dir1": {
972 "kind": "entry",
973 "uri": "https://dir.example.com/addrbook/jdoe/Jean%20Dupont.vcf"
974 }
975 }
976 });
977 let card: ContactCard = serde_json::from_value(card_json).unwrap();
978 let dirs: HashMap<String, Directory> =
979 serde_json::from_value(card.directories.clone().unwrap()).unwrap();
980 assert_eq!(dirs["dir1"].kind.as_deref(), Some("entry"));
981 let back = serde_json::to_value(&dirs).unwrap();
982 assert_eq!(back, card.directories.unwrap());
983 }
984
985 #[test]
988 fn sloppy_field_links_roundtrips_through_jscontact_link_map() {
989 let card_json = json!({
990 "links": {
991 "link3": { "kind": "contact", "uri": "mailto:contact@example.com", "pref": 1 }
992 }
993 });
994 let card: ContactCard = serde_json::from_value(card_json).unwrap();
995 let links: HashMap<String, Link> =
996 serde_json::from_value(card.links.clone().unwrap()).unwrap();
997 let back = serde_json::to_value(&links).unwrap();
998 assert_eq!(back, card.links.unwrap());
999 }
1000
1001 #[test]
1004 fn sloppy_field_media_roundtrips_through_jscontact_media_map() {
1005 let card_json = json!({
1006 "media": {
1007 "res47": { "kind": "logo", "uri": "https://www.example.com/pub/logos/abccorp.jpg" }
1008 }
1009 });
1010 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1011 let media: HashMap<String, Media> =
1012 serde_json::from_value(card.media.clone().unwrap()).unwrap();
1013 assert_eq!(media["res47"].kind.as_deref(), Some("logo"));
1014 let back = serde_json::to_value(&media).unwrap();
1015 assert_eq!(back, card.media.unwrap());
1016 }
1017
1018 #[test]
1021 fn sloppy_field_anniversaries_roundtrips_through_jscontact_anniversary_map() {
1022 let card_json = json!({
1023 "anniversaries": {
1024 "k8": {
1025 "kind": "birth",
1026 "date": { "year": 1953, "month": 4, "day": 15 }
1027 }
1028 }
1029 });
1030 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1031 let annivs: HashMap<String, Anniversary> =
1032 serde_json::from_value(card.anniversaries.clone().unwrap()).unwrap();
1033 assert_eq!(annivs["k8"].kind, "birth");
1034 let back = serde_json::to_value(&annivs).unwrap();
1035 assert_eq!(back, card.anniversaries.unwrap());
1036 }
1037
1038 #[test]
1041 fn sloppy_field_notes_roundtrips_through_jscontact_note_map() {
1042 let card_json = json!({
1043 "notes": {
1044 "n1": {
1045 "note": "Open office hours are 1600 to 1715 EST, Mon-Fri",
1046 "created": "2022-11-23T15:01:32Z",
1047 "author": { "name": "John" }
1048 }
1049 }
1050 });
1051 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1052 let notes: HashMap<String, Note> =
1053 serde_json::from_value(card.notes.clone().unwrap()).unwrap();
1054 assert_eq!(
1055 notes["n1"].author.as_ref().unwrap().name.as_deref(),
1056 Some("John")
1057 );
1058 let back = serde_json::to_value(¬es).unwrap();
1059 assert_eq!(back, card.notes.unwrap());
1060 }
1061
1062 #[test]
1065 fn sloppy_field_personal_info_roundtrips_through_jscontact_personal_info_map() {
1066 let card_json = json!({
1067 "personalInfo": {
1068 "pi2": { "kind": "expertise", "value": "chemistry", "level": "high" }
1069 }
1070 });
1071 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1072 let info: HashMap<String, PersonalInfo> =
1073 serde_json::from_value(card.personal_info.clone().unwrap()).unwrap();
1074 assert_eq!(info["pi2"].kind, "expertise");
1075 let back = serde_json::to_value(&info).unwrap();
1076 assert_eq!(back, card.personal_info.unwrap());
1077 }
1078
1079 #[test]
1082 fn sloppy_field_related_to_roundtrips_through_jscontact_relation_map() {
1083 let card_json = json!({
1084 "relatedTo": {
1085 "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6": {
1086 "relation": { "friend": true }
1087 }
1088 }
1089 });
1090 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1091 let rels: HashMap<String, Relation> =
1092 serde_json::from_value(card.related_to.clone().unwrap()).unwrap();
1093 let back = serde_json::to_value(&rels).unwrap();
1094 assert_eq!(back, card.related_to.unwrap());
1095 }
1096
1097 #[test]
1106 fn keywords_roundtrips_as_string_bool_map() {
1107 let card_json = json!({
1108 "keywords": {
1109 "internet": true,
1110 "IETF": true
1111 }
1112 });
1113 let card: ContactCard = serde_json::from_value(card_json).unwrap();
1114 let kws = card.keywords.as_ref().expect("keywords present");
1115 assert!(kws["internet"]);
1116 assert!(kws["IETF"]);
1117 assert_eq!(kws.len(), 2);
1118 }
1119
1120 #[test]
1130 fn jscontact_module_alias_is_jmap_jscontact_types() {
1131 const _ASSERT_ALIASED_SAME_AS_DIRECT: fn(
1132 jmap_jscontact_types::Name,
1133 ) -> crate::jscontact::Name = |x| x;
1134 const _ASSERT_TOP_LEVEL_SAME_AS_DIRECT: fn(jmap_jscontact_types::Name) -> Name = |x| x;
1135
1136 let v = json!({ "full": "Vincent van Gogh" });
1140 let _direct: jmap_jscontact_types::Name = serde_json::from_value(v.clone()).unwrap();
1141 let _aliased: crate::jscontact::Name = serde_json::from_value(v.clone()).unwrap();
1142 let _top_level: Name = serde_json::from_value(v).unwrap();
1143 }
1144
1145 #[test]
1149 fn address_book_rights_preserves_vendor_extras() {
1150 let raw = json!({
1151 "mayRead": true,
1152 "mayWrite": false,
1153 "mayShare": false,
1154 "mayDelete": false,
1155 "acmeCorpMayMerge": true
1156 });
1157 let r: AddressBookRights = serde_json::from_value(raw).unwrap();
1158 assert_eq!(
1159 r.extra.get("acmeCorpMayMerge").and_then(|v| v.as_bool()),
1160 Some(true)
1161 );
1162 let back = serde_json::to_value(&r).unwrap();
1163 assert_eq!(back["acmeCorpMayMerge"], true);
1164 }
1165
1166 #[test]
1168 fn address_book_preserves_vendor_extras() {
1169 let raw = json!({
1170 "id": "ab1",
1171 "name": "Personal",
1172 "description": null,
1173 "sortOrder": 0,
1174 "isDefault": true,
1175 "isSubscribed": true,
1176 "shareWith": null,
1177 "myRights": {
1178 "mayRead": true, "mayWrite": true,
1179 "mayShare": false, "mayDelete": false
1180 },
1181 "acmeCorpRetentionDays": 365
1182 });
1183 let ab: AddressBook = serde_json::from_value(raw).unwrap();
1184 assert_eq!(
1185 ab.extra
1186 .get("acmeCorpRetentionDays")
1187 .and_then(|v| v.as_u64()),
1188 Some(365)
1189 );
1190 let back = serde_json::to_value(&ab).unwrap();
1191 assert_eq!(back["acmeCorpRetentionDays"], 365);
1192 }
1193
1194 #[test]
1196 fn contact_card_preserves_vendor_extras() {
1197 let raw = json!({
1198 "uid": "card-1",
1199 "version": "1.0",
1200 "acmeCorpExternalId": "ldap-42"
1201 });
1202 let c: ContactCard = serde_json::from_value(raw).unwrap();
1203 assert_eq!(
1204 c.extra.get("acmeCorpExternalId").and_then(|v| v.as_str()),
1205 Some("ldap-42")
1206 );
1207 let back = serde_json::to_value(&c).unwrap();
1208 assert_eq!(back["acmeCorpExternalId"], "ldap-42");
1209 }
1210
1211 #[test]
1225 fn contact_card_address_book_ids_accepts_false_deserialise() {
1226 let raw = json!({
1227 "id": "card-1",
1228 "addressBookIds": { "ab1": false }
1229 });
1230 let c: ContactCard = serde_json::from_value(raw).unwrap();
1231 let ids = c.address_book_ids.as_ref().unwrap();
1232 let ab1: Id = Id::from("ab1");
1233 assert_eq!(ids.get(&ab1).copied(), Some(false));
1235 }
1236
1237 #[test]
1240 fn contact_card_members_accepts_false_deserialise() {
1241 let raw = json!({
1242 "id": "card-1",
1243 "kind": "group",
1244 "members": { "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6": false }
1245 });
1246 let c: ContactCard = serde_json::from_value(raw).unwrap();
1247 let mems = c.members.as_ref().unwrap();
1248 assert_eq!(
1249 mems.get("urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6")
1250 .copied(),
1251 Some(false),
1252 );
1253 }
1254
1255 #[test]
1258 fn contact_card_keywords_accepts_false_deserialise() {
1259 let raw = json!({
1260 "id": "card-1",
1261 "keywords": { "IETF": false }
1262 });
1263 let c: ContactCard = serde_json::from_value(raw).unwrap();
1264 let kws = c.keywords.as_ref().unwrap();
1265 assert_eq!(kws.get("IETF").copied(), Some(false));
1266 }
1267
1268 #[test]
1286 fn malformed_json_returns_error() {
1287 let truncated = r#"{"id": "card-1""#;
1289 assert!(serde_json::from_str::<ContactCard>(truncated).is_err());
1290
1291 let mismatched = r#"{"id": "card-1"]"#;
1293 assert!(serde_json::from_str::<ContactCard>(mismatched).is_err());
1294
1295 let unterminated = r#"{"id": "card-1}"#;
1297 assert!(serde_json::from_str::<ContactCard>(unterminated).is_err());
1298 }
1299
1300 #[test]
1308 fn oversized_name_deserialises_to_typed_field() {
1309 let big_name = "x".repeat(1000);
1310 let raw = json!({
1311 "id": "ab-big-name",
1312 "name": big_name,
1313 "description": null,
1314 "sortOrder": 0,
1315 "isDefault": false,
1316 "isSubscribed": true,
1317 "shareWith": null,
1318 "myRights": {
1319 "mayRead": true, "mayWrite": false,
1320 "mayShare": false, "mayDelete": false
1321 }
1322 });
1323 let ab: AddressBook = serde_json::from_value(raw).unwrap();
1324 assert_eq!(ab.name.len(), 1000);
1325 }
1326
1327 #[test]
1334 fn sort_order_overflow_outside_spec_range_accepts() {
1335 let raw = json!({
1336 "id": "ab-overflow",
1337 "name": "Overflow",
1338 "description": null,
1339 "sortOrder": 2_147_483_648u64,
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.sort_order, 2_147_483_648);
1350 }
1351
1352 #[test]
1360 fn sloppy_field_accepts_non_object_value() {
1361 let raw = json!({
1362 "id": "card-1",
1363 "name": "vincent"
1364 });
1365 let c: ContactCard = serde_json::from_value(raw).unwrap();
1366 let name_value = c.name.as_ref().unwrap();
1367 assert!(name_value.is_string(), "sloppy field preserved the string");
1368
1369 let typed = serde_json::from_value::<Name>(name_value.clone());
1372 assert!(typed.is_err(), "Name expects an object, got a string");
1373 }
1374
1375 #[test]
1380 fn deeply_nested_sloppy_field_within_serde_json_limit_succeeds() {
1381 let mut nested = String::from("1");
1384 for _ in 0..100 {
1385 nested = format!("{{\"x\":{nested}}}");
1386 }
1387 let payload = format!("{{\"id\":\"c\",\"name\":{nested}}}");
1388 let c: ContactCard = serde_json::from_str(&payload)
1389 .expect("100-deep nesting must be within serde_json's default 128 recursion limit");
1390 assert!(c.name.is_some());
1391 }
1392
1393 #[test]
1400 fn deeply_nested_sloppy_field_beyond_serde_json_limit_errors() {
1401 let mut nested = String::from("1");
1402 for _ in 0..200 {
1403 nested = format!("{{\"x\":{nested}}}");
1404 }
1405 let payload = format!("{{\"id\":\"c\",\"name\":{nested}}}");
1406 let result: Result<ContactCard, _> = serde_json::from_str(&payload);
1407 assert!(
1408 result.is_err(),
1409 "200-deep nesting must exceed serde_json's default 128 recursion limit"
1410 );
1411 }
1412
1413 #[test]
1422 fn duplicate_typed_field_keys_rejected() {
1423 let raw = r#"{"id":"card-1","uid":"first","uid":"second"}"#;
1424 let err = serde_json::from_str::<ContactCard>(raw)
1425 .expect_err("typed field duplicate must be rejected");
1426 let msg = err.to_string();
1427 assert!(
1428 msg.contains("duplicate field"),
1429 "expected duplicate-field error, got: {msg}"
1430 );
1431 }
1432
1433 #[test]
1442 fn duplicate_vendor_extra_keys_last_wins() {
1443 let raw = r#"{"id":"card-1","acmeCorpExternalId":"first","acmeCorpExternalId":"second"}"#;
1444 let c: ContactCard = serde_json::from_str(raw)
1445 .expect("vendor-extra duplicates fold into extra map, last-wins");
1446 assert_eq!(
1447 c.extra.get("acmeCorpExternalId").and_then(|v| v.as_str()),
1448 Some("second"),
1449 "extra map last-wins on duplicate keys"
1450 );
1451 }
1452
1453 #[test]
1464 fn contact_card_validate_extras_ok_on_clean() {
1465 let c = ContactCard::default();
1466 assert!(c.validate_extras().is_ok(), "empty extras must pass");
1467
1468 let mut c = ContactCard::default();
1469 c.extra.insert("acmeCorpFoo".into(), json!("bar"));
1470 assert!(c.validate_extras().is_ok(), "vendor-only key must pass");
1471 }
1472
1473 #[test]
1479 fn contact_card_validate_extras_detects_every_typed_field_collision() {
1480 let typed_fields = [
1483 "id",
1484 "addressBookIds",
1485 "version",
1486 "created",
1487 "kind",
1488 "language",
1489 "members",
1490 "prodId",
1491 "relatedTo",
1492 "uid",
1493 "updated",
1494 "name",
1495 "nicknames",
1496 "organizations",
1497 "speakToAs",
1498 "titles",
1499 "emails",
1500 "onlineServices",
1501 "phones",
1502 "preferredLanguages",
1503 "calendars",
1504 "schedulingAddresses",
1505 "addresses",
1506 "cryptoKeys",
1507 "directories",
1508 "links",
1509 "media",
1510 "localizations",
1511 "anniversaries",
1512 "keywords",
1513 "notes",
1514 "personalInfo",
1515 ];
1516 for field in typed_fields {
1517 let mut c = ContactCard::default();
1518 c.extra.insert(field.into(), json!("collision"));
1519 let err = c
1520 .validate_extras()
1521 .expect_err(&format!("{field} collision must be detected"));
1522 assert_eq!(err.keys(), &[field.to_string()]);
1523 }
1524 }
1525
1526 #[test]
1529 fn contact_card_validate_extras_reports_multiple_collisions() {
1530 let mut c = ContactCard::default();
1531 c.extra.insert("uid".into(), json!("a"));
1532 c.extra.insert("id".into(), json!("b"));
1533 c.extra.insert("acmeCorpFoo".into(), json!("c"));
1534 let err = c.validate_extras().expect_err("multi-collision must error");
1535 assert_eq!(err.keys(), &["id".to_string(), "uid".to_string()]);
1536 let msg = err.to_string();
1537 assert!(msg.contains("id, uid"), "got: {msg}");
1538 }
1539
1540 #[test]
1543 fn address_book_validate_extras_detects_every_typed_field_collision() {
1544 let typed_fields = [
1545 "id",
1546 "name",
1547 "description",
1548 "sortOrder",
1549 "isDefault",
1550 "isSubscribed",
1551 "shareWith",
1552 "myRights",
1553 ];
1554 let template = json!({
1557 "id": "ab-test",
1558 "name": "Test",
1559 "description": null,
1560 "sortOrder": 0,
1561 "isDefault": false,
1562 "isSubscribed": true,
1563 "shareWith": null,
1564 "myRights": {
1565 "mayRead": true, "mayWrite": false,
1566 "mayShare": false, "mayDelete": false
1567 }
1568 });
1569 let mut ab: AddressBook = serde_json::from_value(template).unwrap();
1570 assert!(ab.validate_extras().is_ok(), "clean extras pass");
1571
1572 for field in typed_fields {
1573 ab.extra.clear();
1574 ab.extra.insert(field.into(), json!("collision"));
1575 let err = ab
1576 .validate_extras()
1577 .expect_err(&format!("{field} collision must be detected"));
1578 assert_eq!(err.keys(), &[field.to_string()]);
1579 }
1580 }
1581
1582 #[test]
1585 fn address_book_rights_validate_extras_detects_every_typed_field_collision() {
1586 let typed_fields = ["mayRead", "mayWrite", "mayShare", "mayDelete"];
1587 let mut r = AddressBookRights::default();
1588 assert!(r.validate_extras().is_ok(), "clean extras pass");
1589
1590 for field in typed_fields {
1591 r.extra.clear();
1592 r.extra.insert(field.into(), json!(true));
1593 let err = r
1594 .validate_extras()
1595 .expect_err(&format!("{field} collision must be detected"));
1596 assert_eq!(err.keys(), &[field.to_string()]);
1597 }
1598 }
1599}