1use crate::methods::ensure_account_ownership;
8use crate::types::{JmapSetError, Principal};
9use async_trait::async_trait;
10use rusmes_storage::MessageStore;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::PathBuf;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17struct IdentityAccountState {
18 identities: HashMap<String, Identity>,
20 state_version: u64,
22}
23
24impl IdentityAccountState {
25 fn new_with_default(account_id: &str, username: &str) -> Self {
26 let default_email = if username.contains('@') {
27 username.to_string()
28 } else {
29 format!("{}@localhost", account_id)
30 };
31
32 let mut identities = HashMap::new();
33 identities.insert(
34 "default".to_string(),
35 Identity {
36 id: "default".to_string(),
37 name: "Default User".to_string(),
38 email: default_email,
39 reply_to: None,
40 bcc: None,
41 text_signature: None,
42 html_signature: None,
43 may_delete: false,
44 },
45 );
46
47 Self {
48 identities,
49 state_version: 1,
50 }
51 }
52}
53
54#[async_trait]
56pub trait IdentityStore: Send + Sync {
57 async fn list_identities(
59 &self,
60 account_id: &str,
61 username: &str,
62 ) -> anyhow::Result<Vec<Identity>>;
63
64 async fn get_identity(
66 &self,
67 account_id: &str,
68 username: &str,
69 id: &str,
70 ) -> anyhow::Result<Option<Identity>>;
71
72 async fn create_identity(
74 &self,
75 account_id: &str,
76 username: &str,
77 identity: Identity,
78 ) -> anyhow::Result<Identity>;
79
80 async fn update_identity(
82 &self,
83 account_id: &str,
84 username: &str,
85 id: &str,
86 patch: &serde_json::Value,
87 ) -> anyhow::Result<Identity>;
88
89 async fn delete_identity(
91 &self,
92 account_id: &str,
93 username: &str,
94 id: &str,
95 ) -> anyhow::Result<()>;
96
97 async fn state_token(&self, account_id: &str, username: &str) -> anyhow::Result<String>;
99}
100
101pub struct FileIdentityStore {
106 base_dir: PathBuf,
107}
108
109impl FileIdentityStore {
110 pub fn new(base_dir: impl Into<PathBuf>) -> Self {
112 Self {
113 base_dir: base_dir.into(),
114 }
115 }
116
117 fn account_path(&self, account_id: &str) -> PathBuf {
118 self.base_dir
119 .join("identities")
120 .join(format!("{}.json", account_id))
121 }
122
123 async fn load(&self, account_id: &str, username: &str) -> anyhow::Result<IdentityAccountState> {
124 let path = self.account_path(account_id);
125 if !path.exists() {
126 return Ok(IdentityAccountState::new_with_default(account_id, username));
127 }
128 let bytes = tokio::fs::read(&path).await?;
129 let state: IdentityAccountState = serde_json::from_slice(&bytes)?;
130 Ok(state)
131 }
132
133 async fn save(&self, account_id: &str, state: &IdentityAccountState) -> anyhow::Result<()> {
134 let path = self.account_path(account_id);
135 if let Some(parent) = path.parent() {
136 tokio::fs::create_dir_all(parent).await?;
137 }
138 let bytes = serde_json::to_vec_pretty(state)?;
139 tokio::fs::write(&path, bytes).await?;
140 Ok(())
141 }
142}
143
144#[async_trait]
145impl IdentityStore for FileIdentityStore {
146 async fn list_identities(
147 &self,
148 account_id: &str,
149 username: &str,
150 ) -> anyhow::Result<Vec<Identity>> {
151 let state = self.load(account_id, username).await?;
152 Ok(state.identities.into_values().collect())
153 }
154
155 async fn get_identity(
156 &self,
157 account_id: &str,
158 username: &str,
159 id: &str,
160 ) -> anyhow::Result<Option<Identity>> {
161 let state = self.load(account_id, username).await?;
162 Ok(state.identities.get(id).cloned())
163 }
164
165 async fn create_identity(
166 &self,
167 account_id: &str,
168 username: &str,
169 identity: Identity,
170 ) -> anyhow::Result<Identity> {
171 let mut state = self.load(account_id, username).await?;
172 state
173 .identities
174 .insert(identity.id.clone(), identity.clone());
175 state.state_version += 1;
176 self.save(account_id, &state).await?;
177 Ok(identity)
178 }
179
180 async fn update_identity(
181 &self,
182 account_id: &str,
183 username: &str,
184 id: &str,
185 patch: &serde_json::Value,
186 ) -> anyhow::Result<Identity> {
187 let mut state = self.load(account_id, username).await?;
188 let existing = state
189 .identities
190 .get(id)
191 .cloned()
192 .ok_or_else(|| anyhow::anyhow!("Identity '{}' not found", id))?;
193
194 let mut current_json = serde_json::to_value(&existing)?;
197 if let (Some(obj), Some(patch_obj)) = (current_json.as_object_mut(), patch.as_object()) {
198 for (path_key, value) in patch_obj {
199 let field = path_key.trim_start_matches('/');
201 obj.insert(field.to_string(), value.clone());
202 }
203 }
204 let mut updated: Identity = serde_json::from_value(current_json)?;
205
206 updated.id = existing.id.clone();
208 if id == "default" {
209 updated.may_delete = false;
210 }
211
212 state.identities.insert(id.to_string(), updated.clone());
213 state.state_version += 1;
214 self.save(account_id, &state).await?;
215 Ok(updated)
216 }
217
218 async fn delete_identity(
219 &self,
220 account_id: &str,
221 username: &str,
222 id: &str,
223 ) -> anyhow::Result<()> {
224 let mut state = self.load(account_id, username).await?;
225 if state.identities.remove(id).is_none() {
226 return Err(anyhow::anyhow!("Identity '{}' not found", id));
227 }
228 state.state_version += 1;
229 self.save(account_id, &state).await?;
230 Ok(())
231 }
232
233 async fn state_token(&self, account_id: &str, username: &str) -> anyhow::Result<String> {
234 let state = self.load(account_id, username).await?;
235 Ok(state.state_version.to_string())
236 }
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
243#[serde(rename_all = "camelCase")]
244pub struct Identity {
245 pub id: String,
247 pub name: String,
249 pub email: String,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub reply_to: Option<Vec<crate::types::EmailAddress>>,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub bcc: Option<Vec<crate::types::EmailAddress>>,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub text_signature: Option<String>,
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub html_signature: Option<String>,
263 pub may_delete: bool,
265}
266
267#[derive(Debug, Clone, Deserialize)]
269#[serde(rename_all = "camelCase")]
270pub struct IdentityGetRequest {
271 pub account_id: String,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub ids: Option<Vec<String>>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub properties: Option<Vec<String>>,
276}
277
278#[derive(Debug, Clone, Serialize)]
280#[serde(rename_all = "camelCase")]
281pub struct IdentityGetResponse {
282 pub account_id: String,
283 pub state: String,
284 pub list: Vec<Identity>,
285 pub not_found: Vec<String>,
286}
287
288#[derive(Debug, Clone, Deserialize)]
290#[serde(rename_all = "camelCase")]
291pub struct IdentitySetRequest {
292 pub account_id: String,
293 #[serde(skip_serializing_if = "Option::is_none")]
294 pub if_in_state: Option<String>,
295 #[serde(skip_serializing_if = "Option::is_none")]
296 pub create: Option<HashMap<String, IdentityObject>>,
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub update: Option<HashMap<String, serde_json::Value>>,
299 #[serde(skip_serializing_if = "Option::is_none")]
300 pub destroy: Option<Vec<String>>,
301}
302
303#[derive(Debug, Clone, Deserialize)]
305#[serde(rename_all = "camelCase")]
306pub struct IdentityObject {
307 pub name: String,
308 pub email: String,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub reply_to: Option<Vec<crate::types::EmailAddress>>,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub bcc: Option<Vec<crate::types::EmailAddress>>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub text_signature: Option<String>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub html_signature: Option<String>,
317}
318
319#[derive(Debug, Clone, Serialize)]
321#[serde(rename_all = "camelCase")]
322pub struct IdentitySetResponse {
323 pub account_id: String,
324 pub old_state: String,
325 pub new_state: String,
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub created: Option<HashMap<String, Identity>>,
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub updated: Option<HashMap<String, Option<Identity>>>,
330 #[serde(skip_serializing_if = "Option::is_none")]
331 pub destroyed: Option<Vec<String>>,
332 #[serde(skip_serializing_if = "Option::is_none")]
333 pub not_created: Option<HashMap<String, JmapSetError>>,
334 #[serde(skip_serializing_if = "Option::is_none")]
335 pub not_updated: Option<HashMap<String, JmapSetError>>,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub not_destroyed: Option<HashMap<String, JmapSetError>>,
338}
339
340#[derive(Debug, Clone, Deserialize)]
342#[serde(rename_all = "camelCase")]
343pub struct IdentityChangesRequest {
344 pub account_id: String,
345 pub since_state: String,
346 #[serde(skip_serializing_if = "Option::is_none")]
347 pub max_changes: Option<u64>,
348}
349
350#[derive(Debug, Clone, Serialize)]
352#[serde(rename_all = "camelCase")]
353pub struct IdentityChangesResponse {
354 pub account_id: String,
355 pub old_state: String,
356 pub new_state: String,
357 pub has_more_changes: bool,
358 pub created: Vec<String>,
359 pub updated: Vec<String>,
360 pub destroyed: Vec<String>,
361}
362
363fn is_valid_email(email: &str) -> bool {
368 let at_count = email.chars().filter(|&c| c == '@').count();
369 if at_count != 1 {
370 return false;
371 }
372 let mut parts = email.splitn(2, '@');
373 let local = parts.next().unwrap_or("");
374 let domain = parts.next().unwrap_or("");
375 !local.is_empty() && !domain.is_empty()
376}
377
378pub async fn identity_get(
382 request: IdentityGetRequest,
383 _message_store: &dyn MessageStore,
384 identity_store: &dyn IdentityStore,
385 principal: &Principal,
386) -> anyhow::Result<IdentityGetResponse> {
387 ensure_account_ownership(&request.account_id, principal)?;
388
389 let state = identity_store
390 .state_token(&request.account_id, &principal.username)
391 .await?;
392
393 let mut list = Vec::new();
394 let mut not_found = Vec::new();
395
396 match request.ids {
397 None => {
398 let all = identity_store
399 .list_identities(&request.account_id, &principal.username)
400 .await?;
401 list.extend(all);
402 }
403 Some(ids) => {
404 for id in ids {
405 match identity_store
406 .get_identity(&request.account_id, &principal.username, &id)
407 .await?
408 {
409 Some(identity) => list.push(identity),
410 None => not_found.push(id),
411 }
412 }
413 }
414 }
415
416 Ok(IdentityGetResponse {
417 account_id: request.account_id,
418 state,
419 list,
420 not_found,
421 })
422}
423
424pub async fn identity_set(
426 request: IdentitySetRequest,
427 _message_store: &dyn MessageStore,
428 identity_store: &dyn IdentityStore,
429 principal: &Principal,
430) -> anyhow::Result<IdentitySetResponse> {
431 ensure_account_ownership(&request.account_id, principal)?;
432
433 let old_state = identity_store
434 .state_token(&request.account_id, &principal.username)
435 .await?;
436
437 if let Some(ref expected) = request.if_in_state {
439 if *expected != old_state {
440 return Err(anyhow::anyhow!(
441 "stateMismatch: expected state '{}', current state is '{}'",
442 expected,
443 old_state
444 ));
445 }
446 }
447
448 let mut created: HashMap<String, Identity> = HashMap::new();
449 let mut updated: HashMap<String, Option<Identity>> = HashMap::new();
450 let mut destroyed: Vec<String> = Vec::new();
451 let mut not_created: HashMap<String, JmapSetError> = HashMap::new();
452 let mut not_updated: HashMap<String, JmapSetError> = HashMap::new();
453 let mut not_destroyed: HashMap<String, JmapSetError> = HashMap::new();
454
455 if let Some(create_map) = request.create {
457 for (creation_id, identity_obj) in create_map {
458 if !is_valid_email(&identity_obj.email) {
459 not_created.insert(
460 creation_id,
461 JmapSetError {
462 error_type: "invalidProperties".to_string(),
463 description: Some(format!(
464 "Invalid email address: '{}'",
465 identity_obj.email
466 )),
467 },
468 );
469 continue;
470 }
471 let new_id = uuid::Uuid::new_v4().to_string();
472 let new_identity = Identity {
473 id: new_id,
474 name: identity_obj.name,
475 email: identity_obj.email,
476 reply_to: identity_obj.reply_to,
477 bcc: identity_obj.bcc,
478 text_signature: identity_obj.text_signature,
479 html_signature: identity_obj.html_signature,
480 may_delete: true,
481 };
482 match identity_store
483 .create_identity(&request.account_id, &principal.username, new_identity)
484 .await
485 {
486 Ok(stored) => {
487 created.insert(creation_id, stored);
488 }
489 Err(e) => {
490 not_created.insert(
491 creation_id,
492 JmapSetError {
493 error_type: "serverFail".to_string(),
494 description: Some(format!("Failed to create identity: {}", e)),
495 },
496 );
497 }
498 }
499 }
500 }
501
502 if let Some(update_map) = request.update {
504 for (id, patch) in update_map {
505 match identity_store
506 .update_identity(&request.account_id, &principal.username, &id, &patch)
507 .await
508 {
509 Ok(stored) => {
510 updated.insert(id, Some(stored));
511 }
512 Err(e) => {
513 let err_msg = e.to_string();
514 let error_type = if err_msg.contains("not found") {
515 "notFound"
516 } else {
517 "serverFail"
518 };
519 not_updated.insert(
520 id,
521 JmapSetError {
522 error_type: error_type.to_string(),
523 description: Some(err_msg),
524 },
525 );
526 }
527 }
528 }
529 }
530
531 if let Some(destroy_ids) = request.destroy {
533 for id in destroy_ids {
534 if id == "default" {
535 not_destroyed.insert(
536 id,
537 JmapSetError {
538 error_type: "forbidden".to_string(),
539 description: Some("Cannot delete default identity".to_string()),
540 },
541 );
542 continue;
543 }
544 match identity_store
545 .delete_identity(&request.account_id, &principal.username, &id)
546 .await
547 {
548 Ok(()) => destroyed.push(id),
549 Err(e) => {
550 let err_msg = e.to_string();
551 let error_type = if err_msg.contains("not found") {
552 "notFound"
553 } else {
554 "serverFail"
555 };
556 not_destroyed.insert(
557 id,
558 JmapSetError {
559 error_type: error_type.to_string(),
560 description: Some(err_msg),
561 },
562 );
563 }
564 }
565 }
566 }
567
568 let new_state = identity_store
569 .state_token(&request.account_id, &principal.username)
570 .await?;
571
572 Ok(IdentitySetResponse {
573 account_id: request.account_id,
574 old_state,
575 new_state,
576 created: if created.is_empty() {
577 None
578 } else {
579 Some(created)
580 },
581 updated: if updated.is_empty() {
582 None
583 } else {
584 Some(updated)
585 },
586 destroyed: if destroyed.is_empty() {
587 None
588 } else {
589 Some(destroyed)
590 },
591 not_created: if not_created.is_empty() {
592 None
593 } else {
594 Some(not_created)
595 },
596 not_updated: if not_updated.is_empty() {
597 None
598 } else {
599 Some(not_updated)
600 },
601 not_destroyed: if not_destroyed.is_empty() {
602 None
603 } else {
604 Some(not_destroyed)
605 },
606 })
607}
608
609pub async fn identity_changes(
611 request: IdentityChangesRequest,
612 _message_store: &dyn MessageStore,
613 identity_store: &dyn IdentityStore,
614 principal: &Principal,
615) -> anyhow::Result<IdentityChangesResponse> {
616 ensure_account_ownership(&request.account_id, principal)?;
617
618 let new_state = identity_store
619 .state_token(&request.account_id, &principal.username)
620 .await?;
621 let old_state = request.since_state;
622
623 Ok(IdentityChangesResponse {
627 account_id: request.account_id,
628 old_state,
629 new_state,
630 has_more_changes: false,
631 created: Vec::new(),
632 updated: Vec::new(),
633 destroyed: Vec::new(),
634 })
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640 use rusmes_storage::backends::filesystem::FilesystemBackend;
641 use rusmes_storage::StorageBackend;
642 use std::path::PathBuf;
643
644 fn test_principal() -> crate::types::Principal {
645 crate::types::Principal {
646 username: "alice@example.com".to_string(),
647 account_id: "acc1".to_string(),
648 scopes: vec![crate::types::SCOPE_ADMIN.to_string()],
649 }
650 }
651
652 fn create_test_store() -> std::sync::Arc<dyn MessageStore> {
653 let backend = FilesystemBackend::new(PathBuf::from("/tmp/rusmes-test-storage"));
654 backend.message_store()
655 }
656
657 fn create_identity_store(sub: &str) -> FileIdentityStore {
658 let mut dir = std::env::temp_dir();
659 dir.push(format!("rusmes-identity-test-{}", sub));
660 FileIdentityStore::new(dir)
661 }
662
663 #[tokio::test]
666 async fn test_identity_create_and_get() {
667 let msg_store = create_test_store();
668 let id_store = create_identity_store("create_and_get");
669 let principal = test_principal();
670
671 let mut create_map = HashMap::new();
672 create_map.insert(
673 "c1".to_string(),
674 IdentityObject {
675 name: "Alice".to_string(),
676 email: "alice@example.com".to_string(),
677 reply_to: None,
678 bcc: None,
679 text_signature: None,
680 html_signature: None,
681 },
682 );
683 let set_resp = identity_set(
684 IdentitySetRequest {
685 account_id: "acc1".to_string(),
686 if_in_state: None,
687 create: Some(create_map),
688 update: None,
689 destroy: None,
690 },
691 msg_store.as_ref(),
692 &id_store,
693 &principal,
694 )
695 .await
696 .unwrap();
697
698 assert!(set_resp.not_created.is_none(), "create should succeed");
699 let created = set_resp.created.unwrap();
700 assert_eq!(created.len(), 1);
701 let stored = created.get("c1").unwrap();
702 assert_eq!(stored.name, "Alice");
703 assert_eq!(stored.email, "alice@example.com");
704 assert!(stored.may_delete);
705
706 let get_resp = identity_get(
708 IdentityGetRequest {
709 account_id: "acc1".to_string(),
710 ids: Some(vec![stored.id.clone()]),
711 properties: None,
712 },
713 msg_store.as_ref(),
714 &id_store,
715 &principal,
716 )
717 .await
718 .unwrap();
719 assert_eq!(get_resp.list.len(), 1);
720 assert_eq!(get_resp.list[0].email, "alice@example.com");
721 }
722
723 #[tokio::test]
724 async fn test_identity_update_name() {
725 let msg_store = create_test_store();
726 let id_store = create_identity_store("update_name");
727 let principal = test_principal();
728
729 let mut create_map = HashMap::new();
731 create_map.insert(
732 "c1".to_string(),
733 IdentityObject {
734 name: "Original".to_string(),
735 email: "orig@example.com".to_string(),
736 reply_to: None,
737 bcc: None,
738 text_signature: None,
739 html_signature: None,
740 },
741 );
742 let set_resp = identity_set(
743 IdentitySetRequest {
744 account_id: "acc1".to_string(),
745 if_in_state: None,
746 create: Some(create_map),
747 update: None,
748 destroy: None,
749 },
750 msg_store.as_ref(),
751 &id_store,
752 &principal,
753 )
754 .await
755 .unwrap();
756 let new_id = set_resp.created.unwrap().get("c1").unwrap().id.clone();
757
758 let mut update_map = HashMap::new();
760 update_map.insert(new_id.clone(), serde_json::json!({"/name": "Updated Name"}));
761 let upd_resp = identity_set(
762 IdentitySetRequest {
763 account_id: "acc1".to_string(),
764 if_in_state: None,
765 create: None,
766 update: Some(update_map),
767 destroy: None,
768 },
769 msg_store.as_ref(),
770 &id_store,
771 &principal,
772 )
773 .await
774 .unwrap();
775
776 assert!(upd_resp.not_updated.is_none(), "update should succeed");
777 let upd = upd_resp.updated.unwrap();
778 let id_obj = upd.get(&new_id).unwrap().as_ref().unwrap();
779 assert_eq!(id_obj.name, "Updated Name");
780 }
781
782 #[tokio::test]
783 async fn test_identity_destroy_custom() {
784 let msg_store = create_test_store();
785 let id_store = create_identity_store("destroy_custom");
786 let principal = test_principal();
787
788 let mut create_map = HashMap::new();
790 create_map.insert(
791 "c1".to_string(),
792 IdentityObject {
793 name: "To Be Deleted".to_string(),
794 email: "delete@example.com".to_string(),
795 reply_to: None,
796 bcc: None,
797 text_signature: None,
798 html_signature: None,
799 },
800 );
801 let set_resp = identity_set(
802 IdentitySetRequest {
803 account_id: "acc1".to_string(),
804 if_in_state: None,
805 create: Some(create_map),
806 update: None,
807 destroy: None,
808 },
809 msg_store.as_ref(),
810 &id_store,
811 &principal,
812 )
813 .await
814 .unwrap();
815 let new_id = set_resp.created.unwrap().get("c1").unwrap().id.clone();
816
817 let del_resp = identity_set(
819 IdentitySetRequest {
820 account_id: "acc1".to_string(),
821 if_in_state: None,
822 create: None,
823 update: None,
824 destroy: Some(vec![new_id.clone()]),
825 },
826 msg_store.as_ref(),
827 &id_store,
828 &principal,
829 )
830 .await
831 .unwrap();
832
833 assert!(del_resp.not_destroyed.is_none(), "destroy should succeed");
834 let destroyed = del_resp.destroyed.unwrap();
835 assert!(destroyed.contains(&new_id));
836
837 let get_resp = identity_get(
839 IdentityGetRequest {
840 account_id: "acc1".to_string(),
841 ids: Some(vec![new_id.clone()]),
842 properties: None,
843 },
844 msg_store.as_ref(),
845 &id_store,
846 &principal,
847 )
848 .await
849 .unwrap();
850 assert_eq!(get_resp.not_found, vec![new_id]);
851 }
852
853 #[tokio::test]
854 async fn test_identity_destroy_default_rejected() {
855 let msg_store = create_test_store();
856 let id_store = create_identity_store("destroy_default_rejected");
857 let principal = test_principal();
858
859 let resp = identity_set(
860 IdentitySetRequest {
861 account_id: "acc1".to_string(),
862 if_in_state: None,
863 create: None,
864 update: None,
865 destroy: Some(vec!["default".to_string()]),
866 },
867 msg_store.as_ref(),
868 &id_store,
869 &principal,
870 )
871 .await
872 .unwrap();
873
874 assert!(resp.not_destroyed.is_some());
875 let errors = resp.not_destroyed.unwrap();
876 assert_eq!(errors.get("default").unwrap().error_type, "forbidden");
877 }
878
879 #[tokio::test]
880 async fn test_identity_state_mismatch() {
881 let msg_store = create_test_store();
882 let id_store = create_identity_store("state_mismatch");
883 let principal = test_principal();
884
885 let mut create_map = HashMap::new();
887 create_map.insert(
888 "c1".to_string(),
889 IdentityObject {
890 name: "Trigger".to_string(),
891 email: "trigger@example.com".to_string(),
892 reply_to: None,
893 bcc: None,
894 text_signature: None,
895 html_signature: None,
896 },
897 );
898 identity_set(
899 IdentitySetRequest {
900 account_id: "acc1".to_string(),
901 if_in_state: None,
902 create: Some(create_map),
903 update: None,
904 destroy: None,
905 },
906 msg_store.as_ref(),
907 &id_store,
908 &principal,
909 )
910 .await
911 .unwrap();
912
913 let result = identity_set(
915 IdentitySetRequest {
916 account_id: "acc1".to_string(),
917 if_in_state: Some("999".to_string()),
918 create: None,
919 update: None,
920 destroy: None,
921 },
922 msg_store.as_ref(),
923 &id_store,
924 &principal,
925 )
926 .await;
927
928 assert!(result.is_err(), "wrong state should return Err");
929 let err_msg = result.unwrap_err().to_string();
930 assert!(
931 err_msg.contains("stateMismatch"),
932 "error should mention stateMismatch: {}",
933 err_msg
934 );
935 }
936
937 #[tokio::test]
938 async fn test_identity_full_roundtrip() {
939 let msg_store = create_test_store();
940 let id_store = create_identity_store("full_roundtrip");
941 let principal = test_principal();
942
943 let get1 = identity_get(
945 IdentityGetRequest {
946 account_id: "acc1".to_string(),
947 ids: None,
948 properties: None,
949 },
950 msg_store.as_ref(),
951 &id_store,
952 &principal,
953 )
954 .await
955 .unwrap();
956 assert_eq!(get1.list.len(), 1);
957 assert_eq!(get1.list[0].id, "default");
958 assert!(!get1.list[0].may_delete);
959 let state_after_default = get1.state.clone();
960
961 let mut create_map = HashMap::new();
963 create_map.insert(
964 "newone".to_string(),
965 IdentityObject {
966 name: "Work".to_string(),
967 email: "work@company.com".to_string(),
968 reply_to: None,
969 bcc: None,
970 text_signature: Some("Regards,\nAlice".to_string()),
971 html_signature: None,
972 },
973 );
974 let set1 = identity_set(
975 IdentitySetRequest {
976 account_id: "acc1".to_string(),
977 if_in_state: Some(state_after_default.clone()),
978 create: Some(create_map),
979 update: None,
980 destroy: None,
981 },
982 msg_store.as_ref(),
983 &id_store,
984 &principal,
985 )
986 .await
987 .unwrap();
988 assert!(set1.not_created.is_none());
989 let work_id = set1.created.unwrap().get("newone").unwrap().id.clone();
990 let state_after_create = set1.new_state.clone();
991 assert_ne!(state_after_default, state_after_create);
992
993 let mut upd_map = HashMap::new();
995 upd_map.insert(
996 work_id.clone(),
997 serde_json::json!({"/name": "Work (updated)"}),
998 );
999 let set2 = identity_set(
1000 IdentitySetRequest {
1001 account_id: "acc1".to_string(),
1002 if_in_state: Some(state_after_create.clone()),
1003 create: None,
1004 update: Some(upd_map),
1005 destroy: None,
1006 },
1007 msg_store.as_ref(),
1008 &id_store,
1009 &principal,
1010 )
1011 .await
1012 .unwrap();
1013 assert!(set2.not_updated.is_none());
1014 let upd_identity = set2
1015 .updated
1016 .unwrap()
1017 .get(&work_id)
1018 .unwrap()
1019 .as_ref()
1020 .unwrap()
1021 .clone();
1022 assert_eq!(upd_identity.name, "Work (updated)");
1023
1024 let set3 = identity_set(
1026 IdentitySetRequest {
1027 account_id: "acc1".to_string(),
1028 if_in_state: None,
1029 create: None,
1030 update: None,
1031 destroy: Some(vec![work_id.clone()]),
1032 },
1033 msg_store.as_ref(),
1034 &id_store,
1035 &principal,
1036 )
1037 .await
1038 .unwrap();
1039 assert!(set3.not_destroyed.is_none());
1040 assert_eq!(set3.destroyed.unwrap(), vec![work_id.clone()]);
1041
1042 let get_final = identity_get(
1044 IdentityGetRequest {
1045 account_id: "acc1".to_string(),
1046 ids: None,
1047 properties: None,
1048 },
1049 msg_store.as_ref(),
1050 &id_store,
1051 &principal,
1052 )
1053 .await
1054 .unwrap();
1055 assert_eq!(get_final.list.len(), 1);
1056 assert_eq!(get_final.list[0].id, "default");
1057 }
1058
1059 #[tokio::test]
1062 async fn test_identity_get() {
1063 let msg_store = create_test_store();
1064 let id_store = create_identity_store("get_default");
1065 let principal = test_principal();
1066 let request = IdentityGetRequest {
1067 account_id: "acc1".to_string(),
1068 ids: Some(vec!["default".to_string()]),
1069 properties: None,
1070 };
1071
1072 let response = identity_get(request, msg_store.as_ref(), &id_store, &principal)
1073 .await
1074 .unwrap();
1075 assert_eq!(response.list.len(), 1);
1076 assert_eq!(response.list[0].id, "default");
1077 }
1078
1079 #[tokio::test]
1080 async fn test_identity_set_create() {
1081 let msg_store = create_test_store();
1082 let id_store = create_identity_store("set_create");
1083 let principal = test_principal();
1084
1085 let mut create_map = HashMap::new();
1086 create_map.insert(
1087 "new1".to_string(),
1088 IdentityObject {
1089 name: "John Doe".to_string(),
1090 email: "john@example.com".to_string(),
1091 reply_to: None,
1092 bcc: None,
1093 text_signature: Some("Best regards,\nJohn".to_string()),
1094 html_signature: None,
1095 },
1096 );
1097
1098 let request = IdentitySetRequest {
1099 account_id: "acc1".to_string(),
1100 if_in_state: None,
1101 create: Some(create_map),
1102 update: None,
1103 destroy: None,
1104 };
1105
1106 let response = identity_set(request, msg_store.as_ref(), &id_store, &principal)
1107 .await
1108 .unwrap();
1109 assert!(response.created.is_some());
1110 assert_eq!(response.created.as_ref().unwrap().len(), 1);
1111 }
1112
1113 #[tokio::test]
1114 async fn test_identity_changes() {
1115 let msg_store = create_test_store();
1116 let id_store = create_identity_store("changes");
1117 let principal = test_principal();
1118 let request = IdentityChangesRequest {
1119 account_id: "acc1".to_string(),
1120 since_state: "1".to_string(),
1121 max_changes: Some(50),
1122 };
1123
1124 let response = identity_changes(request, msg_store.as_ref(), &id_store, &principal)
1125 .await
1126 .unwrap();
1127 assert_eq!(response.old_state, "1");
1128 assert!(!response.new_state.is_empty());
1129 }
1130
1131 #[tokio::test]
1132 async fn test_identity_set_destroy_default() {
1133 let msg_store = create_test_store();
1134 let id_store = create_identity_store("destroy_default");
1135 let principal = test_principal();
1136 let request = IdentitySetRequest {
1137 account_id: "acc1".to_string(),
1138 if_in_state: None,
1139 create: None,
1140 update: None,
1141 destroy: Some(vec!["default".to_string()]),
1142 };
1143
1144 let response = identity_set(request, msg_store.as_ref(), &id_store, &principal)
1145 .await
1146 .unwrap();
1147 assert!(response.not_destroyed.is_some());
1148 let errors = response.not_destroyed.unwrap();
1149 assert_eq!(errors.get("default").unwrap().error_type, "forbidden");
1150 }
1151
1152 #[tokio::test]
1153 async fn test_identity_with_signature() {
1154 let msg_store = create_test_store();
1155 let id_store = create_identity_store("with_signature");
1156 let principal = test_principal();
1157
1158 let mut create_map = HashMap::new();
1159 create_map.insert(
1160 "sig1".to_string(),
1161 IdentityObject {
1162 name: "Test User".to_string(),
1163 email: "test@example.com".to_string(),
1164 reply_to: None,
1165 bcc: None,
1166 text_signature: Some("--\nBest regards".to_string()),
1167 html_signature: Some("<p>Best regards</p>".to_string()),
1168 },
1169 );
1170
1171 let request = IdentitySetRequest {
1172 account_id: "acc1".to_string(),
1173 if_in_state: None,
1174 create: Some(create_map),
1175 update: None,
1176 destroy: None,
1177 };
1178
1179 let response = identity_set(request, msg_store.as_ref(), &id_store, &principal)
1180 .await
1181 .unwrap();
1182 assert!(response.created.is_some());
1183 let stored = response.created.as_ref().unwrap().get("sig1").unwrap();
1184 assert_eq!(stored.text_signature.as_deref(), Some("--\nBest regards"));
1185 }
1186
1187 #[tokio::test]
1188 async fn test_identity_with_bcc() {
1189 let msg_store = create_test_store();
1190 let id_store = create_identity_store("with_bcc");
1191 let principal = test_principal();
1192
1193 let mut create_map = HashMap::new();
1194 let bcc = vec![crate::types::EmailAddress::new(
1195 "archive@example.com".to_string(),
1196 )];
1197
1198 create_map.insert(
1199 "bcc1".to_string(),
1200 IdentityObject {
1201 name: "Test User".to_string(),
1202 email: "test@example.com".to_string(),
1203 reply_to: None,
1204 bcc: Some(bcc),
1205 text_signature: None,
1206 html_signature: None,
1207 },
1208 );
1209
1210 let request = IdentitySetRequest {
1211 account_id: "acc1".to_string(),
1212 if_in_state: None,
1213 create: Some(create_map),
1214 update: None,
1215 destroy: None,
1216 };
1217
1218 let response = identity_set(request, msg_store.as_ref(), &id_store, &principal)
1219 .await
1220 .unwrap();
1221 assert!(response.created.is_some());
1222 let stored = response.created.as_ref().unwrap().get("bcc1").unwrap();
1223 assert!(stored.bcc.is_some());
1224 }
1225
1226 #[tokio::test]
1227 async fn test_identity_get_not_found() {
1228 let msg_store = create_test_store();
1229 let id_store = create_identity_store("get_not_found");
1230 let principal = test_principal();
1231 let request = IdentityGetRequest {
1232 account_id: "acc1".to_string(),
1233 ids: Some(vec!["nonexistent".to_string()]),
1234 properties: None,
1235 };
1236
1237 let response = identity_get(request, msg_store.as_ref(), &id_store, &principal)
1238 .await
1239 .unwrap();
1240 assert_eq!(response.not_found.len(), 1);
1241 }
1242
1243 #[tokio::test]
1244 async fn test_identity_get_all() {
1245 let msg_store = create_test_store();
1246 let id_store = create_identity_store("get_all");
1247 let principal = test_principal();
1248 let request = IdentityGetRequest {
1249 account_id: "acc1".to_string(),
1250 ids: None,
1251 properties: None,
1252 };
1253
1254 let response = identity_get(request, msg_store.as_ref(), &id_store, &principal)
1255 .await
1256 .unwrap();
1257 assert!(!response.list.is_empty());
1258 }
1259
1260 #[tokio::test]
1261 async fn test_identity_set_update() {
1262 let msg_store = create_test_store();
1263 let id_store = create_identity_store("set_update_default");
1264 let principal = test_principal();
1265
1266 let mut update_map = HashMap::new();
1267 update_map.insert(
1268 "default".to_string(),
1269 serde_json::json!({"/name": "New Name"}),
1270 );
1271
1272 let request = IdentitySetRequest {
1273 account_id: "acc1".to_string(),
1274 if_in_state: None,
1275 create: None,
1276 update: Some(update_map),
1277 destroy: None,
1278 };
1279
1280 let response = identity_set(request, msg_store.as_ref(), &id_store, &principal)
1281 .await
1282 .unwrap();
1283 assert!(response.not_updated.is_none());
1285 let upd = response.updated.unwrap();
1286 let id_obj = upd.get("default").unwrap().as_ref().unwrap();
1287 assert_eq!(id_obj.name, "New Name");
1288 }
1289
1290 #[tokio::test]
1291 async fn test_identity_changes_state_progression() {
1292 let msg_store = create_test_store();
1293 let id_store = create_identity_store("changes_state_progression");
1294 let principal = test_principal();
1295
1296 let mut create_map = HashMap::new();
1298 create_map.insert(
1299 "c1".to_string(),
1300 IdentityObject {
1301 name: "Test".to_string(),
1302 email: "test@example.com".to_string(),
1303 reply_to: None,
1304 bcc: None,
1305 text_signature: None,
1306 html_signature: None,
1307 },
1308 );
1309 identity_set(
1310 IdentitySetRequest {
1311 account_id: "acc1".to_string(),
1312 if_in_state: None,
1313 create: Some(create_map),
1314 update: None,
1315 destroy: None,
1316 },
1317 msg_store.as_ref(),
1318 &id_store,
1319 &principal,
1320 )
1321 .await
1322 .unwrap();
1323
1324 let request1 = IdentityChangesRequest {
1325 account_id: "acc1".to_string(),
1326 since_state: "1".to_string(),
1327 max_changes: None,
1328 };
1329 let response1 = identity_changes(request1, msg_store.as_ref(), &id_store, &principal)
1330 .await
1331 .unwrap();
1332
1333 let new_state_num: u64 = response1.new_state.parse().unwrap();
1334 assert!(new_state_num > 1, "state should have advanced beyond 1");
1335
1336 let request2 = IdentityChangesRequest {
1338 account_id: "acc1".to_string(),
1339 since_state: response1.new_state.clone(),
1340 max_changes: None,
1341 };
1342 let response2 = identity_changes(request2, msg_store.as_ref(), &id_store, &principal)
1343 .await
1344 .unwrap();
1345 assert_eq!(response1.new_state, response2.new_state);
1346 }
1347
1348 #[tokio::test]
1349 async fn test_identity_default_may_not_delete() {
1350 let msg_store = create_test_store();
1351 let id_store = create_identity_store("default_may_not_delete");
1352 let principal = test_principal();
1353 let request = IdentityGetRequest {
1354 account_id: "acc1".to_string(),
1355 ids: Some(vec!["default".to_string()]),
1356 properties: None,
1357 };
1358
1359 let response = identity_get(request, msg_store.as_ref(), &id_store, &principal)
1360 .await
1361 .unwrap();
1362 assert!(!response.list[0].may_delete);
1363 }
1364
1365 #[tokio::test]
1366 async fn test_identity_with_reply_to() {
1367 let msg_store = create_test_store();
1368 let id_store = create_identity_store("with_reply_to");
1369 let principal = test_principal();
1370
1371 let mut create_map = HashMap::new();
1372 let reply_to = vec![crate::types::EmailAddress::new(
1373 "support@example.com".to_string(),
1374 )];
1375
1376 create_map.insert(
1377 "replyto1".to_string(),
1378 IdentityObject {
1379 name: "Support".to_string(),
1380 email: "noreply@example.com".to_string(),
1381 reply_to: Some(reply_to),
1382 bcc: None,
1383 text_signature: None,
1384 html_signature: None,
1385 },
1386 );
1387
1388 let request = IdentitySetRequest {
1389 account_id: "acc1".to_string(),
1390 if_in_state: None,
1391 create: Some(create_map),
1392 update: None,
1393 destroy: None,
1394 };
1395
1396 let response = identity_set(request, msg_store.as_ref(), &id_store, &principal)
1397 .await
1398 .unwrap();
1399 assert!(response.created.is_some());
1400 let stored = response.created.as_ref().unwrap().get("replyto1").unwrap();
1401 assert!(stored.reply_to.is_some());
1402 }
1403
1404 #[tokio::test]
1405 async fn test_identity_invalid_email_rejected() {
1406 let msg_store = create_test_store();
1407 let id_store = create_identity_store("invalid_email");
1408 let principal = test_principal();
1409
1410 let mut create_map = HashMap::new();
1411 create_map.insert(
1412 "bad1".to_string(),
1413 IdentityObject {
1414 name: "Bad".to_string(),
1415 email: "not-an-email".to_string(),
1416 reply_to: None,
1417 bcc: None,
1418 text_signature: None,
1419 html_signature: None,
1420 },
1421 );
1422
1423 let response = identity_set(
1424 IdentitySetRequest {
1425 account_id: "acc1".to_string(),
1426 if_in_state: None,
1427 create: Some(create_map),
1428 update: None,
1429 destroy: None,
1430 },
1431 msg_store.as_ref(),
1432 &id_store,
1433 &principal,
1434 )
1435 .await
1436 .unwrap();
1437 assert!(response.not_created.is_some());
1438 let err = response.not_created.unwrap();
1439 assert_eq!(err.get("bad1").unwrap().error_type, "invalidProperties");
1440 }
1441}