Query paging for masked email and archived items #297

Merged
jcoffey-dev merged 2 commits from fix/query-pagination-296 into main 2026-11-06 07:08:38 +00:00
4 changed files with 101 additions and 2 deletions

No files matched your search

+1 -1
View File
@@ -291,7 +291,7 @@ pub(crate) async fn query(mut req: RegistryQueryResponse<'_>) -> trc::Result<Que
_ => 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
+3 -1
View File
@@ -180,8 +180,10 @@ pub(crate) async fn query(mut req: RegistryQueryResponse<'_>) -> trc::Result<Que
(Property::AccountId, _, _) => 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,
+52
View File
@@ -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("[email protected]").await;
admin.find_or_create_domain("example.org").await;
let alice = admin
.create_user_account("[email protected]", 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::<Vec<_>>();
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]
+45
View File
@@ -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::<Vec<_>>();
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<_>>(),
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("[email protected]", 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("[email protected]").await;