mirror of
https://github.com/narrrl/proton-sdk-rs.git
synced 2026-11-03 16:41:26 +01:00
507 lines
15 KiB
Rust
507 lines
15 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::{LinkId, 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");
|
|
|
|
let outcomes = client
|
|
.move_nodes(&[c1.clone(), c2.clone()], &dst)
|
|
.await
|
|
.expect("move_nodes");
|
|
|
|
assert_eq!(outcomes.len(), 2, "one outcome per requested node");
|
|
for (uid, outcome) in &outcomes {
|
|
outcome
|
|
.as_ref()
|
|
.unwrap_or_else(|e| panic!("move of {uid} must succeed: {e}"));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// One bad node must not sink the batch: `move_nodes` reports per-node outcomes
|
|
/// (C# `MoveNodesAsync`'s result map), so a bogus link id fails alone while its
|
|
/// neighbour still moves.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn move_nodes_partial_failure() {
|
|
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!("partial-dst-{suffix}"), None)
|
|
.await
|
|
.expect("create dst");
|
|
let good = client
|
|
.create_folder(&root.uid, &format!("partial-good-{suffix}"), None)
|
|
.await
|
|
.expect("create good");
|
|
let bogus = NodeUid::new(
|
|
dst.volume_id.clone(),
|
|
LinkId::new(format!("nonexistent-link-{suffix}")),
|
|
);
|
|
|
|
let outcomes = client
|
|
.move_nodes(&[good.clone(), bogus.clone()], &dst)
|
|
.await
|
|
.expect("move_nodes must not fail as a whole");
|
|
|
|
assert_eq!(outcomes.len(), 2, "one outcome per requested node");
|
|
assert_eq!(outcomes[0].0, good, "outcomes keep the input order");
|
|
outcomes[0]
|
|
.1
|
|
.as_ref()
|
|
.expect("the real node must still move");
|
|
assert_eq!(outcomes[1].0, bogus);
|
|
let e = outcomes[1]
|
|
.1
|
|
.as_ref()
|
|
.expect_err("the bogus node must report its own failure");
|
|
eprintln!("[info] bogus node outcome: {e}");
|
|
|
|
assert_eq!(
|
|
get(client, &good, "good after move")
|
|
.await
|
|
.parent_uid
|
|
.as_ref(),
|
|
Some(&dst),
|
|
"the real node must be under dst"
|
|
);
|
|
|
|
cleanup(client, &[good, 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;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Hierarchy & path helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// `create_folder_path` → `get_node_by_path` → `get_node_hierarchy` →
|
|
/// `get_node_path`, asserting the four agree on the same nested tree.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn folder_path_helpers_round_trip() {
|
|
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 top = format!("path-ops-{}", common::unique_suffix());
|
|
let path = format!("{top}/nested/deep");
|
|
|
|
// mkdir -p
|
|
let deep = client
|
|
.create_folder_path(&root.uid, &path)
|
|
.await
|
|
.expect("create_folder_path");
|
|
|
|
// Idempotent: a second call adopts the existing folders, same uid.
|
|
let again = client
|
|
.create_folder_path(&root.uid, &path)
|
|
.await
|
|
.expect("create_folder_path is idempotent");
|
|
assert_eq!(again, deep, "re-creating a path must reuse the folders");
|
|
|
|
// Lookup by path, tolerating separator noise.
|
|
for variant in [path.as_str(), &format!("/{path}"), &format!("//{path}//")] {
|
|
let found = client
|
|
.get_node_by_path(&root.uid, variant)
|
|
.await
|
|
.unwrap_or_else(|e| panic!("get_node_by_path({variant}) errored: {e}"))
|
|
.unwrap_or_else(|| panic!("get_node_by_path({variant}) returned None"));
|
|
assert_eq!(found.uid, deep, "path {variant} must resolve to the folder");
|
|
}
|
|
|
|
// An empty path is the root itself.
|
|
let at_root = client
|
|
.get_node_by_path(&root.uid, "/")
|
|
.await
|
|
.expect("get_node_by_path(/)")
|
|
.expect("root must resolve");
|
|
assert_eq!(at_root.uid, root.uid, "empty path is the root");
|
|
|
|
// A missing segment resolves to None, not an error.
|
|
assert!(
|
|
client
|
|
.get_node_by_path(&root.uid, &format!("{top}/nope"))
|
|
.await
|
|
.expect("missing path must not error")
|
|
.is_none(),
|
|
"a missing segment must resolve to None"
|
|
);
|
|
|
|
// Hierarchy is root-first and ends at the queried node.
|
|
let hierarchy = client
|
|
.get_node_hierarchy(&deep)
|
|
.await
|
|
.expect("get_node_hierarchy")
|
|
.expect("hierarchy for an existing node");
|
|
assert_eq!(
|
|
hierarchy.first().map(|n| &n.uid),
|
|
Some(&root.uid),
|
|
"hierarchy must start at the tree root"
|
|
);
|
|
assert_eq!(
|
|
hierarchy.last().map(|n| &n.uid),
|
|
Some(&deep),
|
|
"hierarchy must end at the queried node"
|
|
);
|
|
let names: Vec<&str> = hierarchy.iter().skip(1).map(|n| n.name.as_str()).collect();
|
|
assert_eq!(names, [top.as_str(), "nested", "deep"]);
|
|
|
|
// The rendered path round-trips back through the path lookup.
|
|
let rendered = client
|
|
.get_node_path(&deep)
|
|
.await
|
|
.expect("get_node_path")
|
|
.expect("path for an existing node");
|
|
assert_eq!(rendered, format!("/{path}"));
|
|
let reresolved = client
|
|
.get_node_by_path(&root.uid, &rendered)
|
|
.await
|
|
.expect("re-resolve rendered path")
|
|
.expect("rendered path must resolve");
|
|
assert_eq!(reresolved.uid, deep, "rendered path must round-trip");
|
|
|
|
// Only the top folder needs cleaning — delete cascades to its children.
|
|
let top_uid = client
|
|
.get_node_by_path(&root.uid, &top)
|
|
.await
|
|
.expect("locate top folder")
|
|
.expect("top folder must exist")
|
|
.uid;
|
|
cleanup(client, &[top_uid]).await;
|
|
}
|