#![cfg(feature = "storage")]
use bsv_rs::primitives::hash::sha256;
use bsv_rs::storage::{
get_hash_from_url, get_hash_hex_from_url, get_url_for_file, get_url_for_hash, is_valid_url,
normalize_url, DownloadResult, StorageDownloader, StorageDownloaderConfig, StorageUploader,
StorageUploaderConfig, UploadFileResult, UploadableFile, UHRP_PREFIX, WEB_UHRP_PREFIX,
};
const TS_EXAMPLE_HASH_HEX: &str =
"1a5ec49a3f32cd56d19732e89bde5d81755ddc0fd8515dc8b226d47654139dca";
const TS_EXAMPLE_FILE_HEX: &str =
"687da27f04a112aa48f1cab2e7949f1eea4f7ba28319c1e999910cd561a634a05a3516e6db";
const TS_EXAMPLE_URL_BASE58: &str = "XUT6PqWb3GP3LR7dmBMCJwZ3oo5g1iGCF3CrpzyuJCemkGu1WGoq";
const TS_BAD_CHECKSUM_URL: &str = "XUU7cTfy6fA6q2neLDmzPqJnGB6o18PXKoGaWLPrH1SeWLKgdCKq";
#[test]
fn test_get_url_for_file_known_data() {
let file = hex::decode(TS_EXAMPLE_FILE_HEX).unwrap();
let url = get_url_for_file(&file).unwrap();
assert!(url.starts_with(UHRP_PREFIX));
let normalized = normalize_url(&url);
assert_eq!(normalized, TS_EXAMPLE_URL_BASE58);
}
#[test]
fn test_get_url_for_file_empty() {
let url = get_url_for_file(b"").unwrap();
assert!(url.starts_with(UHRP_PREFIX));
let hash = get_hash_from_url(&url).unwrap();
let expected_hash = sha256(b"");
assert_eq!(hash, expected_hash);
assert_eq!(
hex::encode(hash),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn test_get_url_for_file_hello_world() {
let data = b"Hello, World!";
let url = get_url_for_file(data).unwrap();
assert!(url.starts_with(UHRP_PREFIX));
assert!(is_valid_url(&url));
let recovered_hash = get_hash_from_url(&url).unwrap();
let expected_hash = sha256(data);
assert_eq!(recovered_hash, expected_hash);
}
#[test]
fn test_get_url_for_file_binary_data() {
let mut data = Vec::with_capacity(256);
for i in 0..=255u8 {
data.push(i);
}
let url = get_url_for_file(&data).unwrap();
assert!(is_valid_url(&url));
let recovered_hash = get_hash_from_url(&url).unwrap();
assert_eq!(recovered_hash, sha256(&data));
}
#[test]
fn test_get_url_for_hash_known_hash() {
let hash = hex::decode(TS_EXAMPLE_HASH_HEX).unwrap();
let mut hash_array = [0u8; 32];
hash_array.copy_from_slice(&hash);
let url = get_url_for_hash(&hash_array).unwrap();
assert!(url.starts_with(UHRP_PREFIX));
let normalized = normalize_url(&url);
assert_eq!(normalized, TS_EXAMPLE_URL_BASE58);
}
#[test]
fn test_get_url_for_hash_zero_hash() {
let hash = [0u8; 32];
let url = get_url_for_hash(&hash).unwrap();
assert!(url.starts_with(UHRP_PREFIX));
assert!(is_valid_url(&url));
}
#[test]
fn test_get_url_for_hash_max_hash() {
let hash = [0xff; 32];
let url = get_url_for_hash(&hash).unwrap();
assert!(url.starts_with(UHRP_PREFIX));
assert!(is_valid_url(&url));
}
#[test]
fn test_get_url_for_hash_invalid_length() {
let short_hash = vec![0u8; 16];
let result = get_url_for_hash(&short_hash);
assert!(result.is_err());
let long_hash = vec![0u8; 64];
let result = get_url_for_hash(&long_hash);
assert!(result.is_err());
}
#[test]
fn test_get_hash_from_url_extracts_correct_hash() {
let hash = get_hash_from_url(TS_EXAMPLE_URL_BASE58).unwrap();
assert_eq!(hex::encode(hash), TS_EXAMPLE_HASH_HEX);
}
#[test]
fn test_get_hash_from_url_with_uhrp_prefix() {
let url_with_prefix = format!("uhrp://{}", TS_EXAMPLE_URL_BASE58);
let hash = get_hash_from_url(&url_with_prefix).unwrap();
assert_eq!(hex::encode(hash), TS_EXAMPLE_HASH_HEX);
}
#[test]
fn test_get_hash_from_url_with_web_prefix() {
let url_with_prefix = format!("web+uhrp://{}", TS_EXAMPLE_URL_BASE58);
let hash = get_hash_from_url(&url_with_prefix).unwrap();
assert_eq!(hex::encode(hash), TS_EXAMPLE_HASH_HEX);
}
#[test]
fn test_get_hash_from_url_invalid_checksum() {
let result = get_hash_from_url(TS_BAD_CHECKSUM_URL);
assert!(result.is_err());
}
#[test]
fn test_get_hash_from_url_too_short() {
let result = get_hash_from_url("SomeBase58CheckTooShortOrTooLong");
assert!(result.is_err());
}
#[test]
fn test_get_hash_from_url_invalid_base58() {
let result = get_hash_from_url("0InvalidBase58");
assert!(result.is_err());
let result = get_hash_from_url("OInvalidBase58");
assert!(result.is_err());
let result = get_hash_from_url("lInvalidBase58");
assert!(result.is_err());
let result = get_hash_from_url("IInvalidBase58");
assert!(result.is_err());
}
#[test]
fn test_get_hash_hex_from_url_correct_hex() {
let hash_hex = get_hash_hex_from_url(TS_EXAMPLE_URL_BASE58).unwrap();
assert_eq!(hash_hex.len(), 64); assert_eq!(hash_hex, TS_EXAMPLE_HASH_HEX);
}
#[test]
fn test_get_hash_hex_from_url_with_prefix() {
let url = format!("uhrp://{}", TS_EXAMPLE_URL_BASE58);
let hash_hex = get_hash_hex_from_url(&url).unwrap();
assert_eq!(hash_hex, TS_EXAMPLE_HASH_HEX);
}
#[test]
fn test_get_hash_hex_from_url_invalid_returns_error() {
let result = get_hash_hex_from_url(TS_BAD_CHECKSUM_URL);
assert!(result.is_err());
}
#[test]
fn test_is_valid_url_accepts_valid_urls() {
assert!(is_valid_url(TS_EXAMPLE_URL_BASE58));
assert!(is_valid_url(&format!("uhrp://{}", TS_EXAMPLE_URL_BASE58)));
assert!(is_valid_url(&format!(
"web+uhrp://{}",
TS_EXAMPLE_URL_BASE58
)));
}
#[test]
fn test_is_valid_url_rejects_invalid_checksum() {
assert!(!is_valid_url(TS_BAD_CHECKSUM_URL));
assert!(!is_valid_url(&format!("uhrp://{}", TS_BAD_CHECKSUM_URL)));
}
#[test]
fn test_is_valid_url_rejects_non_uhrp_urls() {
assert!(!is_valid_url("https://example.com"));
assert!(!is_valid_url("http://localhost:8080"));
assert!(!is_valid_url("file:///path/to/file"));
assert!(!is_valid_url("data:text/plain;base64,SGVsbG8="));
}
#[test]
fn test_is_valid_url_rejects_too_short() {
assert!(!is_valid_url("uhrp://tooshort"));
assert!(!is_valid_url("invalid"));
assert!(!is_valid_url(""));
}
#[test]
fn test_is_valid_url_rejects_invalid_prefix() {
assert!(!is_valid_url("AInvalidPrefixTestString1"));
assert!(!is_valid_url("AInvalidPrefixTestString2"));
assert!(!is_valid_url("AnotherInvalidPrefixTestString"));
assert!(!is_valid_url("YetAnotherInvalidPrefixTestString"));
}
#[test]
fn test_normalize_url_removes_uhrp_prefix() {
let url = "uhrp://abc123";
assert_eq!(normalize_url(url), "abc123");
}
#[test]
fn test_normalize_url_removes_web_uhrp_prefix() {
let url = "web+uhrp://abc123";
assert_eq!(normalize_url(url), "abc123");
}
#[test]
fn test_normalize_url_preserves_raw_base58() {
let url = "abc123";
assert_eq!(normalize_url(url), "abc123");
}
#[test]
fn test_normalize_url_case_insensitive() {
assert_eq!(normalize_url("UHRP://ABC123"), "ABC123");
assert_eq!(normalize_url("WEB+UHRP://ABC123"), "ABC123");
assert_eq!(normalize_url("Uhrp://XyZ"), "XyZ");
}
#[test]
fn test_normalize_url_handles_uhrp_without_double_slash() {
let url = "uhrp:abc123";
assert_eq!(normalize_url(url), "abc123");
}
#[test]
fn test_normalize_url_handles_various_formats() {
let url = format!("uhrp://{}", TS_EXAMPLE_URL_BASE58);
assert_eq!(normalize_url(&url), TS_EXAMPLE_URL_BASE58);
let url = format!("web+uhrp://{}", TS_EXAMPLE_URL_BASE58);
assert_eq!(normalize_url(&url), TS_EXAMPLE_URL_BASE58);
assert_eq!(normalize_url(TS_EXAMPLE_URL_BASE58), TS_EXAMPLE_URL_BASE58);
}
#[test]
fn test_roundtrip_file_to_url_to_hash() {
let file = hex::decode(TS_EXAMPLE_FILE_HEX).unwrap();
let url = get_url_for_file(&file).unwrap();
let hash = get_hash_from_url(&url).unwrap();
assert_eq!(hash, sha256(&file));
assert_eq!(hex::encode(hash), TS_EXAMPLE_HASH_HEX);
}
#[test]
fn test_roundtrip_hash_to_url_to_hash() {
let original_hash = hex::decode(TS_EXAMPLE_HASH_HEX).unwrap();
let mut hash_array = [0u8; 32];
hash_array.copy_from_slice(&original_hash);
let url = get_url_for_hash(&hash_array).unwrap();
let recovered_hash = get_hash_from_url(&url).unwrap();
assert_eq!(recovered_hash, hash_array);
}
#[test]
fn test_roundtrip_various_files() {
let zeros_1000 = [0u8; 1000];
let ones_1000 = vec![0xffu8; 1000];
let test_cases: Vec<&[u8]> = vec![
b"",
b"a",
b"hello",
b"Hello, World!",
b"The quick brown fox jumps over the lazy dog",
&zeros_1000,
&ones_1000,
];
for (i, data) in test_cases.iter().enumerate() {
let url = get_url_for_file(data).unwrap();
let hash = get_hash_from_url(&url).unwrap();
let expected = sha256(data);
assert_eq!(
hash, expected,
"Round-trip failed for test case {}: {:?}",
i, data
);
}
}
#[test]
fn test_roundtrip_with_different_prefixes() {
let file = b"test data for prefix handling";
let url_base = get_url_for_file(file).unwrap();
let hash_base = get_hash_from_url(&url_base).unwrap();
let url_uhrp = format!("uhrp://{}", normalize_url(&url_base));
let hash_uhrp = get_hash_from_url(&url_uhrp).unwrap();
assert_eq!(hash_base, hash_uhrp);
let url_web = format!("web+uhrp://{}", normalize_url(&url_base));
let hash_web = get_hash_from_url(&url_web).unwrap();
assert_eq!(hash_base, hash_web);
let url_raw = normalize_url(&url_base);
let hash_raw = get_hash_from_url(&url_raw).unwrap();
assert_eq!(hash_base, hash_raw);
}
#[test]
fn test_cross_sdk_hash_to_url() {
let hash = hex::decode(TS_EXAMPLE_HASH_HEX).unwrap();
let mut hash_array = [0u8; 32];
hash_array.copy_from_slice(&hash);
let url = get_url_for_hash(&hash_array).unwrap();
let normalized = normalize_url(&url);
assert_eq!(
normalized, TS_EXAMPLE_URL_BASE58,
"URL mismatch with TypeScript SDK"
);
}
#[test]
fn test_cross_sdk_file_to_url() {
let file = hex::decode(TS_EXAMPLE_FILE_HEX).unwrap();
let url = get_url_for_file(&file).unwrap();
let normalized = normalize_url(&url);
assert_eq!(
normalized, TS_EXAMPLE_URL_BASE58,
"URL mismatch with TypeScript SDK"
);
}
#[test]
fn test_cross_sdk_url_to_hash() {
let hash = get_hash_from_url(TS_EXAMPLE_URL_BASE58).unwrap();
assert_eq!(
hex::encode(hash),
TS_EXAMPLE_HASH_HEX,
"Hash mismatch with TypeScript SDK"
);
}
#[test]
fn test_cross_sdk_file_hash_verification() {
let file = hex::decode(TS_EXAMPLE_FILE_HEX).unwrap();
let computed_hash = sha256(&file);
assert_eq!(
hex::encode(computed_hash),
TS_EXAMPLE_HASH_HEX,
"SHA256 hash mismatch"
);
}
#[test]
fn test_cross_sdk_is_valid_url() {
assert!(is_valid_url(TS_EXAMPLE_URL_BASE58));
assert!(is_valid_url(&format!("uhrp://{}", TS_EXAMPLE_URL_BASE58)));
assert!(is_valid_url(&format!(
"web+uhrp://{}",
TS_EXAMPLE_URL_BASE58
)));
assert!(!is_valid_url(TS_BAD_CHECKSUM_URL));
}
#[test]
fn test_cross_sdk_empty_file_hash() {
let empty_hash = sha256(b"");
assert_eq!(
hex::encode(empty_hash),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
let url = get_url_for_hash(&empty_hash).unwrap();
let recovered = get_hash_from_url(&url).unwrap();
assert_eq!(recovered, empty_hash);
}
#[test]
fn test_storage_downloader_config_default() {
let config = StorageDownloaderConfig::default();
assert_eq!(
config.network_preset,
bsv_rs::overlay::NetworkPreset::Mainnet
);
assert!(config.resolver.is_none());
assert_eq!(config.timeout_ms, Some(30000));
}
#[test]
fn test_storage_downloader_config_custom_timeout() {
let config = StorageDownloaderConfig {
timeout_ms: Some(60000),
..Default::default()
};
assert_eq!(config.timeout_ms, Some(60000));
}
#[test]
fn test_storage_downloader_config_no_timeout() {
let config = StorageDownloaderConfig {
timeout_ms: None,
..Default::default()
};
assert!(config.timeout_ms.is_none());
}
#[test]
fn test_storage_downloader_config_testnet() {
let config = StorageDownloaderConfig {
network_preset: bsv_rs::overlay::NetworkPreset::Testnet,
resolver: None,
timeout_ms: Some(30000),
};
assert_eq!(
config.network_preset,
bsv_rs::overlay::NetworkPreset::Testnet
);
}
#[test]
fn test_storage_downloader_config_local() {
let config = StorageDownloaderConfig {
network_preset: bsv_rs::overlay::NetworkPreset::Local,
resolver: None,
timeout_ms: Some(5000),
};
assert_eq!(config.network_preset, bsv_rs::overlay::NetworkPreset::Local);
}
#[test]
fn test_storage_downloader_creation() {
let downloader = StorageDownloader::default();
assert!(std::mem::size_of_val(&downloader) > 0);
}
#[test]
fn test_storage_downloader_creation_with_config() {
let config = StorageDownloaderConfig {
network_preset: bsv_rs::overlay::NetworkPreset::Testnet,
resolver: None,
timeout_ms: Some(15000),
};
let downloader = StorageDownloader::new(config);
assert!(std::mem::size_of_val(&downloader) > 0);
}
#[test]
fn test_storage_uploader_config_new() {
let config = StorageUploaderConfig::new("https://storage.example.com");
assert_eq!(config.storage_url, "https://storage.example.com");
assert_eq!(config.default_retention_minutes, 7 * 24 * 60); }
#[test]
fn test_storage_uploader_config_with_retention() {
let config =
StorageUploaderConfig::new("https://storage.example.com").with_retention_minutes(1440);
assert_eq!(config.default_retention_minutes, 1440); }
#[test]
fn test_storage_uploader_config_with_zero_retention() {
let config =
StorageUploaderConfig::new("https://storage.example.com").with_retention_minutes(0);
assert_eq!(config.default_retention_minutes, 0);
}
#[test]
fn test_storage_uploader_config_with_large_retention() {
let config = StorageUploaderConfig::new("https://storage.example.com")
.with_retention_minutes(365 * 24 * 60); assert_eq!(config.default_retention_minutes, 365 * 24 * 60);
}
#[test]
fn test_storage_uploader_creation() {
let config = StorageUploaderConfig::new("https://storage.example.com");
let uploader = StorageUploader::new(config);
assert_eq!(uploader.base_url(), "https://storage.example.com");
}
#[test]
fn test_storage_uploader_base_url() {
let config = StorageUploaderConfig::new("https://my-storage.test");
let uploader = StorageUploader::new(config);
assert_eq!(uploader.base_url(), "https://my-storage.test");
}
#[test]
fn test_storage_uploader_different_urls() {
let urls = [
"https://storage1.example.com",
"https://storage2.example.com:8080",
"http://localhost:3000",
"https://storage.test/api/v1",
];
for url in urls {
let config = StorageUploaderConfig::new(url);
let uploader = StorageUploader::new(config);
assert_eq!(uploader.base_url(), url);
}
}
#[test]
fn test_uploadable_file_creation() {
let file = UploadableFile::new(b"Hello, World!".to_vec(), "text/plain");
assert_eq!(file.data, b"Hello, World!");
assert_eq!(file.mime_type, "text/plain");
assert_eq!(file.size(), 13);
}
#[test]
fn test_uploadable_file_empty() {
let file = UploadableFile::new(Vec::new(), "application/octet-stream");
assert!(file.data.is_empty());
assert_eq!(file.size(), 0);
}
#[test]
fn test_uploadable_file_binary() {
let binary_data: Vec<u8> = (0..=255).collect();
let file = UploadableFile::new(binary_data.clone(), "application/octet-stream");
assert_eq!(file.data, binary_data);
assert_eq!(file.size(), 256);
}
#[test]
fn test_uploadable_file_various_mime_types() {
let mime_types = [
"text/plain",
"text/html",
"application/json",
"application/octet-stream",
"image/png",
"image/jpeg",
"application/pdf",
"video/mp4",
];
for mime in mime_types {
let file = UploadableFile::new(b"test".to_vec(), mime);
assert_eq!(file.mime_type, mime);
}
}
#[test]
fn test_uploadable_file_large() {
let large_data = vec![0xABu8; 1024 * 1024];
let file = UploadableFile::new(large_data.clone(), "application/octet-stream");
assert_eq!(file.size(), 1024 * 1024);
assert_eq!(file.data, large_data);
}
#[test]
fn test_download_result_creation() {
let result = DownloadResult::new(vec![1, 2, 3], "application/octet-stream");
assert_eq!(result.data, vec![1, 2, 3]);
assert_eq!(result.mime_type, "application/octet-stream");
}
#[test]
fn test_download_result_empty() {
let result = DownloadResult::new(Vec::new(), "text/plain");
assert!(result.data.is_empty());
assert_eq!(result.mime_type, "text/plain");
}
#[test]
fn test_download_result_with_string() {
let content = b"Hello, World!".to_vec();
let result = DownloadResult::new(content.clone(), String::from("text/plain; charset=utf-8"));
assert_eq!(result.data, content);
assert_eq!(result.mime_type, "text/plain; charset=utf-8");
}
#[test]
fn test_upload_file_result_success() {
let result = UploadFileResult::new("uhrp://abc123", true);
assert_eq!(result.uhrp_url, "uhrp://abc123");
assert!(result.published);
}
#[test]
fn test_upload_file_result_failure() {
let result = UploadFileResult::new("", false);
assert_eq!(result.uhrp_url, "");
assert!(!result.published);
}
#[test]
fn test_upload_file_result_with_real_url() {
let file_data = b"test file for upload result";
let url = get_url_for_file(file_data).unwrap();
let result = UploadFileResult::new(url.clone(), true);
assert_eq!(result.uhrp_url, url);
assert!(result.published);
assert!(is_valid_url(&result.uhrp_url));
}
#[test]
fn test_uhrp_prefix_constant() {
assert_eq!(UHRP_PREFIX, "uhrp://");
}
#[test]
fn test_web_uhrp_prefix_constant() {
assert_eq!(WEB_UHRP_PREFIX, "web+uhrp://");
}
#[test]
fn test_url_generation_deterministic() {
let data = b"deterministic test data";
let url1 = get_url_for_file(data).unwrap();
let url2 = get_url_for_file(data).unwrap();
assert_eq!(url1, url2);
}
#[test]
fn test_different_data_different_urls() {
let url1 = get_url_for_file(b"data A").unwrap();
let url2 = get_url_for_file(b"data B").unwrap();
assert_ne!(url1, url2);
}
#[test]
fn test_similar_data_different_urls() {
let url1 = get_url_for_file(b"Hello, World!").unwrap();
let url2 = get_url_for_file(b"Hello, World?").unwrap();
let url3 = get_url_for_file(b"hello, World!").unwrap();
assert_ne!(url1, url2);
assert_ne!(url1, url3);
assert_ne!(url2, url3);
}
#[test]
fn test_special_characters_in_data() {
let test_data = [
"Hello\nWorld".as_bytes(),
"Tab\tSeparated".as_bytes(),
"Carriage\rReturn".as_bytes(),
"Null\0Byte".as_bytes(),
"Unicode: ".as_bytes(),
];
for data in test_data {
let url = get_url_for_file(data).unwrap();
assert!(is_valid_url(&url));
let hash = get_hash_from_url(&url).unwrap();
assert_eq!(hash, sha256(data));
}
}
#[test]
fn test_summary() {
println!("Storage module test summary:");
let file = hex::decode(TS_EXAMPLE_FILE_HEX).unwrap();
let url = get_url_for_file(&file).unwrap();
println!(" - Generated URL: {}", url);
assert!(is_valid_url(&url));
println!(" - URL validated: true");
let hash = get_hash_from_url(&url).unwrap();
assert_eq!(hex::encode(hash), TS_EXAMPLE_HASH_HEX);
println!(" - Extracted hash: {}", hex::encode(hash));
assert_eq!(normalize_url(&url), TS_EXAMPLE_URL_BASE58);
println!(" - Cross-SDK compatible: true");
let downloader_config = StorageDownloaderConfig::default();
println!(
" - Downloader network: {:?}",
downloader_config.network_preset
);
let uploader_config = StorageUploaderConfig::new("https://storage.example.com");
println!(" - Uploader URL: {}", uploader_config.storage_url);
println!("All storage module tests passed!");
}