mirror of
https://github.com/rustfs/rustfs.git
synced 2026-03-17 14:24:08 +00:00
Co-authored-by: houseme <housemecn@gmail.com> Co-authored-by: Copilot <noreply@github.com>
486 lines
15 KiB
Rust
486 lines
15 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! Comprehensive tests for container listing and symlink features
|
|
//!
|
|
//! Tests cover:
|
|
//! - Container listing with prefix filter
|
|
//! - Container listing with delimiter (subdirectories)
|
|
//! - Container listing with marker/end_marker (pagination)
|
|
//! - Container listing with limit
|
|
//! - Symlink creation and validation
|
|
//! - Symlink GET/HEAD following
|
|
//! - Symlink target resolution
|
|
//! - Symlink loop detection
|
|
|
|
#![cfg(feature = "swift")]
|
|
|
|
use rustfs_protocols::swift::symlink::*;
|
|
use std::collections::HashMap;
|
|
|
|
/// Test symlink target validation
|
|
#[test]
|
|
fn test_is_symlink() {
|
|
// Valid symlink metadata with correct header
|
|
let mut metadata = HashMap::new();
|
|
metadata.insert("x-object-symlink-target".to_string(), "container/object".to_string());
|
|
assert!(is_symlink(&metadata));
|
|
|
|
// No symlink metadata
|
|
let metadata2 = HashMap::new();
|
|
assert!(!is_symlink(&metadata2));
|
|
|
|
// Regular object metadata
|
|
let mut metadata3 = HashMap::new();
|
|
metadata3.insert("content-type".to_string(), "text/plain".to_string());
|
|
assert!(!is_symlink(&metadata3));
|
|
}
|
|
|
|
/// Test symlink target extraction
|
|
#[test]
|
|
fn test_get_symlink_target() {
|
|
// Valid symlink target
|
|
let mut metadata = HashMap::new();
|
|
metadata.insert("x-object-symlink-target".to_string(), "photos/cat.jpg".to_string());
|
|
|
|
let target = get_symlink_target(&metadata).unwrap();
|
|
assert!(target.is_some());
|
|
|
|
let target = target.unwrap();
|
|
assert_eq!(target.container, Some("photos".to_string()));
|
|
assert_eq!(target.object, "cat.jpg");
|
|
|
|
// Same container target
|
|
let mut metadata2 = HashMap::new();
|
|
metadata2.insert("x-object-symlink-target".to_string(), "report.pdf".to_string());
|
|
|
|
let target2 = get_symlink_target(&metadata2).unwrap();
|
|
assert!(target2.is_some());
|
|
|
|
let target2 = target2.unwrap();
|
|
assert_eq!(target2.container, None);
|
|
assert_eq!(target2.object, "report.pdf");
|
|
|
|
// No symlink metadata
|
|
let metadata3 = HashMap::new();
|
|
let target3 = get_symlink_target(&metadata3).unwrap();
|
|
assert_eq!(target3, None);
|
|
}
|
|
|
|
/// Test symlink target parsing
|
|
#[test]
|
|
fn test_parse_symlink_target() {
|
|
use rustfs_protocols::swift::symlink::SymlinkTarget;
|
|
|
|
// Standard format: container/object
|
|
let target = SymlinkTarget::parse("photos/cat.jpg").unwrap();
|
|
assert_eq!(target.container, Some("photos".to_string()));
|
|
assert_eq!(target.object, "cat.jpg");
|
|
|
|
// Nested object path
|
|
let target2 = SymlinkTarget::parse("docs/2024/reports/summary.pdf").unwrap();
|
|
assert_eq!(target2.container, Some("docs".to_string()));
|
|
assert_eq!(target2.object, "2024/reports/summary.pdf");
|
|
|
|
// Single slash
|
|
let target3 = SymlinkTarget::parse("container/object").unwrap();
|
|
assert_eq!(target3.container, Some("container".to_string()));
|
|
assert_eq!(target3.object, "object");
|
|
|
|
// Same container (no slash)
|
|
let target4 = SymlinkTarget::parse("object.txt").unwrap();
|
|
assert_eq!(target4.container, None);
|
|
assert_eq!(target4.object, "object.txt");
|
|
}
|
|
|
|
/// Test invalid symlink targets
|
|
#[test]
|
|
fn test_parse_symlink_target_invalid() {
|
|
use rustfs_protocols::swift::symlink::SymlinkTarget;
|
|
|
|
// Empty string
|
|
let result = SymlinkTarget::parse("");
|
|
assert!(result.is_err());
|
|
|
|
// Only slash (empty container and object)
|
|
let result2 = SymlinkTarget::parse("/");
|
|
assert!(result2.is_err());
|
|
|
|
// Empty container
|
|
let result3 = SymlinkTarget::parse("/object");
|
|
assert!(result3.is_err());
|
|
|
|
// Empty object
|
|
let result4 = SymlinkTarget::parse("container/");
|
|
assert!(result4.is_err());
|
|
}
|
|
|
|
/// Test symlink metadata format
|
|
#[test]
|
|
fn test_symlink_metadata_format() {
|
|
let mut metadata = HashMap::new();
|
|
metadata.insert("x-object-symlink-target".to_string(), "photos/cat.jpg".to_string());
|
|
metadata.insert("content-type".to_string(), "application/symlink".to_string());
|
|
|
|
assert!(is_symlink(&metadata));
|
|
|
|
let target = get_symlink_target(&metadata).unwrap().unwrap();
|
|
assert_eq!(target.container, Some("photos".to_string()));
|
|
assert_eq!(target.object, "cat.jpg");
|
|
|
|
// Content-Type should indicate symlink
|
|
assert_eq!(metadata.get("content-type").unwrap(), "application/symlink");
|
|
}
|
|
|
|
/// Test symlink with empty target
|
|
#[test]
|
|
fn test_symlink_empty_target() {
|
|
use rustfs_protocols::swift::symlink::SymlinkTarget;
|
|
|
|
let mut metadata = HashMap::new();
|
|
metadata.insert("x-object-symlink-target".to_string(), String::new());
|
|
|
|
// Empty target should be invalid when parsed
|
|
let result = SymlinkTarget::parse("");
|
|
assert!(result.is_err());
|
|
|
|
// Also check that is_symlink returns true (header exists)
|
|
// but parsing will fail
|
|
assert!(is_symlink(&metadata));
|
|
let target_result = get_symlink_target(&metadata);
|
|
assert!(target_result.is_err());
|
|
}
|
|
|
|
/// Test symlink target with special characters
|
|
#[test]
|
|
fn test_symlink_target_special_chars() {
|
|
use rustfs_protocols::swift::symlink::SymlinkTarget;
|
|
|
|
let test_cases = vec![
|
|
("container/file with spaces.txt", "container", "file with spaces.txt"),
|
|
("container/file-with-dashes.txt", "container", "file-with-dashes.txt"),
|
|
("container/file_with_underscores.txt", "container", "file_with_underscores.txt"),
|
|
("photos/2024/january/cat.jpg", "photos", "2024/january/cat.jpg"),
|
|
];
|
|
|
|
for (target_str, expected_container, expected_object) in test_cases {
|
|
let target = SymlinkTarget::parse(target_str).unwrap();
|
|
|
|
assert_eq!(target.container, Some(expected_container.to_string()));
|
|
assert_eq!(target.object, expected_object);
|
|
}
|
|
|
|
// Same container (no slash)
|
|
let target = SymlinkTarget::parse("file.txt").unwrap();
|
|
assert_eq!(target.container, None);
|
|
assert_eq!(target.object, "file.txt");
|
|
}
|
|
|
|
/// Test symlink loop detection structure
|
|
#[test]
|
|
fn test_symlink_loop_detection() {
|
|
// Test data structure for loop detection
|
|
let mut visited = std::collections::HashSet::new();
|
|
|
|
// Visit chain of symlinks
|
|
let chain = vec!["link1", "link2", "link3"];
|
|
|
|
for link in &chain {
|
|
assert!(!visited.contains(link));
|
|
visited.insert(*link);
|
|
}
|
|
|
|
// Try to revisit - should detect loop
|
|
assert!(visited.contains(&"link1"));
|
|
}
|
|
|
|
/// Test maximum symlink depth
|
|
#[test]
|
|
fn test_symlink_max_depth() {
|
|
use rustfs_protocols::swift::symlink::validate_symlink_depth;
|
|
|
|
const MAX_SYMLINK_DEPTH: u8 = 5;
|
|
|
|
// Depths 0-4 should be valid
|
|
for depth in 0..MAX_SYMLINK_DEPTH {
|
|
assert!(validate_symlink_depth(depth).is_ok());
|
|
}
|
|
|
|
// Depth 5 and above should fail
|
|
assert!(validate_symlink_depth(MAX_SYMLINK_DEPTH).is_err());
|
|
assert!(validate_symlink_depth(MAX_SYMLINK_DEPTH + 1).is_err());
|
|
}
|
|
|
|
/// Test symlink with query parameters in target
|
|
#[test]
|
|
fn test_symlink_target_query_params() {
|
|
use rustfs_protocols::swift::symlink::SymlinkTarget;
|
|
|
|
// Symlink targets should not include query parameters
|
|
// (those are part of the request, not the target)
|
|
|
|
let target = SymlinkTarget::parse("container/object").unwrap();
|
|
|
|
assert_eq!(target.container, Some("container".to_string()));
|
|
assert_eq!(target.object, "object");
|
|
|
|
// Query params would be on the request URL, not the target
|
|
}
|
|
|
|
/// Test symlink metadata preservation
|
|
#[test]
|
|
fn test_symlink_metadata_preservation() {
|
|
let mut metadata = HashMap::new();
|
|
metadata.insert("x-object-symlink-target".to_string(), "photos/cat.jpg".to_string());
|
|
metadata.insert("x-object-meta-description".to_string(), "Link to cat photo".to_string());
|
|
metadata.insert("content-type".to_string(), "application/symlink".to_string());
|
|
|
|
// All metadata should be preserved
|
|
assert_eq!(metadata.len(), 3);
|
|
assert!(metadata.contains_key("x-object-symlink-target"));
|
|
assert!(metadata.contains_key("x-object-meta-description"));
|
|
assert!(metadata.contains_key("content-type"));
|
|
}
|
|
|
|
/// Test container listing prefix filter structure
|
|
#[test]
|
|
fn test_listing_prefix_structure() {
|
|
// Test that prefix filtering structure works correctly
|
|
let objects = [
|
|
"photos/2024/cat.jpg",
|
|
"photos/2024/dog.jpg",
|
|
"photos/2023/bird.jpg",
|
|
"documents/report.pdf",
|
|
];
|
|
|
|
// Filter by prefix "photos/2024/"
|
|
let prefix = "photos/2024/";
|
|
let filtered: Vec<_> = objects.iter().filter(|o| o.starts_with(prefix)).collect();
|
|
|
|
assert_eq!(filtered.len(), 2);
|
|
assert!(filtered.contains(&&"photos/2024/cat.jpg"));
|
|
assert!(filtered.contains(&&"photos/2024/dog.jpg"));
|
|
}
|
|
|
|
/// Test container listing delimiter structure
|
|
#[test]
|
|
fn test_listing_delimiter_structure() {
|
|
// Test delimiter-based directory listing
|
|
let objects = vec![
|
|
"photos/2024/cat.jpg",
|
|
"photos/2024/dog.jpg",
|
|
"photos/2023/bird.jpg",
|
|
"photos/README.txt",
|
|
"documents/report.pdf",
|
|
];
|
|
|
|
let delimiter = '/';
|
|
|
|
// Group by first component (before first delimiter)
|
|
let mut directories = std::collections::HashSet::new();
|
|
for obj in &objects {
|
|
if let Some(pos) = obj.find(delimiter) {
|
|
directories.insert(&obj[..=pos]); // Include delimiter
|
|
}
|
|
}
|
|
|
|
assert!(directories.contains("photos/"));
|
|
assert!(directories.contains("documents/"));
|
|
}
|
|
|
|
/// Test container listing with marker (pagination)
|
|
#[test]
|
|
fn test_listing_marker_structure() {
|
|
let objects = ["a.txt", "b.txt", "c.txt", "d.txt", "e.txt"];
|
|
|
|
// List starting after marker "b.txt"
|
|
let marker = "b.txt";
|
|
let filtered: Vec<_> = objects.iter().filter(|o| *o > &marker).collect();
|
|
|
|
assert_eq!(filtered.len(), 3);
|
|
assert_eq!(*filtered[0], "c.txt");
|
|
assert_eq!(*filtered[1], "d.txt");
|
|
assert_eq!(*filtered[2], "e.txt");
|
|
}
|
|
|
|
/// Test container listing with end_marker
|
|
#[test]
|
|
fn test_listing_end_marker_structure() {
|
|
let objects = ["a.txt", "b.txt", "c.txt", "d.txt", "e.txt"];
|
|
|
|
// List up to (but not including) end_marker "d.txt"
|
|
let end_marker = "d.txt";
|
|
let filtered: Vec<_> = objects.iter().filter(|o| *o < &end_marker).collect();
|
|
|
|
assert_eq!(filtered.len(), 3);
|
|
assert_eq!(*filtered[0], "a.txt");
|
|
assert_eq!(*filtered[1], "b.txt");
|
|
assert_eq!(*filtered[2], "c.txt");
|
|
}
|
|
|
|
/// Test container listing with both marker and end_marker
|
|
#[test]
|
|
fn test_listing_marker_and_end_marker() {
|
|
let objects = ["a.txt", "b.txt", "c.txt", "d.txt", "e.txt"];
|
|
|
|
let marker = "b.txt";
|
|
let end_marker = "e.txt";
|
|
|
|
let filtered: Vec<_> = objects.iter().filter(|o| *o > &marker && *o < &end_marker).collect();
|
|
|
|
assert_eq!(filtered.len(), 2);
|
|
assert_eq!(*filtered[0], "c.txt");
|
|
assert_eq!(*filtered[1], "d.txt");
|
|
}
|
|
|
|
/// Test container listing with limit
|
|
#[test]
|
|
fn test_listing_limit_structure() {
|
|
let objects = ["a.txt", "b.txt", "c.txt", "d.txt", "e.txt"];
|
|
|
|
let limit = 3;
|
|
let limited: Vec<_> = objects.iter().take(limit).collect();
|
|
|
|
assert_eq!(limited.len(), 3);
|
|
assert_eq!(*limited[0], "a.txt");
|
|
assert_eq!(*limited[1], "b.txt");
|
|
assert_eq!(*limited[2], "c.txt");
|
|
}
|
|
|
|
/// Test container listing with prefix and limit
|
|
#[test]
|
|
fn test_listing_prefix_and_limit() {
|
|
let objects = [
|
|
"photos/a.jpg",
|
|
"photos/b.jpg",
|
|
"photos/c.jpg",
|
|
"photos/d.jpg",
|
|
"documents/x.pdf",
|
|
];
|
|
|
|
let prefix = "photos/";
|
|
let limit = 2;
|
|
|
|
let filtered: Vec<_> = objects.iter().filter(|o| o.starts_with(prefix)).take(limit).collect();
|
|
|
|
assert_eq!(filtered.len(), 2);
|
|
assert_eq!(*filtered[0], "photos/a.jpg");
|
|
assert_eq!(*filtered[1], "photos/b.jpg");
|
|
}
|
|
|
|
/// Test container listing with delimiter and prefix
|
|
#[test]
|
|
fn test_listing_delimiter_and_prefix() {
|
|
let objects = [
|
|
"photos/2024/cat.jpg",
|
|
"photos/2024/dog.jpg",
|
|
"photos/2023/bird.jpg",
|
|
"documents/report.pdf",
|
|
];
|
|
|
|
let prefix = "photos/";
|
|
let delimiter = '/';
|
|
|
|
// Filter by prefix first
|
|
let with_prefix: Vec<_> = objects.iter().filter(|o| o.starts_with(prefix)).collect();
|
|
|
|
// Then group by next delimiter
|
|
let mut subdirs = std::collections::HashSet::new();
|
|
for obj in with_prefix {
|
|
let after_prefix = &obj[prefix.len()..];
|
|
if let Some(pos) = after_prefix.find(delimiter) {
|
|
subdirs.insert(&after_prefix[..=pos]);
|
|
}
|
|
}
|
|
|
|
assert!(subdirs.contains("2024/"));
|
|
assert!(subdirs.contains("2023/"));
|
|
}
|
|
|
|
/// Test symlink cross-container references
|
|
#[test]
|
|
fn test_symlink_cross_container() {
|
|
use rustfs_protocols::swift::symlink::SymlinkTarget;
|
|
|
|
// Symlinks can reference objects in different containers
|
|
let target = SymlinkTarget::parse("other-container/object.txt").unwrap();
|
|
|
|
assert_eq!(target.container, Some("other-container".to_string()));
|
|
assert_eq!(target.object, "object.txt");
|
|
}
|
|
|
|
/// Test symlink to nested object
|
|
#[test]
|
|
fn test_symlink_to_nested_object() {
|
|
use rustfs_protocols::swift::symlink::SymlinkTarget;
|
|
|
|
let target = SymlinkTarget::parse("container/folder1/folder2/file.txt").unwrap();
|
|
|
|
assert_eq!(target.container, Some("container".to_string()));
|
|
assert_eq!(target.object, "folder1/folder2/file.txt");
|
|
}
|
|
|
|
/// Test listing empty container
|
|
#[test]
|
|
fn test_listing_empty_container() {
|
|
let objects: Vec<&str> = vec![];
|
|
|
|
let filtered: Vec<_> = objects.iter().collect();
|
|
assert_eq!(filtered.len(), 0);
|
|
|
|
// With prefix
|
|
let with_prefix: Vec<_> = objects.iter().filter(|o| o.starts_with("prefix/")).collect();
|
|
assert_eq!(with_prefix.len(), 0);
|
|
}
|
|
|
|
/// Test listing lexicographic ordering
|
|
#[test]
|
|
fn test_listing_lexicographic_order() {
|
|
let mut objects = ["z.txt", "a.txt", "m.txt", "b.txt"];
|
|
objects.sort();
|
|
|
|
assert_eq!(objects[0], "a.txt");
|
|
assert_eq!(objects[1], "b.txt");
|
|
assert_eq!(objects[2], "m.txt");
|
|
assert_eq!(objects[3], "z.txt");
|
|
}
|
|
|
|
/// Test listing with numeric-like names
|
|
#[test]
|
|
fn test_listing_numeric_names() {
|
|
let mut objects = ["file10.txt", "file2.txt", "file1.txt", "file20.txt"];
|
|
objects.sort();
|
|
|
|
// Lexicographic sort, not numeric
|
|
assert_eq!(objects[0], "file1.txt");
|
|
assert_eq!(objects[1], "file10.txt");
|
|
assert_eq!(objects[2], "file2.txt");
|
|
assert_eq!(objects[3], "file20.txt");
|
|
}
|
|
|
|
/// Test symlink with absolute path target
|
|
#[test]
|
|
fn test_symlink_absolute_path() {
|
|
// Swift symlinks typically use relative paths, but test absolute format
|
|
let target = "/v1/AUTH_account/container/object";
|
|
|
|
// Parse should handle leading slashes
|
|
// (Implementation-dependent - may strip leading slash)
|
|
if target.starts_with('/') {
|
|
let stripped = target.trim_start_matches('/');
|
|
// Should still be parseable after stripping
|
|
assert!(stripped.contains('/'));
|
|
}
|
|
}
|