Bump JMAP File Storage to draft-ietf-jmap-filenode-13

This commit is contained in:
Maurus Decimus
2026-05-15 19:08:54 +02:00
parent e049f9ff4f
commit b9794315aa
9 changed files with 829 additions and 79 deletions

View File

@@ -48,7 +48,10 @@ pub async fn test(test: &TestServer) {
"name": "Test #1",
"myRights": {
"mayRead": true,
"mayWrite": true,
"mayAddChildren": true,
"mayRename": true,
"mayDelete": true,
"mayModifyContent": true,
"mayShare": true
},
"shareWith": {}
@@ -113,7 +116,10 @@ pub async fn test(test: &TestServer) {
"shareWith": {
&jane_id : {
"mayRead": true,
"mayWrite": false,
"mayAddChildren": false,
"mayRename": false,
"mayDelete": false,
"mayModifyContent": false,
"mayShare": false
}
}
@@ -137,7 +143,10 @@ pub async fn test(test: &TestServer) {
"name": "Test #1",
"myRights": {
"mayRead": true,
"mayWrite": false,
"mayAddChildren": false,
"mayRename": false,
"mayDelete": false,
"mayModifyContent": false,
"mayShare": false
}
}));
@@ -178,12 +187,18 @@ pub async fn test(test: &TestServer) {
"objectId": &john_folder_id,
"oldRights": {
"mayRead": false,
"mayWrite": false,
"mayAddChildren": false,
"mayRename": false,
"mayDelete": false,
"mayModifyContent": false,
"mayShare": false
},
"newRights": {
"mayRead": true,
"mayWrite": false,
"mayAddChildren": false,
"mayRename": false,
"mayDelete": false,
"mayModifyContent": false,
"mayShare": false
},
"name": null
@@ -221,7 +236,10 @@ pub async fn test(test: &TestServer) {
[(
&john_folder_id,
json!({
format!("shareWith/{jane_id}/mayWrite"): true,
format!("shareWith/{jane_id}/mayAddChildren"): true,
format!("shareWith/{jane_id}/mayRename"): true,
format!("shareWith/{jane_id}/mayDelete"): true,
format!("shareWith/{jane_id}/mayModifyContent"): true,
}),
)],
Vec::<(&str, &str)>::new(),
@@ -245,7 +263,10 @@ pub async fn test(test: &TestServer) {
"name": "Test #1",
"myRights": {
"mayRead": true,
"mayWrite": true,
"mayAddChildren": true,
"mayRename": true,
"mayDelete": true,
"mayModifyContent": true,
"mayShare": false
}
}));
@@ -286,12 +307,18 @@ pub async fn test(test: &TestServer) {
"objectId": &john_folder_id,
"oldRights": {
"mayRead": true,
"mayWrite": false,
"mayAddChildren": false,
"mayRename": false,
"mayDelete": false,
"mayModifyContent": false,
"mayShare": false
},
"newRights": {
"mayRead": true,
"mayWrite": true,
"mayAddChildren": true,
"mayRename": true,
"mayDelete": true,
"mayModifyContent": true,
"mayShare": false
},
"name": null
@@ -419,12 +446,18 @@ pub async fn test(test: &TestServer) {
"objectId": &john_folder_id,
"oldRights": {
"mayRead": true,
"mayWrite": true,
"mayAddChildren": true,
"mayRename": true,
"mayDelete": true,
"mayModifyContent": true,
"mayShare": false
},
"newRights": {
"mayRead": false,
"mayWrite": false,
"mayAddChildren": false,
"mayRename": false,
"mayDelete": false,
"mayModifyContent": false,
"mayShare": false
},
"name": null
@@ -437,7 +470,10 @@ pub async fn test(test: &TestServer) {
&john_folder_id,
json!({
format!("shareWith/{jane_id}/mayRead"): true,
format!("shareWith/{jane_id}/mayWrite"): true,
format!("shareWith/{jane_id}/mayAddChildren"): true,
format!("shareWith/{jane_id}/mayRename"): true,
format!("shareWith/{jane_id}/mayDelete"): true,
format!("shareWith/{jane_id}/mayModifyContent"): true,
}),
)],
Vec::<(&str, &str)>::new(),

View File

@@ -197,10 +197,9 @@ pub async fn test(test: &TestServer) {
Vec::<(&str, &str)>::new(),
)
.await;
assert_eq!(
response.not_created(0).description(),
"A node with the same name already exists in this folder."
);
let err = response.not_created(0);
assert_eq!(err.typ(), "alreadyExists");
assert_eq!(err.text_field("existingId"), sub_folder_id.as_str());
assert_eq!(
response.not_created(1).description(),
"Parent ID does not exist or is not a folder."
@@ -331,6 +330,144 @@ pub async fn test(test: &TestServer) {
.collect::<AHashSet<_>>()
);
// fetchParents: requesting a leaf should return its ancestors too
let response = account
.jmap_create(
MethodObject::FileNode,
[
json!({"name": "fp-root"}),
json!({"name": "fp-sub", "parentId": "#i0"}),
json!({"name": "fp-leaf", "parentId": "#i1"}),
],
Vec::<(&str, &str)>::new(),
)
.await;
let fp_root = response.created(0).id().to_string();
let fp_sub = response.created(1).id().to_string();
let fp_leaf = response.created(2).id().to_string();
let response = account
.jmap_method_calls(json!([[
"FileNode/get",
{
"accountId": account.id_string(),
"ids": [&fp_leaf],
"fetchParents": true,
"properties": ["id"]
},
"0"
]]))
.await;
let ids = response
.pointer("/methodResponses/0/1/list")
.and_then(|v| v.as_array())
.map(|list| {
list.iter()
.map(|n| n.text_field("id").to_string())
.collect::<AHashSet<_>>()
})
.expect("fetchParents response");
assert_eq!(
ids,
[fp_leaf.as_str(), fp_sub.as_str(), fp_root.as_str()]
.into_iter()
.map(str::to_string)
.collect::<AHashSet<_>>()
);
account
.jmap_destroy(
MethodObject::FileNode,
[&fp_root],
[("onDestroyRemoveChildren", true)],
)
.await
.destroyed()
.for_each(drop);
// onExists=rename should produce a unique sibling name
let response = account
.jmap_create(
MethodObject::FileNode,
[json!({"name": "dupe.txt", "parentId": null, "blobId": null})],
Vec::<(&str, &str)>::new(),
)
.await;
let dupe_orig = response.created(0).id().to_string();
let response = account
.jmap_create(
MethodObject::FileNode,
[json!({"name": "dupe.txt"})],
[("onExists", "rename")],
)
.await;
let dupe_renamed = response.created(0);
let dupe_renamed_id = dupe_renamed.id().to_string();
assert_eq!(dupe_renamed.text_field("name"), "dupe (2).txt");
// onExists=reject (default) should return alreadyExists with existingId
let response = account
.jmap_create(
MethodObject::FileNode,
[json!({"name": "dupe.txt"})],
Vec::<(&str, &str)>::new(),
)
.await;
let err = response.not_created(0);
assert_eq!(err.typ(), "alreadyExists");
assert_eq!(err.text_field("existingId"), dupe_orig.as_str());
// onExists=replace should destroy the existing sibling
let response = account
.jmap_create(
MethodObject::FileNode,
[json!({"name": "dupe.txt"})],
[("onExists", "replace")],
)
.await;
let dupe_replacement = response.created(0).id().to_string();
let destroyed = response.destroyed().collect::<AHashSet<_>>();
assert!(
destroyed.contains(dupe_orig.as_str()),
"Expected old id {dupe_orig} to be destroyed, got {destroyed:?}"
);
account
.jmap_destroy(
MethodObject::FileNode,
[&dupe_renamed_id, &dupe_replacement],
[("onDestroyRemoveChildren", true)],
)
.await
.destroyed()
.for_each(drop);
// compareCaseInsensitively should treat sibling names as case-insensitive
let response = account
.jmap_create(
MethodObject::FileNode,
[json!({"name": "CASE"})],
Vec::<(&str, &str)>::new(),
)
.await;
let case_id = response.created(0).id().to_string();
let response = account
.jmap_create(
MethodObject::FileNode,
[json!({"name": "case"})],
[("compareCaseInsensitively", true)],
)
.await;
let err = response.not_created(0);
assert_eq!(err.typ(), "alreadyExists");
assert_eq!(err.text_field("existingId"), case_id.as_str());
account
.jmap_destroy(
MethodObject::FileNode,
[&case_id],
Vec::<(&str, &str)>::new(),
)
.await
.destroyed()
.for_each(drop);
// Make sure everything is gone
test.assert_is_empty().await;
}