diff --git a/crates/jmap/src/inbuxa/masked_email.rs b/crates/jmap/src/inbuxa/masked_email.rs index 65afa735..5650e1fb 100644 --- a/crates/jmap/src/inbuxa/masked_email.rs +++ b/crates/jmap/src/inbuxa/masked_email.rs @@ -291,7 +291,7 @@ pub(crate) async fn query(mut req: RegistryQueryResponse<'_>) -> trc::Result false, })?; req.request - .extract_parameters(req.server.core.jmap.query_max_results, Some(Property::Id))?; + .extract_parameters(req.server.core.jmap.query_max_results, None)?; // ME-19: one account's masks, or every mask for a server-level // administrator who asks for no account in particular diff --git a/crates/jmap/src/inbuxa/undelete.rs b/crates/jmap/src/inbuxa/undelete.rs index 97570c3b..4eb57324 100644 --- a/crates/jmap/src/inbuxa/undelete.rs +++ b/crates/jmap/src/inbuxa/undelete.rs @@ -180,8 +180,10 @@ pub(crate) async fn query(mut req: RegistryQueryResponse<'_>) -> trc::Result true, _ => false, })?; + // `None`: the items are filtered and sorted here, so position, anchor + // and limit stay on the request for the response builder req.request - .extract_parameters(req.server.core.jmap.query_max_results, Some(Property::Id))?; + .extract_parameters(req.server.core.jmap.query_max_results, None)?; let mut items = undelete::records::of_account( &req.server.core.storage.data, diff --git a/tests/src/system/masked_email.rs b/tests/src/system/masked_email.rs index b307a66d..4d33fe9c 100644 --- a/tests/src/system/masked_email.rs +++ b/tests/src/system/masked_email.rs @@ -492,6 +492,58 @@ pub async fn masked_email_compat() { } } +/// `cargo test -p tests masked_email_query_pagination -- --ignored`. +#[ignore] +#[tokio::test(flavor = "multi_thread")] +pub async fn masked_email_query_pagination() { + let test = TestServerBuilder::new("masked_email_query_pagination") + .await + .with_default_listeners() + .await + .build() + .await; + let admin = test.create_admin_account("admin@example.org").await; + admin.find_or_create_domain("example.org").await; + let alice = admin + .create_user_account("alice@example.org", SECRET_ALICE, "Alice", &[], vec![]) + .await; + let mut ids = Vec::new(); + for prefix in ["first", "second", "third"] { + ids.push(alice.x_create(json!({"emailPrefix": prefix})).await); + } + ids.sort_unstable(); + + let mut seen = Vec::new(); + for (position, id) in ids.iter().enumerate() { + // calculateTotal must be false to exercise parameter extraction's + // storage-level pagination path. + let page = alice + .registry_query_paginated( + ObjectType::MaskedEmail, + "id", + true, + Some(position as i32), + Some(1), + None, + None, + false, + ) + .await; + let page_ids = page.object_ids().collect::>(); + assert!( + page_ids.iter().all(|id| !seen.contains(id)), + "a previous page's result was repeated" + ); + assert_eq!(page_ids, vec![*id], "page at position {position}"); + assert_eq!(page.method_response()["position"], json!(position)); + seen.extend(page_ids); + } + + if test.is_reset() { + test.temp_dir.delete(); + } +} + /// Runs the masked email tests alone: /// `cargo test -p tests masked_email_tests -- --ignored`. #[ignore] diff --git a/tests/src/system/undelete.rs b/tests/src/system/undelete.rs index 7fbde1d5..3ec6486f 100644 --- a/tests/src/system/undelete.rs +++ b/tests/src/system/undelete.rs @@ -273,6 +273,44 @@ pub async fn test(test: &mut TestServer) { .await; assert_eq!(found.len(), 1, "query text"); + // /query pages: each position gives the next item, never the first page + // again (calculateTotal off, the path that dropped them) + let all = user + .registry_query_paginated( + ObjectType::ArchivedItem, + "id", + false, + None, + None, + None, + None, + false, + ) + .await + .object_ids() + .collect::>(); + assert!(all.len() >= 3, "query pages: {all:?}"); + for (position, id) in all.iter().enumerate() { + let page = user + .registry_query_paginated( + ObjectType::ArchivedItem, + "id", + false, + Some(position as i32), + Some(1), + None, + None, + false, + ) + .await; + assert_eq!( + page.object_ids().collect::>(), + vec![*id], + "query page at position {position}" + ); + assert_eq!(page.method_response()["position"], json!(position)); + } + // Acceptance test 15: nobody else sees the archive (UD-7) let other = admin .create_user_account("other@example.org", SECRET, "Other", &[], vec![]) @@ -630,6 +668,13 @@ pub async fn undelete_tests() { ..Default::default() }) .await + // test 14 shares with an account it has nothing else to do with, as + // system_tests allows + .with_object(registry::schema::structs::Sharing { + allow_directory_queries: true, + ..Default::default() + }) + .await .build() .await; let admin = test.create_admin_account("admin@example.org").await;