Undelete: deleted files, calendar events, contacts and Sieve scripts are kept and restored where they were (UD-1, UD-8 to UD-11)
Files, events and contacts are noted when deleted for good and archived by the unindex task when retention is on; Sieve scripts are archived at deletion. A restore goes back to its folder, calendar or address book if it still exists, takes a free " (restored)" name, comes back inactive for scripts, and is refused over quota with the item left archived. Acceptance tests 6, 8 and 12.
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
|
||||
use crate::utils::{
|
||||
account::Account,
|
||||
webdav::DummyWebDavClient,
|
||||
imap::{ImapConnection, Type},
|
||||
jmap::JmapUtils,
|
||||
pop3::{self, Pop3Connection},
|
||||
@@ -27,10 +28,13 @@ use registry::{
|
||||
},
|
||||
types::duration::Duration,
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use serde_json::{Value, json};
|
||||
use types::id::Id;
|
||||
|
||||
const SECRET: &str = "undelete test user passphrase";
|
||||
const EVENT: &str = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:test\r\nBEGIN:VEVENT\r\nUID:lunch-1\r\nDTSTAMP:20260918T100000Z\r\nDTSTART:20260918T120000Z\r\nDTEND:20260918T130000Z\r\nSUMMARY:Lunch\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
|
||||
const CARD: &str = "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:jane-1\r\nFN:Jane Doe\r\nEND:VCARD\r\n";
|
||||
const DAY: u64 = 86_400;
|
||||
|
||||
pub async fn test(test: &mut TestServer) {
|
||||
@@ -267,6 +271,133 @@ pub async fn test(test: &mut TestServer) {
|
||||
"test 15"
|
||||
);
|
||||
|
||||
// Acceptance test 6: a file, event, contact and script each come back to
|
||||
// the right place (UD-1, UD-8)
|
||||
let dav = DummyWebDavClient::new(
|
||||
user.id().document_id(),
|
||||
"[email protected]",
|
||||
SECRET,
|
||||
"[email protected]",
|
||||
);
|
||||
let home = "undelete%40example.org";
|
||||
dav.mkcol("MKCOL", &format!("/dav/file/{home}/Folder/"), [], [])
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
let file = format!("/dav/file/{home}/Folder/notes.txt");
|
||||
let event = format!("/dav/cal/{home}/default/lunch.ics");
|
||||
let card = format!("/dav/card/{home}/default/jane.vcf");
|
||||
for (path, body) in [
|
||||
(&file, "Some notes.".to_string()),
|
||||
(&event, EVENT.to_string()),
|
||||
(&card, CARD.to_string()),
|
||||
] {
|
||||
dav.request("PUT", path, body)
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
}
|
||||
let script = client
|
||||
.sieve_script_create("Filters", b"keep;".to_vec(), true)
|
||||
.await
|
||||
.unwrap()
|
||||
.take_id();
|
||||
for path in [&file, &event, &card] {
|
||||
dav.request("DELETE", path, "")
|
||||
.await
|
||||
.with_status(StatusCode::NO_CONTENT);
|
||||
}
|
||||
client.sieve_script_deactivate().await.unwrap();
|
||||
client.sieve_script_destroy(&script).await.unwrap();
|
||||
test.wait_for_tasks().await;
|
||||
// A new file takes the old name, so the restored one is renamed
|
||||
dav.request("PUT", &file, "Newer notes.")
|
||||
.await
|
||||
.with_status(StatusCode::CREATED);
|
||||
for kind in ["FileNode", "CalendarEvent", "ContactCard", "SieveScript"] {
|
||||
let item = user
|
||||
.archived()
|
||||
.await
|
||||
.into_iter()
|
||||
.find(|item| item["@type"] == kind)
|
||||
.unwrap_or_else(|| panic!("test 6: no archived {kind}"));
|
||||
user.request_restore(item.object_id()).await;
|
||||
}
|
||||
test.wait_for_tasks().await;
|
||||
dav.request("GET", &format!("/dav/file/{home}/Folder/notes%20%28restored%29.txt"), "")
|
||||
.await
|
||||
.with_status(StatusCode::OK)
|
||||
.with_body("Some notes.");
|
||||
dav.request("GET", &event, "")
|
||||
.await
|
||||
.with_status(StatusCode::OK);
|
||||
dav.request("GET", &card, "").await.with_status(StatusCode::OK);
|
||||
let restored = client
|
||||
.sieve_script_query(
|
||||
jmap_client::sieve::query::Filter::name("Filters").into(),
|
||||
None::<Vec<_>>,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let restored = client
|
||||
.sieve_script_get(restored.ids().first().expect("test 6: script"), None::<Vec<_>>)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(!restored.is_active(), "test 6: a restored script is inactive");
|
||||
assert!(
|
||||
user.archived()
|
||||
.await
|
||||
.iter()
|
||||
.all(|item| item["@type"] == "Email"),
|
||||
"test 6: records removed"
|
||||
);
|
||||
|
||||
// Acceptance test 8: a restore past the account's quota is refused, and
|
||||
// the item stays archived (UD-10)
|
||||
let used = test
|
||||
.server
|
||||
.get_used_quota_account(user.id().document_id())
|
||||
.await
|
||||
.unwrap();
|
||||
admin
|
||||
.registry_update_object(
|
||||
ObjectType::Account,
|
||||
user.id(),
|
||||
json!({ Property::Quotas: {"maxDiskQuota": used} }),
|
||||
)
|
||||
.await;
|
||||
let item = user.archived_with_subject("Via POP3").await;
|
||||
user.request_restore(item.object_id()).await;
|
||||
test.wait_for_tasks_skip_failures().await;
|
||||
assert_eq!(user.count_with_subject("Via POP3").await, 0, "test 8");
|
||||
assert_eq!(
|
||||
user.archived_with_subject("Via POP3").await["status"],
|
||||
"archived",
|
||||
"test 8: still archived"
|
||||
);
|
||||
// The refused restore stays in the queue as failed
|
||||
admin.registry_destroy_all(ObjectType::Task).await;
|
||||
admin
|
||||
.registry_update_object(
|
||||
ObjectType::Account,
|
||||
user.id(),
|
||||
json!({ Property::Quotas: {} }),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Acceptance test 12: past its deadline an item isn't restorable
|
||||
admin.set_retention(Some(1)).await;
|
||||
let id = import(&client, "Short-lived", &[INBOX_ID], &[]).await;
|
||||
client.email_destroy(&id).await.unwrap();
|
||||
test.wait_for_tasks().await;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(2100)).await;
|
||||
assert!(
|
||||
user.archived()
|
||||
.await
|
||||
.iter()
|
||||
.all(|item| item["subject"] != "Short-lived"),
|
||||
"test 12"
|
||||
);
|
||||
|
||||
// Clean up
|
||||
admin.set_retention(None).await;
|
||||
for item in user.archived().await {
|
||||
|
||||
Reference in New Issue
Block a user