mirror of
https://github.com/narrrl/proton-sdk-rs.git
synced 2026-11-03 16:41:26 +01:00
334 lines
9.8 KiB
Rust
334 lines
9.8 KiB
Rust
//! Live integration: folder operations, node moves, and signature
|
|
//! verification against a real Proton account.
|
|
//!
|
|
//! Skipped by default. Run against the test account with:
|
|
//! cargo test -p proton-drive-rs --test live_drive_ops -- --ignored --nocapture
|
|
//!
|
|
//! Each test cleans up after itself (trash + delete-from-trash) so the account
|
|
//! stays reusable across runs.
|
|
|
|
mod common;
|
|
|
|
use proton_drive_rs::proton_sdk::crypto::VerificationStatus;
|
|
use proton_drive_rs::{Node, NodeKind};
|
|
use proton_sdk::ids::NodeUid;
|
|
|
|
/// Trash then permanently delete the given nodes; best-effort, logs on failure.
|
|
async fn cleanup(client: &proton_drive_rs::ProtonDriveClient, uids: &[NodeUid]) {
|
|
if let Err(e) = client.trash_nodes(uids).await {
|
|
eprintln!("[cleanup] trash failed: {e}");
|
|
return;
|
|
}
|
|
if let Err(e) = client.delete_nodes(uids).await {
|
|
eprintln!("[cleanup] delete failed: {e}");
|
|
}
|
|
}
|
|
|
|
/// Fetch a node that must exist, panicking with context otherwise.
|
|
async fn get(client: &proton_drive_rs::ProtonDriveClient, uid: &NodeUid, what: &str) -> Node {
|
|
client
|
|
.get_node(uid)
|
|
.await
|
|
.unwrap_or_else(|e| panic!("get_node({what}) errored: {e}"))
|
|
.unwrap_or_else(|| panic!("get_node({what}) returned None"))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Folder operations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// create → rename → trash → restore → delete, asserting state at each step.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn folder_create_rename_trash_restore() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
|
|
// create
|
|
let name = format!("folder-ops-{}", common::unique_suffix());
|
|
let uid = client
|
|
.create_folder(&root.uid, &name, None)
|
|
.await
|
|
.expect("create_folder");
|
|
|
|
let node = get(client, &uid, "after create").await;
|
|
assert!(matches!(node.kind, NodeKind::Folder), "must be a folder");
|
|
assert_eq!(node.name, name, "name must round-trip");
|
|
assert_eq!(
|
|
node.parent_uid.as_ref(),
|
|
Some(&root.uid),
|
|
"parent must be root"
|
|
);
|
|
|
|
// rename
|
|
let renamed = format!("{name}-renamed");
|
|
client
|
|
.rename_node(&uid, &renamed, None)
|
|
.await
|
|
.expect("rename_node");
|
|
let node = get(client, &uid, "after rename").await;
|
|
assert_eq!(node.name, renamed, "rename must take effect");
|
|
|
|
// trash
|
|
client
|
|
.trash_nodes(std::slice::from_ref(&uid))
|
|
.await
|
|
.expect("trash");
|
|
let node = get(client, &uid, "after trash").await;
|
|
assert!(node.trashed, "node must report trashed");
|
|
let trash = client
|
|
.enumerate_trash_node_uids()
|
|
.await
|
|
.expect("enumerate trash");
|
|
assert!(trash.contains(&uid), "trash listing must include the node");
|
|
|
|
// restore
|
|
client
|
|
.restore_nodes(std::slice::from_ref(&uid))
|
|
.await
|
|
.expect("restore");
|
|
let node = get(client, &uid, "after restore").await;
|
|
assert!(!node.trashed, "node must no longer be trashed");
|
|
|
|
// delete (final)
|
|
cleanup(client, &[uid]).await;
|
|
}
|
|
|
|
/// empty_trash removes a trashed node from the trash listing.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn folder_empty_trash() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
|
|
let name = format!("empty-trash-{}", common::unique_suffix());
|
|
let uid = client
|
|
.create_folder(&root.uid, &name, None)
|
|
.await
|
|
.expect("create_folder");
|
|
|
|
client
|
|
.trash_nodes(std::slice::from_ref(&uid))
|
|
.await
|
|
.expect("trash");
|
|
assert!(
|
|
client
|
|
.enumerate_trash_node_uids()
|
|
.await
|
|
.expect("enumerate trash")
|
|
.contains(&uid),
|
|
"node must be in trash before empty"
|
|
);
|
|
|
|
client.empty_trash().await.expect("empty_trash");
|
|
|
|
assert!(
|
|
!client
|
|
.enumerate_trash_node_uids()
|
|
.await
|
|
.expect("enumerate trash")
|
|
.contains(&uid),
|
|
"node must be gone from trash after empty_trash"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Move
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Single move: a child folder relocates from parent A to parent B.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn move_node_single() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
let suffix = common::unique_suffix();
|
|
|
|
let a = client
|
|
.create_folder(&root.uid, &format!("move-src-{suffix}"), None)
|
|
.await
|
|
.expect("create A");
|
|
let b = client
|
|
.create_folder(&root.uid, &format!("move-dst-{suffix}"), None)
|
|
.await
|
|
.expect("create B");
|
|
let child = client
|
|
.create_folder(&a, &format!("move-child-{suffix}"), None)
|
|
.await
|
|
.expect("create child in A");
|
|
|
|
// Precondition: child parented to A.
|
|
assert_eq!(
|
|
get(client, &child, "before move").await.parent_uid.as_ref(),
|
|
Some(&a),
|
|
"child must start under A"
|
|
);
|
|
|
|
client.move_node(&child, &b).await.expect("move_node");
|
|
|
|
assert_eq!(
|
|
get(client, &child, "after move").await.parent_uid.as_ref(),
|
|
Some(&b),
|
|
"child must be reparented to B"
|
|
);
|
|
|
|
cleanup(client, &[child, a, b]).await;
|
|
}
|
|
|
|
/// Batch move: two children relocate to a destination folder in one call.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn move_nodes_batch() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
let suffix = common::unique_suffix();
|
|
|
|
let dst = client
|
|
.create_folder(&root.uid, &format!("batch-dst-{suffix}"), None)
|
|
.await
|
|
.expect("create dst");
|
|
let c1 = client
|
|
.create_folder(&root.uid, &format!("batch-c1-{suffix}"), None)
|
|
.await
|
|
.expect("create c1");
|
|
let c2 = client
|
|
.create_folder(&root.uid, &format!("batch-c2-{suffix}"), None)
|
|
.await
|
|
.expect("create c2");
|
|
|
|
client
|
|
.move_nodes(&[c1.clone(), c2.clone()], &dst)
|
|
.await
|
|
.expect("move_nodes");
|
|
|
|
assert_eq!(
|
|
get(client, &c1, "c1 after move").await.parent_uid.as_ref(),
|
|
Some(&dst),
|
|
"c1 must be under dst"
|
|
);
|
|
assert_eq!(
|
|
get(client, &c2, "c2 after move").await.parent_uid.as_ref(),
|
|
Some(&dst),
|
|
"c2 must be under dst"
|
|
);
|
|
|
|
cleanup(client, &[c1, c2, dst]).await;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Signature verification
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// A self-uploaded file must verify cleanly: this exercises the live
|
|
/// `core/v4/keys/all` author-key resolution path (the offline tests sign with
|
|
/// in-process keys and never prove the remote lookup works).
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn verification_file_fully_verified() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
|
|
let name = format!("verify-{}.txt", common::unique_suffix());
|
|
let payload = b"verification probe payload".to_vec();
|
|
let uid = client
|
|
.upload_file(&root.uid, &name, "text/plain", &payload)
|
|
.await
|
|
.expect("upload_file");
|
|
|
|
let node = get(client, &uid, "uploaded file").await;
|
|
let v = node.verification;
|
|
|
|
// Name is inline-signed to the parent by our address key; an `Ok` here
|
|
// proves the author email resolved to a usable verification key remotely.
|
|
assert_eq!(
|
|
v.name,
|
|
VerificationStatus::Ok,
|
|
"name signature must verify against the resolved author key"
|
|
);
|
|
assert_eq!(
|
|
v.passphrase,
|
|
VerificationStatus::Ok,
|
|
"node passphrase signature must verify"
|
|
);
|
|
assert_eq!(
|
|
v.content_key,
|
|
Some(VerificationStatus::Ok),
|
|
"content-key signature (node-key signer) must verify"
|
|
);
|
|
assert!(
|
|
v.is_fully_verified(),
|
|
"no signature may be NoVerifier/Failed: {v:?}"
|
|
);
|
|
|
|
cleanup(client, &[uid]).await;
|
|
}
|
|
|
|
/// A created folder must verify cleanly (name + passphrase signed; no
|
|
/// content-key / xattr for a plain folder).
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn verification_folder_fully_verified() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
|
|
let name = format!("verify-folder-{}", common::unique_suffix());
|
|
let uid = client
|
|
.create_folder(&root.uid, &name, None)
|
|
.await
|
|
.expect("create_folder");
|
|
|
|
let node = get(client, &uid, "created folder").await;
|
|
let v = node.verification;
|
|
assert_eq!(v.name, VerificationStatus::Ok, "folder name must verify");
|
|
assert_eq!(
|
|
v.passphrase,
|
|
VerificationStatus::Ok,
|
|
"folder passphrase must verify"
|
|
);
|
|
assert!(v.is_fully_verified(), "folder must fully verify: {v:?}");
|
|
|
|
cleanup(client, &[uid]).await;
|
|
}
|