1#![forbid(unsafe_code)]
30
31use std::sync::Arc;
32
33use jmap_server::{Dispatcher, HandlerFuture, JmapHandler};
34
35pub mod backend;
36mod helpers;
37#[cfg(feature = "memory")]
42pub mod memory;
43pub mod task;
44pub mod task_list;
45pub mod task_notification;
46
47pub use backend::{
48 AddedItem, BackendChangesError, BackendSetError, ChangesResult, GetObject, JmapBackend,
49 JmapObject, QueryChangesResult, QueryObject, QueryResult, SetError, SetErrorType, SetObject,
50 TaskListProperty, TaskNotificationProperty, TaskProperty, TasksBackend,
51};
52pub use task::{
53 handle_task_changes, handle_task_copy, handle_task_get, handle_task_query,
54 handle_task_query_changes, handle_task_set,
55};
56pub use task_list::{handle_task_list_changes, handle_task_list_get, handle_task_list_set};
57pub use task_notification::{
58 handle_task_notification_changes, handle_task_notification_get, handle_task_notification_query,
59 handle_task_notification_query_changes, handle_task_notification_set,
60};
61
62pub use jmap_tasks_types::JMAP_TASKS_URI;
64
65pub fn register_tasks_handlers<B>(dispatcher: &mut Dispatcher<B::CallerCtx>, backend: Arc<B>)
106where
107 B: TasksBackend + 'static,
108{
109 macro_rules! reg {
115 ($method:expr, $backend:expr, |$b:ident, $ci:ident, $a:ident, $ctx:ident| $body:expr) => {{
116 let backend_arc: Arc<B> = Arc::clone(&$backend);
117 let h: Arc<dyn JmapHandler<B::CallerCtx>> = Arc::new(ClosureHandler::new(
118 backend_arc,
119 move |$b: Arc<B>, $ci: String, $a: serde_json::Value, $ctx: B::CallerCtx| {
120 Box::pin(async move { $body }) as HandlerFuture
121 },
122 ));
123 dispatcher.register($method, h);
124 }};
125 }
126
127 reg!("TaskList/get", backend, |b, _ci, a, ctx| {
129 handle_task_list_get(&*b, &ctx, a).await
130 });
131 reg!("TaskList/changes", backend, |b, _ci, a, ctx| {
132 handle_task_list_changes(&*b, &ctx, a).await
133 });
134 reg!("TaskList/set", backend, |b, _ci, a, ctx| {
135 handle_task_list_set(&*b, &ctx, a).await
136 });
137
138 reg!("Task/get", backend, |b, _ci, a, ctx| {
140 handle_task_get(&*b, &ctx, a).await
141 });
142 reg!("Task/changes", backend, |b, _ci, a, ctx| {
143 handle_task_changes(&*b, &ctx, a).await
144 });
145 reg!("Task/set", backend, |b, _ci, a, ctx| {
146 handle_task_set(&*b, &ctx, a).await
147 });
148 reg!("Task/copy", backend, |b, ci, a, ctx| {
149 handle_task_copy(&*b, &ctx, a, &ci).await
150 });
151 reg!("Task/query", backend, |b, _ci, a, ctx| {
152 handle_task_query(&*b, &ctx, a).await
153 });
154 reg!("Task/queryChanges", backend, |b, _ci, a, ctx| {
155 handle_task_query_changes(&*b, &ctx, a).await
156 });
157
158 reg!("TaskNotification/get", backend, |b, _ci, a, ctx| {
160 handle_task_notification_get(&*b, &ctx, a).await
161 });
162 reg!("TaskNotification/changes", backend, |b, _ci, a, ctx| {
163 handle_task_notification_changes(&*b, &ctx, a).await
164 });
165 reg!("TaskNotification/set", backend, |b, _ci, a, ctx| {
166 handle_task_notification_set(&*b, &ctx, a).await
167 });
168 reg!("TaskNotification/query", backend, |b, _ci, a, ctx| {
169 handle_task_notification_query(&*b, &ctx, a).await
170 });
171 reg!(
172 "TaskNotification/queryChanges",
173 backend,
174 |b, _ci, a, ctx| handle_task_notification_query_changes(&*b, &ctx, a).await
175 );
176}
177
178pub use jmap_server::ClosureHandler;
197
198#[cfg(test)]
203#[deny(clippy::await_holding_lock)]
204pub(crate) mod test_support {
205 use std::collections::HashMap;
208 use std::sync::atomic::{AtomicU32, Ordering};
209 use std::sync::{Arc, Mutex};
210
211 use jmap_server::{
212 BackendChangesError, BackendSetError, ChangesResult, GetObject, JmapBackend, JmapObject,
213 QueryChangesResult, QueryObject, QueryResult, SetError, SetErrorType, SetObject,
214 };
215 use jmap_tasks_types::{Task, TaskList, TaskNotification};
216 use jmap_types::{Id, State};
217
218 use crate::backend::TasksBackend;
219
220 #[derive(Debug)]
222 pub struct MockError(pub String);
223
224 impl std::fmt::Display for MockError {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 write!(f, "mock error: {}", self.0)
227 }
228 }
229
230 impl std::error::Error for MockError {}
231
232 #[derive(Default, Clone)]
234 struct AccountState {
235 task_lists: HashMap<Id, TaskList>,
236 tasks: HashMap<Id, Task>,
237 notifications: HashMap<Id, TaskNotification>,
238 task_list_state: u64,
239 task_state: u64,
240 notification_state: u64,
241 }
242
243 #[derive(Clone)]
245 pub struct MockBackend {
246 state: Arc<Mutex<HashMap<String, AccountState>>>,
247 pub per_user_calls: Arc<AtomicU32>,
250 }
251
252 impl MockBackend {
253 pub fn new() -> Self {
255 Self {
256 state: Arc::new(Mutex::new(HashMap::new())),
257 per_user_calls: Arc::new(AtomicU32::new(0)),
258 }
259 }
260
261 pub fn new_with_account(account_id: &str) -> Self {
263 let b = Self::new();
264 b.state
265 .lock()
266 .unwrap()
267 .insert(account_id.to_owned(), AccountState::default());
268 b
269 }
270
271 pub fn add_account(&mut self, account_id: &str) {
275 self.state
276 .lock()
277 .unwrap()
278 .entry(account_id.to_owned())
279 .or_default();
280 }
281
282 pub fn add_notification(&mut self, account_id: &str, notif_id: &str) {
284 let notif: TaskNotification = serde_json::from_value(serde_json::json!({
285 "id": notif_id,
286 "created": "2024-01-01T00:00:00Z",
287 "changedBy": { "@type": "Person", "name": "Test" },
288 "type": "created",
289 "taskId": "task1"
290 }))
291 .expect("test fixture must deserialize");
292 let mut guard = self.state.lock().unwrap();
293 let acct = guard.entry(account_id.to_owned()).or_default();
294 acct.notifications.insert(Id::from(notif_id), notif);
295 }
296
297 pub fn seed_task(&mut self, account_id: &str, task_id: &str, is_draft: bool) {
301 let task: Task = serde_json::from_value(serde_json::json!({
302 "id": task_id,
303 "isDraft": is_draft
304 }))
305 .expect("task seed fixture must deserialize");
306 let mut guard = self.state.lock().unwrap();
307 let acct = guard.entry(account_id.to_owned()).or_default();
308 acct.tasks.insert(Id::from(task_id), task);
309 }
310
311 pub fn add_task_list_with_task(&mut self, account_id: &str, list_id: &str) {
313 let task_list: TaskList = serde_json::from_value(serde_json::json!({
314 "id": list_id,
315 "name": "Test List",
316 "sortOrder": 0,
317 "isSubscribed": true,
318 "myRights": {
319 "mayReadItems": true,
320 "mayWriteAll": true,
321 "mayWriteOwn": true,
322 "mayUpdatePrivate": true,
323 "mayRSVP": true,
324 "mayAdmin": true,
325 "mayDelete": true
326 }
327 }))
328 .expect("task list fixture must deserialize");
329 let mut guard = self.state.lock().unwrap();
330 let acct = guard.entry(account_id.to_owned()).or_default();
331 acct.task_lists.insert(Id::from(list_id), task_list);
332 let task: Task = serde_json::from_value(serde_json::json!({
334 "id": "task1",
335 "taskListId": list_id
336 }))
337 .expect("task fixture must deserialize");
338 acct.tasks.insert(Id::from("task1"), task);
339 }
340 }
341
342 impl JmapBackend for MockBackend {
343 type Error = MockError;
344 type CallerCtx = ();
345
346 async fn account_exists(&self, _caller: &(), account_id: &Id) -> Result<bool, Self::Error> {
347 Ok(self.state.lock().unwrap().contains_key(account_id.as_ref()))
348 }
349
350 async fn get_objects<O: GetObject + Send + Sync>(
351 &self,
352 _caller: &(),
353 account_id: &Id,
354 ids: Option<&[Id]>,
355 _properties: Option<&[String]>,
356 ) -> Result<(Vec<O>, Vec<Id>), Self::Error> {
357 let guard = self.state.lock().unwrap();
362 let Some(acct) = guard.get(account_id.as_ref()) else {
363 return Ok((vec![], vec![]));
364 };
365
366 let mut found: Vec<O> = Vec::new();
367 let mut not_found: Vec<Id> = Vec::new();
368
369 if let Some(id_slice) = ids {
370 for id in id_slice {
371 if let Some(task) = acct.tasks.get(id) {
372 match serde_json::to_value(task)
373 .ok()
374 .and_then(|v| serde_json::from_value::<O>(v).ok())
375 {
376 Some(obj) => found.push(obj),
377 None => not_found.push(id.clone()),
378 }
379 } else {
380 not_found.push(id.clone());
381 }
382 }
383 }
384 Ok((found, not_found))
387 }
388
389 async fn get_state<O: JmapObject + Send + Sync>(
390 &self,
391 _caller: &(),
392 _account_id: &Id,
393 ) -> Result<State, Self::Error> {
394 Ok(State::from("0"))
395 }
396
397 async fn get_changes<O: JmapObject + Send + Sync>(
398 &self,
399 _caller: &(),
400 _account_id: &Id,
401 _since_state: &State,
402 _max_changes: Option<u64>,
403 ) -> Result<ChangesResult, BackendChangesError<Self::Error>> {
404 Ok(ChangesResult::new(
405 vec![],
406 vec![],
407 vec![],
408 false,
409 State::from("0"),
410 ))
411 }
412
413 async fn query_objects<O: QueryObject + Send + Sync>(
414 &self,
415 _caller: &(),
416 _account_id: &Id,
417 _filter: Option<&O::Filter>,
418 _sort: Option<&[O::Comparator]>,
419 _limit: Option<u64>,
420 _position: i64,
421 ) -> Result<QueryResult, Self::Error> {
422 Ok(QueryResult::new(
423 vec![],
424 0,
425 Some(0),
426 State::from("0"),
427 false,
428 ))
429 }
430
431 async fn query_changes<O: QueryObject + Send + Sync>(
432 &self,
433 _caller: &(),
434 _account_id: &Id,
435 since_query_state: &State,
436 _filter: Option<&O::Filter>,
437 _sort: Option<&[O::Comparator]>,
438 _max_changes: Option<u64>,
439 _up_to_id: Option<&Id>,
440 _collapse_threads: bool,
441 ) -> Result<QueryChangesResult, BackendChangesError<Self::Error>> {
442 Ok(QueryChangesResult::new(
443 since_query_state.clone(),
444 State::from("0"),
445 Some(0),
446 vec![],
447 vec![],
448 ))
449 }
450 }
451
452 impl TasksBackend for MockBackend {
453 async fn create_object<O: SetObject + Send + Sync>(
454 &self,
455 _caller: &(),
456 _account_id: &Id,
457 _create_id: &str,
458 obj: O,
459 ) -> Result<(Id, O), BackendSetError<Self::Error>> {
460 Ok((Id::from("mock-id-1"), obj))
461 }
462
463 async fn update_object<O: SetObject + Send + Sync>(
464 &self,
465 _caller: &(),
466 _account_id: &Id,
467 _id: &Id,
468 _patch: O::Patch,
469 ) -> Result<Option<O>, BackendSetError<Self::Error>> {
470 Err(BackendSetError::SetError(SetError::new(
471 SetErrorType::Forbidden,
472 )))
473 }
474
475 async fn destroy_object<O: SetObject + Send + Sync>(
476 &self,
477 _caller: &(),
478 account_id: &Id,
479 id: &Id,
480 ) -> Result<(), BackendSetError<Self::Error>> {
481 let mut guard = self.state.lock().unwrap();
482 if let Some(acct) = guard.get_mut(account_id.as_ref()) {
483 if acct.notifications.remove(id).is_some() {
484 acct.notification_state += 1;
485 return Ok(());
486 }
487 if acct.tasks.remove(id).is_some() {
488 acct.task_state += 1;
489 return Ok(());
490 }
491 if acct.task_lists.remove(id).is_some() {
492 acct.task_list_state += 1;
493 return Ok(());
494 }
495 }
496 Err(BackendSetError::SetError(SetError::new(
497 SetErrorType::NotFound,
498 )))
499 }
500
501 fn supports_type<O: JmapObject>(&self) -> bool {
502 true
503 }
504
505 async fn update_task_per_user(
506 &self,
507 caller: &(),
508 account_id: &Id,
509 id: &Id,
510 patch: jmap_types::PatchObject,
511 ) -> Result<Option<Task>, BackendSetError<Self::Error>> {
512 self.per_user_calls.fetch_add(1, Ordering::Relaxed);
514 self.update_object::<Task>(caller, account_id, id, patch)
516 .await
517 }
518
519 async fn task_list_has_tasks(
520 &self,
521 _caller: &(),
522 account_id: &Id,
523 task_list_id: &Id,
524 ) -> Result<bool, MockError> {
525 let guard = self.state.lock().unwrap();
526 if let Some(acct) = guard.get(account_id.as_ref()) {
527 return Ok(acct.tasks.values().any(|t| {
528 t.task_list_id
529 .as_ref()
530 .is_some_and(|lid| lid == task_list_id)
531 }));
532 }
533 Ok(false)
534 }
535 }
536}
537
538#[cfg(test)]
543mod tests {
544 use std::sync::atomic::Ordering;
545 use std::sync::Arc;
546
547 use jmap_server::{Dispatcher, JmapRequest, State};
548 use serde_json::json;
549
550 use super::*;
551 use crate::test_support::MockBackend;
552
553 fn single_call(method: &str, args: serde_json::Value, call_id: &str) -> JmapRequest {
555 JmapRequest::new(
556 vec!["urn:ietf:params:jmap:tasks".into()],
557 vec![(method.into(), args, call_id.into())],
558 None,
559 )
560 }
561
562 #[tokio::test]
567 async fn registers_all_14_methods() {
568 let backend = Arc::new(MockBackend::new_with_account("acc1"));
569 let mut dispatcher: Dispatcher<()> = Dispatcher::new();
570 register_tasks_handlers(&mut dispatcher, Arc::clone(&backend));
571
572 let methods = [
573 ("TaskList/get", json!({"accountId": "acc1", "ids": null})),
574 (
575 "TaskList/changes",
576 json!({"accountId": "acc1", "sinceState": "0"}),
577 ),
578 ("TaskList/set", json!({"accountId": "acc1", "destroy": []})),
579 ("Task/get", json!({"accountId": "acc1", "ids": null})),
580 (
581 "Task/changes",
582 json!({"accountId": "acc1", "sinceState": "0"}),
583 ),
584 ("Task/set", json!({"accountId": "acc1", "destroy": []})),
585 (
586 "Task/copy",
587 json!({"fromAccountId": "acc1", "accountId": "acc1", "create": {}}),
588 ),
589 (
590 "Task/query",
591 json!({"accountId": "acc1", "filter": null, "sort": null}),
592 ),
593 (
594 "Task/queryChanges",
595 json!({"accountId": "acc1", "sinceQueryState": "0"}),
596 ),
597 (
598 "TaskNotification/get",
599 json!({"accountId": "acc1", "ids": null}),
600 ),
601 (
602 "TaskNotification/changes",
603 json!({"accountId": "acc1", "sinceState": "0"}),
604 ),
605 (
606 "TaskNotification/set",
607 json!({"accountId": "acc1", "destroy": []}),
608 ),
609 (
610 "TaskNotification/query",
611 json!({"accountId": "acc1", "filter": null, "sort": null}),
612 ),
613 (
614 "TaskNotification/queryChanges",
615 json!({"accountId": "acc1", "sinceQueryState": "0"}),
616 ),
617 ];
618
619 for (method, args) in methods {
620 let req = single_call(method, args, "c0");
621 let resp = dispatcher.dispatch(req, (), State::from("s0")).await;
622 assert_eq!(
623 resp.method_responses.len(),
624 1,
625 "{method}: expected 1 response"
626 );
627 let (_, resp_args, _) = &resp.method_responses[0];
628 assert_ne!(
629 resp_args["type"], "unknownMethod",
630 "{method}: must not be unknownMethod — is it registered?"
631 );
632 }
633 }
634
635 #[tokio::test]
638 async fn task_notification_set_create_returns_forbidden() {
639 let backend = Arc::new(MockBackend::new_with_account("acc1"));
640 let mut dispatcher: Dispatcher<()> = Dispatcher::new();
641 register_tasks_handlers(&mut dispatcher, Arc::clone(&backend));
642
643 let req = single_call(
644 "TaskNotification/set",
645 json!({
646 "accountId": "acc1",
647 "create": {
648 "c1": {
649 "id": "x",
650 "created": "2024-01-01T00:00:00Z",
651 "changedBy": { "@type": "Person", "name": "A" },
652 "type": "created",
653 "taskId": "t1"
654 }
655 }
656 }),
657 "c0",
658 );
659 let resp = dispatcher.dispatch(req, (), State::from("s0")).await;
660 let (_, args, _) = &resp.method_responses[0];
661 assert!(
662 args.get("type").is_none(),
663 "must not be a top-level error: {args}"
664 );
665 assert_eq!(
666 args["notCreated"]["c1"]["type"], "forbidden",
667 "create must be forbidden: {args}"
668 );
669 }
670
671 #[tokio::test]
677 async fn task_list_set_destroy_with_tasks_returns_task_list_has_task() {
678 let mut backend = MockBackend::new_with_account("acc1");
679 backend.add_task_list_with_task("acc1", "list1");
680 let backend = Arc::new(backend);
681
682 let mut dispatcher: Dispatcher<()> = Dispatcher::new();
683 register_tasks_handlers(&mut dispatcher, Arc::clone(&backend));
684
685 let req = single_call(
686 "TaskList/set",
687 json!({
688 "accountId": "acc1",
689 "destroy": ["list1"],
690 "onDestroyRemoveTasks": false
691 }),
692 "c0",
693 );
694 let resp = dispatcher.dispatch(req, (), State::from("s0")).await;
695 let (_, args, _) = &resp.method_responses[0];
696 assert!(
697 args.get("type").is_none(),
698 "must not be a top-level error: {args}"
699 );
700 assert_eq!(
701 args["notDestroyed"]["list1"]["type"], "taskListHasTask",
702 "must return taskListHasTask when list has tasks: {args}"
703 );
704 }
705
706 #[tokio::test]
711 async fn isdraft_revert_via_dispatcher() {
712 let mut backend = MockBackend::new_with_account("acc1");
713 backend.seed_task("acc1", "t1", false);
714 let backend = Arc::new(backend);
715
716 let mut dispatcher: Dispatcher<()> = Dispatcher::new();
717 register_tasks_handlers(&mut dispatcher, Arc::clone(&backend));
718
719 let req = single_call(
720 "Task/set",
721 json!({
722 "accountId": "acc1",
723 "update": { "t1": { "isDraft": true } }
724 }),
725 "c0",
726 );
727 let resp = dispatcher.dispatch(req, (), State::from("s0")).await;
728 let (_, args, _) = &resp.method_responses[0];
729 assert!(
730 args.get("type").is_none(),
731 "must not be a top-level error: {args}"
732 );
733 assert_eq!(
734 args["notUpdated"]["t1"]["type"], "invalidProperties",
735 "isDraft revert must be invalidProperties: {args}"
736 );
737 assert_eq!(
738 args["notUpdated"]["t1"]["properties"][0], "isDraft",
739 "isDraft must be listed in properties: {args}"
740 );
741 }
742
743 #[tokio::test]
746 async fn isdraft_draft_to_publish_allowed() {
747 let mut backend = MockBackend::new_with_account("acc1");
748 backend.seed_task("acc1", "t1", true);
749 let backend = Arc::new(backend);
750
751 let mut dispatcher: Dispatcher<()> = Dispatcher::new();
752 register_tasks_handlers(&mut dispatcher, Arc::clone(&backend));
753
754 let req = single_call(
755 "Task/set",
756 json!({
757 "accountId": "acc1",
758 "update": { "t1": { "isDraft": false } }
759 }),
760 "c0",
761 );
762 let resp = dispatcher.dispatch(req, (), State::from("s0")).await;
763 let (_, args, _) = &resp.method_responses[0];
764 if let Some(not_updated) = args["notUpdated"].as_object() {
766 if let Some(err) = not_updated.get("t1") {
767 assert_ne!(
768 err["type"].as_str(),
769 Some("invalidProperties"),
770 "isDraft:false must not produce invalidProperties: {args}"
771 );
772 }
773 }
774 }
775
776 #[tokio::test]
779 async fn utcstart_not_in_default_properties() {
780 let backend = Arc::new(MockBackend::new_with_account("acc1"));
781 let mut dispatcher: Dispatcher<()> = Dispatcher::new();
782 register_tasks_handlers(&mut dispatcher, Arc::clone(&backend));
783
784 let req = single_call(
785 "Task/get",
786 json!({ "accountId": "acc1", "ids": null, "properties": ["id"] }),
787 "c0",
788 );
789 let resp = dispatcher.dispatch(req, (), State::from("s0")).await;
790 let (_, args, _) = &resp.method_responses[0];
791 assert!(args.get("type").is_none(), "must not be error: {args}");
792 for item in args["list"].as_array().into_iter().flatten() {
793 assert!(
794 item.get("utcStart").is_none(),
795 "utcStart must not appear when not requested: {item}"
796 );
797 }
798 }
799
800 #[tokio::test]
804 async fn utcstart_returned_when_requested() {
805 let backend = Arc::new(MockBackend::new_with_account("acc1"));
806 let mut dispatcher: Dispatcher<()> = Dispatcher::new();
807 register_tasks_handlers(&mut dispatcher, Arc::clone(&backend));
808
809 let req = single_call(
810 "Task/get",
811 json!({
812 "accountId": "acc1",
813 "ids": null,
814 "properties": ["id", "utcStart"]
815 }),
816 "c0",
817 );
818 let resp = dispatcher.dispatch(req, (), State::from("s0")).await;
819 let (_, args, _) = &resp.method_responses[0];
820 assert!(
821 args.get("type").is_none(),
822 "Task/get with utcStart must not error: {args}"
823 );
824 }
825
826 #[tokio::test]
832 async fn compute_utc_times_receives_validated_account_id() {
833 use std::sync::Mutex;
834
835 use jmap_server::{
836 BackendChangesError, BackendSetError, ChangesResult, GetObject, JmapBackend,
837 JmapObject, QueryChangesResult, QueryObject, QueryResult, SetObject,
838 };
839 use jmap_tasks_types::Task;
840 use jmap_types::{Id, UTCDate};
841
842 use crate::backend::TasksBackend;
843 use crate::test_support::MockError;
844
845 struct AccountIdRecorder {
850 observed: Mutex<Vec<Id>>,
851 }
852
853 impl JmapBackend for AccountIdRecorder {
854 type Error = MockError;
855 type CallerCtx = ();
856
857 async fn account_exists(
858 &self,
859 _caller: &(),
860 _account_id: &Id,
861 ) -> Result<bool, Self::Error> {
862 Ok(true)
863 }
864
865 async fn get_objects<O: GetObject + Send + Sync>(
866 &self,
867 _caller: &(),
868 _account_id: &Id,
869 _ids: Option<&[Id]>,
870 _properties: Option<&[String]>,
871 ) -> Result<(Vec<O>, Vec<Id>), Self::Error> {
872 let item = json!({ "id": "t1" });
876 match serde_json::from_value::<O>(item) {
877 Ok(obj) => Ok((vec![obj], vec![])),
878 Err(_) => Ok((vec![], vec![])),
879 }
880 }
881
882 async fn get_state<O: JmapObject + Send + Sync>(
883 &self,
884 _caller: &(),
885 _account_id: &Id,
886 ) -> Result<State, Self::Error> {
887 Ok(State::from("s0"))
888 }
889
890 async fn get_changes<O: JmapObject + Send + Sync>(
891 &self,
892 _caller: &(),
893 _account_id: &Id,
894 _since_state: &State,
895 _max_changes: Option<u64>,
896 ) -> Result<ChangesResult, BackendChangesError<Self::Error>> {
897 Ok(ChangesResult::new(
898 vec![],
899 vec![],
900 vec![],
901 false,
902 State::from("s0"),
903 ))
904 }
905
906 async fn query_objects<O: QueryObject + Send + Sync>(
907 &self,
908 _caller: &(),
909 _account_id: &Id,
910 _filter: Option<&O::Filter>,
911 _sort: Option<&[O::Comparator]>,
912 _limit: Option<u64>,
913 _position: i64,
914 ) -> Result<QueryResult, Self::Error> {
915 Ok(QueryResult::new(
916 vec![],
917 0,
918 Some(0),
919 State::from("s0"),
920 false,
921 ))
922 }
923
924 async fn query_changes<O: QueryObject + Send + Sync>(
925 &self,
926 _caller: &(),
927 _account_id: &Id,
928 since_query_state: &State,
929 _filter: Option<&O::Filter>,
930 _sort: Option<&[O::Comparator]>,
931 _max_changes: Option<u64>,
932 _up_to_id: Option<&Id>,
933 _collapse_threads: bool,
934 ) -> Result<QueryChangesResult, BackendChangesError<Self::Error>> {
935 Ok(QueryChangesResult::new(
936 since_query_state.clone(),
937 State::from("s0"),
938 Some(0),
939 vec![],
940 vec![],
941 ))
942 }
943 }
944
945 impl TasksBackend for AccountIdRecorder {
946 async fn create_object<O: SetObject + Send + Sync>(
947 &self,
948 _caller: &(),
949 _account_id: &Id,
950 _create_id: &str,
951 obj: O,
952 ) -> Result<(Id, O), BackendSetError<Self::Error>> {
953 Ok((Id::from("t1"), obj))
954 }
955
956 async fn update_object<O: SetObject + Send + Sync>(
957 &self,
958 _caller: &(),
959 _account_id: &Id,
960 _id: &Id,
961 _patch: O::Patch,
962 ) -> Result<Option<O>, BackendSetError<Self::Error>> {
963 Ok(None)
964 }
965
966 async fn destroy_object<O: SetObject + Send + Sync>(
967 &self,
968 _caller: &(),
969 _account_id: &Id,
970 _id: &Id,
971 ) -> Result<(), BackendSetError<Self::Error>> {
972 Ok(())
973 }
974
975 fn supports_type<O: JmapObject>(&self) -> bool {
976 true
977 }
978
979 async fn task_list_has_tasks(
980 &self,
981 _caller: &(),
982 _account_id: &Id,
983 _task_list_id: &Id,
984 ) -> Result<bool, MockError> {
985 Ok(false)
986 }
987
988 async fn compute_utc_times(
989 &self,
990 _caller: &(),
991 account_id: &Id,
992 _task: &Task,
993 _tz_hint: Option<&str>,
994 ) -> (Option<UTCDate>, Option<UTCDate>) {
995 self.observed.lock().unwrap().push(account_id.clone());
996 (None, None)
997 }
998 }
999
1000 let backend = Arc::new(AccountIdRecorder {
1001 observed: Mutex::new(Vec::new()),
1002 });
1003 let mut dispatcher: Dispatcher<()> = Dispatcher::new();
1004 register_tasks_handlers(&mut dispatcher, Arc::clone(&backend));
1005
1006 let req = single_call(
1007 "Task/get",
1008 json!({
1009 "accountId": "acc1",
1010 "ids": null,
1011 "properties": ["id", "utcStart"]
1012 }),
1013 "c0",
1014 );
1015 let resp = dispatcher.dispatch(req, (), State::from("s0")).await;
1016 let (_, args, _) = &resp.method_responses[0];
1017 assert!(
1018 args.get("type").is_none(),
1019 "Task/get with utcStart must not error: {args}"
1020 );
1021
1022 let seen = backend.observed.lock().unwrap().clone();
1023 assert_eq!(
1024 seen,
1025 vec![Id::from("acc1")],
1026 "compute_utc_times must receive the request's accountId exactly once \
1027 (one Task synthesized, utcStart requested)"
1028 );
1029 }
1030
1031 #[tokio::test]
1034 async fn per_user_patch_split() {
1035 let backend = Arc::new(MockBackend::new_with_account("acc1"));
1036 let mut dispatcher: Dispatcher<()> = Dispatcher::new();
1037 register_tasks_handlers(&mut dispatcher, Arc::clone(&backend));
1038
1039 let before = backend.per_user_calls.load(Ordering::Relaxed);
1040
1041 let req = single_call(
1042 "Task/set",
1043 json!({
1044 "accountId": "acc1",
1045 "update": { "t1": { "color": "#ff0000" } }
1046 }),
1047 "c0",
1048 );
1049 let resp = dispatcher.dispatch(req, (), State::from("s0")).await;
1050 let (_, args, _) = &resp.method_responses[0];
1051 assert!(
1052 args.get("type").is_none(),
1053 "must not be top-level error: {args}"
1054 );
1055
1056 let after = backend.per_user_calls.load(Ordering::Relaxed);
1057 assert_eq!(
1058 after - before,
1059 1,
1060 "per_user update must have been called exactly once for a color-only patch"
1061 );
1062 }
1063}