Bump JMAP File Storage to draft-ietf-jmap-filenode-14
This commit is contained in:
@@ -481,7 +481,87 @@ pub async fn test(test: &TestServer) {
|
||||
.await
|
||||
.updated(&john_folder_id);
|
||||
|
||||
// Verify Jane can delete the folder
|
||||
// FileNode/copy: Jane copies a node from her own account into John's shared folder
|
||||
let jane_folder_id = jane
|
||||
.jmap_create(
|
||||
MethodObject::FileNode,
|
||||
[json!({"name": "jane-src"})],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await
|
||||
.created(0)
|
||||
.id()
|
||||
.to_string();
|
||||
let copied = jane
|
||||
.jmap_copy(
|
||||
jane,
|
||||
john,
|
||||
MethodObject::FileNode,
|
||||
[(
|
||||
&jane_folder_id,
|
||||
json!({ "parentId": &john_folder_id, "name": "copied-here" }),
|
||||
)],
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
let copied_id = copied.copied(&jane_folder_id).id().to_string();
|
||||
assert_ne!(copied_id, jane_folder_id);
|
||||
jane.jmap_get_account(
|
||||
john,
|
||||
MethodObject::FileNode,
|
||||
[
|
||||
FileNodeProperty::Id,
|
||||
FileNodeProperty::Name,
|
||||
FileNodeProperty::ParentId,
|
||||
],
|
||||
[copied_id.as_str()],
|
||||
)
|
||||
.await
|
||||
.list()[0]
|
||||
.assert_is_equal(json!({
|
||||
"id": &copied_id,
|
||||
"name": "copied-here",
|
||||
"parentId": &john_folder_id,
|
||||
}));
|
||||
// Original still exists in Jane's account (onSuccessDestroyOriginal=false)
|
||||
jane.jmap_get(
|
||||
MethodObject::FileNode,
|
||||
[FileNodeProperty::Id],
|
||||
[jane_folder_id.as_str()],
|
||||
)
|
||||
.await
|
||||
.list()[0]
|
||||
.assert_is_equal(json!({ "id": &jane_folder_id }));
|
||||
|
||||
// onExists=rename on copy: colliding into John's folder again must echo the new name
|
||||
let renamed_copy = jane
|
||||
.jmap_method_calls(json!([[
|
||||
"FileNode/copy",
|
||||
{
|
||||
"fromAccountId": jane.id_string(),
|
||||
"accountId": john.id_string(),
|
||||
"onExists": "rename",
|
||||
"create": {
|
||||
&jane_folder_id: { "parentId": &john_folder_id, "name": "copied-here" }
|
||||
}
|
||||
},
|
||||
"0"
|
||||
]]))
|
||||
.await;
|
||||
let renamed_entry = renamed_copy.copied(&jane_folder_id);
|
||||
let renamed_copy_id = renamed_entry.id().to_string();
|
||||
assert_eq!(renamed_entry.text_field("name"), "copied-here (2)");
|
||||
|
||||
jane.jmap_destroy(
|
||||
MethodObject::FileNode,
|
||||
[&jane_folder_id],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await
|
||||
.destroyed()
|
||||
.for_each(drop);
|
||||
|
||||
// Verify Jane can delete the folder (and the node copied into it)
|
||||
assert_eq!(
|
||||
jane.jmap_destroy_account(
|
||||
john,
|
||||
@@ -491,8 +571,14 @@ pub async fn test(test: &TestServer) {
|
||||
)
|
||||
.await
|
||||
.destroyed()
|
||||
.collect::<Vec<_>>(),
|
||||
[john_folder_id.as_str()]
|
||||
.collect::<std::collections::HashSet<_>>(),
|
||||
[
|
||||
john_folder_id.as_str(),
|
||||
copied_id.as_str(),
|
||||
renamed_copy_id.as_str()
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<std::collections::HashSet<_>>()
|
||||
);
|
||||
|
||||
// Destroy all mailboxes
|
||||
|
||||
@@ -468,6 +468,216 @@ pub async fn test(test: &TestServer) {
|
||||
.destroyed()
|
||||
.for_each(drop);
|
||||
|
||||
// Pending+Reject: two creates with the same name in one batch, default onExists
|
||||
let response = account
|
||||
.jmap_create(
|
||||
MethodObject::FileNode,
|
||||
[
|
||||
json!({"name": "twin-reject"}),
|
||||
json!({"name": "twin-reject"}),
|
||||
],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await;
|
||||
let twin_first = response.created(0).id().to_string();
|
||||
let err = response.not_created(1);
|
||||
assert_eq!(err.typ(), "alreadyExists");
|
||||
assert!(
|
||||
err.pointer("/existingId").is_none(),
|
||||
"Pending Create collision has no committed existingId, got {err:?}"
|
||||
);
|
||||
account
|
||||
.jmap_destroy(
|
||||
MethodObject::FileNode,
|
||||
[&twin_first],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await
|
||||
.destroyed()
|
||||
.for_each(drop);
|
||||
|
||||
// Pending+Rename: second create within the batch should auto-rename
|
||||
let response = account
|
||||
.jmap_create(
|
||||
MethodObject::FileNode,
|
||||
[
|
||||
json!({"name": "twin-rename"}),
|
||||
json!({"name": "twin-rename"}),
|
||||
],
|
||||
[("onExists", "rename")],
|
||||
)
|
||||
.await;
|
||||
let twin_a = response.created(0).id().to_string();
|
||||
let twin_b_entry = response.created(1);
|
||||
let twin_b = twin_b_entry.id().to_string();
|
||||
assert_eq!(twin_b_entry.text_field("name"), "twin-rename (2)");
|
||||
account
|
||||
.jmap_destroy(
|
||||
MethodObject::FileNode,
|
||||
[&twin_a, &twin_b],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await
|
||||
.destroyed()
|
||||
.for_each(drop);
|
||||
|
||||
// Pending+Replace: within-batch replace is intentionally not supported; second is rejected
|
||||
let response = account
|
||||
.jmap_create(
|
||||
MethodObject::FileNode,
|
||||
[
|
||||
json!({"name": "twin-replace"}),
|
||||
json!({"name": "twin-replace"}),
|
||||
],
|
||||
[("onExists", "replace")],
|
||||
)
|
||||
.await;
|
||||
let twin_survivor = response.created(0).id().to_string();
|
||||
let err = response.not_created(1);
|
||||
assert_eq!(err.typ(), "alreadyExists");
|
||||
assert!(
|
||||
err.pointer("/existingId").is_none(),
|
||||
"Pending Create + Replace returns alreadyExists with no existingId, got {err:?}"
|
||||
);
|
||||
account
|
||||
.jmap_destroy(
|
||||
MethodObject::FileNode,
|
||||
[&twin_survivor],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await
|
||||
.destroyed()
|
||||
.for_each(drop);
|
||||
|
||||
// Pending+Newest: in-batch newest comparison is intentionally not supported; second is rejected
|
||||
let response = account
|
||||
.jmap_create(
|
||||
MethodObject::FileNode,
|
||||
[
|
||||
json!({"name": "twin-newest", "modified": "2020-01-01T00:00:00Z"}),
|
||||
json!({"name": "twin-newest", "modified": "2040-01-01T00:00:00Z"}),
|
||||
],
|
||||
[("onExists", "newest")],
|
||||
)
|
||||
.await;
|
||||
let twin_keep = response.created(0).id().to_string();
|
||||
let err = response.not_created(1);
|
||||
assert_eq!(err.typ(), "alreadyExists");
|
||||
account
|
||||
.jmap_destroy(
|
||||
MethodObject::FileNode,
|
||||
[&twin_keep],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await
|
||||
.destroyed()
|
||||
.for_each(drop);
|
||||
|
||||
// Create+Update collision in one batch
|
||||
let setup = account
|
||||
.jmap_create(
|
||||
MethodObject::FileNode,
|
||||
[json!({"name": "lhs"})],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await;
|
||||
let lhs_id = setup.created(0).id().to_string();
|
||||
let response = account
|
||||
.jmap_method_calls(json!([[
|
||||
"FileNode/set",
|
||||
{
|
||||
"accountId": account.id_string(),
|
||||
"update": { &lhs_id: { "name": "merged" } },
|
||||
"create": { "new1": { "name": "merged" } }
|
||||
},
|
||||
"0"
|
||||
]]))
|
||||
.await;
|
||||
let created_new = response
|
||||
.pointer("/methodResponses/0/1/created/new1")
|
||||
.expect("new1 should be in created");
|
||||
let new1_id = created_new.id().to_string();
|
||||
let upd_err = response
|
||||
.pointer(&format!("/methodResponses/0/1/notUpdated/{lhs_id}"))
|
||||
.expect("update should fail");
|
||||
assert_eq!(upd_err.typ(), "alreadyExists");
|
||||
assert!(
|
||||
upd_err.pointer("/existingId").is_none(),
|
||||
"Pending-from-Create collision has no existingId, got {upd_err:?}"
|
||||
);
|
||||
account
|
||||
.jmap_destroy(
|
||||
MethodObject::FileNode,
|
||||
[&lhs_id, &new1_id],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await
|
||||
.destroyed()
|
||||
.for_each(drop);
|
||||
|
||||
// compareCaseInsensitively + Pending: in-batch "FOO"/"foo" collide when the flag is set
|
||||
let response = account
|
||||
.jmap_create(
|
||||
MethodObject::FileNode,
|
||||
[json!({"name": "FOO"}), json!({"name": "foo"})],
|
||||
[("compareCaseInsensitively", true)],
|
||||
)
|
||||
.await;
|
||||
let case_keep = response.created(0).id().to_string();
|
||||
assert_eq!(response.not_created(1).typ(), "alreadyExists");
|
||||
account
|
||||
.jmap_destroy(
|
||||
MethodObject::FileNode,
|
||||
[&case_keep],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await
|
||||
.destroyed()
|
||||
.for_each(drop);
|
||||
|
||||
// onExists=newest: incoming must have a strictly later modified to win
|
||||
let response = account
|
||||
.jmap_create(
|
||||
MethodObject::FileNode,
|
||||
[json!({"name": "stamped", "modified": "2030-01-01T00:00:00Z"})],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await;
|
||||
let stamped_id = response.created(0).id().to_string();
|
||||
let older_attempt = account
|
||||
.jmap_create(
|
||||
MethodObject::FileNode,
|
||||
[json!({"name": "stamped", "modified": "2020-01-01T00:00:00Z"})],
|
||||
[("onExists", "newest")],
|
||||
)
|
||||
.await;
|
||||
let err = older_attempt.not_created(0);
|
||||
assert_eq!(err.typ(), "alreadyExists");
|
||||
assert_eq!(err.text_field("existingId"), stamped_id.as_str());
|
||||
let newer_attempt = account
|
||||
.jmap_create(
|
||||
MethodObject::FileNode,
|
||||
[json!({"name": "stamped", "modified": "2040-01-01T00:00:00Z"})],
|
||||
[("onExists", "newest")],
|
||||
)
|
||||
.await;
|
||||
let stamped_winner = newer_attempt.created(0).id().to_string();
|
||||
assert_ne!(stamped_winner, stamped_id);
|
||||
let destroyed = newer_attempt.destroyed().collect::<AHashSet<_>>();
|
||||
assert!(
|
||||
destroyed.contains(stamped_id.as_str()),
|
||||
"Expected {stamped_id} to be destroyed by newer onExists=newest, got {destroyed:?}"
|
||||
);
|
||||
account
|
||||
.jmap_destroy(
|
||||
MethodObject::FileNode,
|
||||
[&stamped_winner],
|
||||
Vec::<(&str, &str)>::new(),
|
||||
)
|
||||
.await
|
||||
.destroyed()
|
||||
.for_each(drop);
|
||||
|
||||
// Make sure everything is gone
|
||||
test.assert_is_empty().await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user