use crate::common::http_split_support::*;
use crate::common::start_server;
use loonfs_api::{DeleteDirectoryBehavior, DestinationBehavior};
use loonfs_client::{ClientError, CopyOptions, DeleteOptions, NamespacePath, PutFileOptions};
use loonfs_test_support::http::raw_agent;
use loonfs_test_support::ids::namespace_id;
use tempfile::tempdir;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn http_stat_omits_the_root_name_and_carries_named_child_names() {
let temp_dir = tempdir().expect("tempdir");
let harness = start_server(test_config(
temp_dir.path().join("store"),
"loonfs-server-root-name",
"http-root-name",
))
.await;
harness
.client
.create_namespace(&namespace_id("demo"))
.await
.expect("create namespace");
harness
.client
.create_directory(
&NamespacePath::parse("demo", "/docs").expect("directory path"),
&loonfs_client::CreateDirectoryOptions::default(),
)
.await
.expect("create directory");
let stat_json = |encoded_path: &str| {
let response = raw_agent()
.get(&format!(
"{}/v0/namespaces/demo/filesystem/stat?path={encoded_path}",
harness.server_url
))
.set("authorization", "Bearer test-token")
.call()
.expect("stat request");
serde_json::from_reader::<_, serde_json::Value>(response.into_reader()).expect("stat JSON")
};
let root = stat_json("%2F");
assert!(root.get("display_name").is_none());
let child = stat_json("%2Fdocs");
assert_eq!(child["display_name"], "docs");
harness.server.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn http_put_no_replace_and_copy_preserve_cli_semantics() {
let temp_dir = tempdir().expect("tempdir");
let harness = start_server(test_config(
temp_dir.path().join("store"),
"loonfs-server-test",
"http-copy-smoke",
))
.await;
harness
.client
.create_namespace(&namespace_id("demo"))
.await
.expect("create namespace");
let source = NamespacePath::parse("demo", "/docs/hello.txt").expect("source");
harness
.client
.put_file_bytes(&source, b"hello over http\n", &PutFileOptions::default())
.await
.expect("initial create");
match harness
.client
.put_file_bytes(&source, b"conflict\n", &PutFileOptions::default())
.await
{
Err(ClientError::Api { code, .. }) => assert_eq!(code, "path_conflict"),
other => unreachable!("expected path_conflict, got {other:?}"),
}
harness
.client
.put_file_bytes(&source, b"forced overwrite\n", &replace_file_options())
.await
.expect("forced overwrite");
let destination = NamespacePath::parse("demo", "/docs/copy.txt").expect("destination");
harness
.client
.copy_path(
&source,
&destination,
&CopyOptions {
behavior: DestinationBehavior::NoReplace,
commit_id: None,
message: None,
},
)
.await
.expect("copy path");
let source_entry = harness
.client
.stat_path(&source)
.await
.expect("source stat");
let dest_entry = harness
.client
.stat_path(&destination)
.await
.expect("dest stat");
assert_ne!(source_entry.inode_id, dest_entry.inode_id);
assert_eq!(source_entry.content_ref, dest_entry.content_ref);
let dest_bytes = harness
.client
.get_file_bytes(&destination)
.await
.expect("read copied file");
assert_eq!(dest_bytes, b"forced overwrite\n");
harness.server.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn http_name_collision_reports_readable_error_message() {
let temp_dir = tempdir().expect("tempdir");
let harness = start_server(test_config(
temp_dir.path().join("store"),
"loonfs-server-collision-message",
"http-collision-message",
))
.await;
let namespace = namespace_id("demo");
harness
.client
.create_namespace(&namespace)
.await
.expect("create namespace");
harness
.client
.put_file_bytes(
&NamespacePath::parse("demo", "/taken.txt").expect("target"),
b"taken bytes\n",
&PutFileOptions::default(),
)
.await
.expect("create file");
match harness
.client
.put_file_bytes(
&NamespacePath::parse("demo", "/TAKEN.txt").expect("colliding target"),
b"taken bytes\n",
&PutFileOptions::default(),
)
.await
{
Err(ClientError::Api {
status,
code,
message,
..
}) => {
assert_eq!(status, 409);
assert_eq!(code, "path_conflict");
assert!(
message.contains("destination already exists at `/TAKEN.txt`")
&& message.contains("stored as `taken.txt`"),
"expected readable collision message, got {message:?}"
);
assert!(
!message.contains("InodeId(") && !message.contains('{'),
"expected no Debug formatting in message, got {message:?}"
);
}
other => unreachable!("expected path_conflict, got {other:?}"),
}
harness.server.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn http_delete_path_behavior_controls_recursive_delete() {
let temp_dir = tempdir().expect("tempdir");
let harness = start_server(test_config(
temp_dir.path().join("store"),
"loonfs-server-delete-behavior",
"http-delete-behavior",
))
.await;
harness
.client
.create_namespace(&namespace_id("demo"))
.await
.expect("create namespace");
let child = NamespacePath::parse("demo", "/docs/child.txt").expect("child path");
harness
.client
.put_file_bytes(&child, b"child", &replace_file_options())
.await
.expect("write child");
let dir = NamespacePath::parse("demo", "/docs").expect("dir path");
let non_recursive = harness
.client
.delete_path(&dir, &DeleteOptions::default())
.await
.expect_err("non-recursive delete rejects non-empty dir");
match non_recursive {
ClientError::Api { status, code, .. } => {
assert_eq!(status, 409);
assert_eq!(code, "directory_not_empty");
}
other => unreachable!("expected directory_not_empty, got {other:?}"),
}
harness
.client
.delete_path(
&dir,
&DeleteOptions {
behavior: DeleteDirectoryBehavior::Recursive,
..DeleteOptions::default()
},
)
.await
.expect("recursive delete succeeds");
match harness.client.stat_path(&child).await {
Err(ClientError::Api { code, .. }) => assert_eq!(code, "path_not_found"),
other => unreachable!("expected path_not_found after recursive delete, got {other:?}"),
}
harness.server.abort();
}