use std::fs;
use std::sync::Arc;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use calimero_blobstore::config::BlobStoreConfig;
use calimero_blobstore::{BlobManager, FileSystem};
use calimero_network_primitives::client::NetworkClient;
use calimero_node_primitives::bundle::{
derive_signer_id_did_key, sign_manifest_json, BundleManifest,
};
use calimero_node_primitives::client::NodeClient;
use calimero_store::db::InMemoryDB;
use calimero_store::Store;
use calimero_utils_actix::LazyRecipient;
use camino::Utf8PathBuf;
use ed25519_dalek::{Signer, SigningKey};
use flate2::write::GzEncoder;
use flate2::Compression;
use futures_util::io::Cursor;
use rand::rngs::OsRng;
use tar::Builder;
use tempfile::TempDir;
use tokio::sync::{broadcast, mpsc};
fn sign_manifest(manifest_json: &mut serde_json::Value, signing_key: &SigningKey) {
sign_manifest_json(manifest_json, signing_key).unwrap();
}
fn create_test_bundle(
temp_dir: &TempDir,
package: &str,
version: &str,
wasm_content: &[u8],
abi_content: Option<&[u8]>,
migrations: Vec<(&str, &[u8])>,
) -> Utf8PathBuf {
let bundle_path = temp_dir.path().join(format!("{}-{}.mpk", package, version));
let bundle_file = fs::File::create(&bundle_path).unwrap();
let encoder = GzEncoder::new(bundle_file, Compression::default());
let mut tar = Builder::new(encoder);
let signing_key = SigningKey::generate(&mut OsRng);
let signer_id = derive_signer_id_did_key(signing_key.verifying_key().as_bytes());
let mut manifest = BundleManifest {
version: "1.0".to_string(),
package: package.to_string(),
app_version: version.to_string(),
signer_id: Some(signer_id),
min_runtime_version: "0.1.0".to_string(),
metadata: None,
interfaces: None,
wasm: Some(calimero_node_primitives::bundle::BundleArtifact {
path: "app.wasm".to_string(),
hash: None,
size: wasm_content.len() as u64,
}),
abi: abi_content.map(|content| calimero_node_primitives::bundle::BundleArtifact {
path: "abi.json".to_string(),
hash: None,
size: content.len() as u64,
}),
migrations: migrations
.iter()
.map(
|(path, content)| calimero_node_primitives::bundle::BundleArtifact {
path: path.to_string(),
hash: None,
size: content.len() as u64,
},
)
.collect(),
links: None,
signature: None,
};
let mut manifest_json: serde_json::Value = serde_json::to_value(&manifest).unwrap();
sign_manifest(&mut manifest_json, &signing_key);
let manifest_bytes = serde_json::to_vec(&manifest_json).unwrap();
let mut manifest_header = tar::Header::new_gnu();
manifest_header.set_path("manifest.json").unwrap();
manifest_header.set_size(manifest_bytes.len() as u64);
manifest_header.set_cksum();
tar.append(&manifest_header, manifest_bytes.as_slice())
.unwrap();
let mut wasm_header = tar::Header::new_gnu();
wasm_header.set_path("app.wasm").unwrap();
wasm_header.set_size(wasm_content.len() as u64);
wasm_header.set_cksum();
tar.append(&wasm_header, wasm_content).unwrap();
if let Some(abi_content) = abi_content {
let mut abi_header = tar::Header::new_gnu();
abi_header.set_path("abi.json").unwrap();
abi_header.set_size(abi_content.len() as u64);
abi_header.set_cksum();
tar.append(&abi_header, abi_content).unwrap();
}
for (path, content) in migrations {
let mut migration_header = tar::Header::new_gnu();
migration_header.set_path(path).unwrap();
migration_header.set_size(content.len() as u64);
migration_header.set_cksum();
tar.append(&migration_header, content).unwrap();
}
tar.finish().unwrap();
bundle_path.try_into().unwrap()
}
async fn create_test_node_client(datastore: Option<Store>) -> (NodeClient, TempDir, TempDir) {
let data_dir = TempDir::new().unwrap();
let blob_dir = TempDir::new().unwrap();
let datastore = datastore.unwrap_or_else(|| Store::new(Arc::new(InMemoryDB::owned())));
let blobstore = BlobManager::new(
datastore.clone(),
FileSystem::new(&BlobStoreConfig::new(
blob_dir.path().to_path_buf().try_into().unwrap(),
))
.await
.unwrap(),
);
let (event_sender, _) = broadcast::channel(256);
let (ctx_sync_tx, _) = mpsc::channel(64);
let node_client = NodeClient::new(
datastore,
blobstore,
NetworkClient::new(LazyRecipient::new()),
LazyRecipient::new(),
event_sender,
ctx_sync_tx,
String::new(), );
(node_client, data_dir, blob_dir)
}
#[tokio::test]
async fn test_bundle_detection() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let wasm_path = temp_dir.path().join("app.wasm");
fs::write(&wasm_path, b"wasm content").unwrap();
let wasm_path_utf8: Utf8PathBuf = wasm_path.try_into().unwrap();
let result = node_client
.install_application_from_path(wasm_path_utf8, vec![], None, None)
.await;
assert!(result.is_ok(), "Single WASM installation should work");
let bundle_path = create_test_bundle(
&temp_dir,
"com.example.bundle",
"1.0.0",
b"wasm content",
None,
vec![],
);
let result = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await;
assert!(result.is_ok(), "Bundle installation should work");
}
#[tokio::test]
async fn test_bundle_installation() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, blob_dir) = create_test_node_client(None).await;
let wasm_content = b"fake wasm bytecode";
let abi_content = b"{\"types\": []}";
let migration1: &[u8] = b"CREATE TABLE test (id INT);";
let migration2: &[u8] = b"CREATE TABLE users (id INT);";
let migrations: Vec<(&str, &[u8])> = vec![
("migrations/001_init.sql", migration1),
("migrations/002_add_users.sql", migration2),
];
let bundle_path = create_test_bundle(
&temp_dir,
"com.example.test",
"1.0.0",
wasm_content,
Some(abi_content),
migrations.clone(),
);
let application_id = node_client
.install_application_from_path(bundle_path.clone(), vec![], None, None)
.await
.expect("Bundle installation should succeed");
let application = node_client
.get_application(&application_id)
.expect("Application should exist");
assert!(application.is_some(), "Application should be found");
let app = application.unwrap();
let blob_exists = node_client
.has_blob(&app.blob.bytecode)
.expect("Should check blob existence");
assert!(blob_exists, "Bundle blob should exist");
let node_root = blob_dir.path().parent().unwrap();
let extract_dir = node_root
.join("applications")
.join("com.example.test")
.join("1.0.0")
.join("extracted");
let wasm_path = extract_dir.join("app.wasm");
assert!(wasm_path.exists(), "Extracted WASM should exist");
let abi_path = extract_dir.join("abi.json");
assert!(abi_path.exists(), "Extracted ABI should exist");
let migration1_path = extract_dir.join("migrations/001_init.sql");
assert!(migration1_path.exists(), "First migration should exist");
let migration2_path = extract_dir.join("migrations/002_add_users.sql");
assert!(migration2_path.exists(), "Second migration should exist");
let extracted_wasm = fs::read(&wasm_path).unwrap();
assert_eq!(extracted_wasm, wasm_content, "WASM content should match");
let extracted_abi = fs::read(&abi_path).unwrap();
assert_eq!(extracted_abi, abi_content, "ABI content should match");
let extracted_migration1 = fs::read(&migration1_path).unwrap();
assert_eq!(
extracted_migration1, migrations[0].1,
"Migration 1 content should match"
);
}
#[tokio::test]
async fn test_bundle_get_application_bytes() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let wasm_content = b"fake wasm bytecode for runtime";
let bundle_path = create_test_bundle(
&temp_dir,
"com.example.runtime",
"2.0.0",
wasm_content,
None,
vec![],
);
let application_id = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await
.expect("Bundle installation should succeed");
let bytes = node_client
.get_application_bytes(&application_id)
.await
.expect("Should get application bytes")
.expect("Application bytes should exist");
assert_eq!(
bytes.as_ref(),
wasm_content,
"Application bytes should match WASM content"
);
}
#[tokio::test]
async fn test_bundle_deduplication() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, blob_dir) = create_test_node_client(None).await;
let wasm_content_v1 = b"shared wasm bytecode";
let bundle_path_v1 = create_test_bundle(
&temp_dir,
"com.example.shared",
"1.0.0",
wasm_content_v1,
None,
vec![],
);
let _app_id_v1 = node_client
.install_application_from_path(bundle_path_v1, vec![], None, None)
.await
.expect("First bundle installation should succeed");
let wasm_content_v2 = wasm_content_v1; let bundle_path_v2 = create_test_bundle(
&temp_dir,
"com.example.shared",
"2.0.0",
wasm_content_v2,
None,
vec![],
);
let _app_id_v2 = node_client
.install_application_from_path(bundle_path_v2, vec![], None, None)
.await
.expect("Second bundle installation should succeed");
let node_root = blob_dir.path().parent().unwrap();
let extract_dir_v1 = node_root
.join("applications")
.join("com.example.shared")
.join("1.0.0")
.join("extracted");
let extract_dir_v2 = node_root
.join("applications")
.join("com.example.shared")
.join("2.0.0")
.join("extracted");
let wasm_path_v1 = extract_dir_v1.join("app.wasm");
let wasm_path_v2 = extract_dir_v2.join("app.wasm");
assert!(wasm_path_v1.exists(), "V1 WASM should exist");
assert!(wasm_path_v2.exists(), "V2 WASM should exist");
let content_v1 = fs::read(&wasm_path_v1).unwrap();
let content_v2 = fs::read(&wasm_path_v2).unwrap();
assert_eq!(
content_v1, content_v2,
"Both versions should have same WASM content"
);
assert_eq!(content_v1, wasm_content_v1, "Content should match original");
}
#[tokio::test]
async fn test_bundle_manifest_validation() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let bundle_path = create_test_bundle(
&temp_dir,
"com.example.wrong", "1.0.0",
b"wasm",
None,
vec![],
);
let result = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await;
assert!(
result.is_ok(),
"Bundle installation should succeed when extracting from manifest"
);
}
#[tokio::test]
async fn test_bundle_validation_missing_fields() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let bundle_path = temp_dir.path().join("invalid-bundle.mpk");
let bundle_file = fs::File::create(&bundle_path).unwrap();
let encoder = GzEncoder::new(bundle_file, Compression::default());
let mut tar = Builder::new(encoder);
let invalid_manifest_json = r#"{
"version": "1.0",
"appVersion": "1.0.0",
"minRuntimeVersion": "0.1.0",
"wasm": {
"path": "app.wasm",
"size": 10
},
"migrations": []
}"#;
let manifest_json = invalid_manifest_json.as_bytes();
let mut manifest_header = tar::Header::new_gnu();
manifest_header.set_path("manifest.json").unwrap();
manifest_header.set_size(manifest_json.len() as u64);
manifest_header.set_cksum();
tar.append(&manifest_header, manifest_json).unwrap();
let mut wasm_header = tar::Header::new_gnu();
wasm_header.set_path("app.wasm").unwrap();
wasm_header.set_size(10);
wasm_header.set_cksum();
tar.append(&wasm_header, &b"fake wasm"[..]).unwrap();
tar.finish().unwrap();
drop(tar); let bundle_path_utf8: Utf8PathBuf = bundle_path.try_into().unwrap();
let result = node_client
.install_application_from_path(bundle_path_utf8, vec![], None, None)
.await;
assert!(
result.is_err(),
"Bundle installation should fail for invalid manifest"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("package")
|| error_msg.contains("missing field")
|| error_msg.contains("empty")
|| error_msg.contains("manifest")
|| error_msg.contains("parse"),
"Error should mention missing package field, manifest issue, or parse error, got: {}",
error_msg
);
}
#[tokio::test]
async fn test_bundle_backward_compatibility() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let wasm_path = temp_dir.path().join("app.wasm");
fs::write(&wasm_path, b"single wasm content").unwrap();
let wasm_path_utf8: Utf8PathBuf = wasm_path.try_into().unwrap();
let application_id = node_client
.install_application_from_path(wasm_path_utf8, vec![], None, None)
.await
.expect("Single WASM installation should work");
let application = node_client
.get_application(&application_id)
.expect("Application should exist");
assert!(
application.is_some(),
"Single WASM application should be found"
);
let bytes = node_client
.get_application_bytes(&application_id)
.await
.expect("Should get application bytes")
.expect("Application bytes should exist");
assert_eq!(bytes.as_ref(), b"single wasm content", "Bytes should match");
}
fn create_test_bundle_custom_wasm_path(
temp_dir: &TempDir,
package: &str,
version: &str,
wasm_path: &str,
wasm_content: &[u8],
) -> Utf8PathBuf {
let bundle_path = temp_dir.path().join(format!("{}-{}.mpk", package, version));
let bundle_file = fs::File::create(&bundle_path).unwrap();
let encoder = GzEncoder::new(bundle_file, Compression::default());
let mut tar = Builder::new(encoder);
let signing_key = SigningKey::generate(&mut OsRng);
let signer_id = derive_signer_id_did_key(signing_key.verifying_key().as_bytes());
let manifest = BundleManifest {
version: "1.0".to_string(),
package: package.to_string(),
app_version: version.to_string(),
signer_id: Some(signer_id),
min_runtime_version: "0.1.0".to_string(),
metadata: None,
interfaces: None,
wasm: Some(calimero_node_primitives::bundle::BundleArtifact {
path: wasm_path.to_string(),
hash: None,
size: wasm_content.len() as u64,
}),
abi: None,
migrations: vec![],
links: None,
signature: None,
};
let mut manifest_json: serde_json::Value = serde_json::to_value(&manifest).unwrap();
sign_manifest(&mut manifest_json, &signing_key);
let manifest_bytes = serde_json::to_vec(&manifest_json).unwrap();
let mut manifest_header = tar::Header::new_gnu();
manifest_header.set_path("manifest.json").unwrap();
manifest_header.set_size(manifest_bytes.len() as u64);
manifest_header.set_cksum();
tar.append(&manifest_header, manifest_bytes.as_slice())
.unwrap();
let mut wasm_header = tar::Header::new_gnu();
wasm_header.set_path(wasm_path).unwrap();
wasm_header.set_size(wasm_content.len() as u64);
wasm_header.set_cksum();
tar.append(&wasm_header, wasm_content).unwrap();
tar.finish().unwrap();
bundle_path.try_into().unwrap()
}
#[tokio::test]
async fn test_bundle_custom_wasm_path() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, blob_dir) = create_test_node_client(None).await;
let wasm_content = b"custom path wasm bytecode";
let bundle_path = create_test_bundle_custom_wasm_path(
&temp_dir,
"com.example.custom",
"1.0.0",
"src/main.wasm",
wasm_content,
);
let application_id = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await
.expect("Bundle installation should succeed");
let node_root = blob_dir.path().parent().unwrap();
let extract_dir = node_root
.join("applications")
.join("com.example.custom")
.join("1.0.0")
.join("extracted");
let wasm_path = extract_dir.join("src/main.wasm");
assert!(
wasm_path.exists(),
"WASM should be extracted at custom path"
);
let bytes = node_client
.get_application_bytes(&application_id)
.await
.expect("Should get application bytes")
.expect("Application bytes should exist");
assert_eq!(
bytes.as_ref(),
wasm_content,
"Application bytes should match WASM content from custom path"
);
}
#[tokio::test]
async fn test_bundle_no_metadata() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let bundle_path = create_test_bundle(
&temp_dir,
"com.example.metadata",
"1.0.0",
b"wasm content",
None,
vec![],
);
let application_id = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await
.expect("Bundle installation should succeed");
let application = node_client
.get_application(&application_id)
.expect("Application should exist")
.expect("Application should be found");
assert!(
!application.metadata.is_empty(),
"Bundle metadata should contain package and version extracted from manifest"
);
let metadata_json: serde_json::Value =
serde_json::from_slice(&application.metadata).expect("Metadata should be valid JSON");
assert_eq!(
metadata_json["package"], "com.example.metadata",
"Package should match manifest"
);
assert_eq!(
metadata_json["version"], "1.0.0",
"Version should match manifest"
);
}
#[tokio::test]
async fn test_bundle_validation_empty_package() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let bundle_path = temp_dir.path().join("empty-package.mpk");
let bundle_file = fs::File::create(&bundle_path).unwrap();
let encoder = GzEncoder::new(bundle_file, Compression::default());
let mut tar = Builder::new(encoder);
let invalid_manifest_json = r#"{
"version": "1.0",
"package": "",
"appVersion": "1.0.0",
"minRuntimeVersion": "0.1.0",
"wasm": {
"path": "app.wasm",
"size": 10
},
"migrations": []
}"#;
let manifest_json = invalid_manifest_json.as_bytes();
let mut manifest_header = tar::Header::new_gnu();
manifest_header.set_path("manifest.json").unwrap();
manifest_header.set_size(manifest_json.len() as u64);
manifest_header.set_cksum();
tar.append(&manifest_header, manifest_json).unwrap();
let mut wasm_header = tar::Header::new_gnu();
wasm_header.set_path("app.wasm").unwrap();
wasm_header.set_size(10);
wasm_header.set_cksum();
tar.append(&wasm_header, &b"fake wasm"[..]).unwrap();
tar.finish().unwrap();
drop(tar);
let bundle_path_utf8: Utf8PathBuf = bundle_path.try_into().unwrap();
let result = node_client
.install_application_from_path(bundle_path_utf8, vec![], None, None)
.await;
assert!(
result.is_err(),
"Bundle installation should fail for empty package"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("package") && error_msg.contains("empty"),
"Error should mention empty package field, got: {}",
error_msg
);
}
#[tokio::test]
async fn test_bundle_validation_empty_app_version() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let bundle_path = temp_dir.path().join("empty-version.mpk");
let bundle_file = fs::File::create(&bundle_path).unwrap();
let encoder = GzEncoder::new(bundle_file, Compression::default());
let mut tar = Builder::new(encoder);
let invalid_manifest_json = r#"{
"version": "1.0",
"package": "com.example.test",
"appVersion": "",
"minRuntimeVersion": "0.1.0",
"wasm": {
"path": "app.wasm",
"size": 10
},
"migrations": []
}"#;
let manifest_json = invalid_manifest_json.as_bytes();
let mut manifest_header = tar::Header::new_gnu();
manifest_header.set_path("manifest.json").unwrap();
manifest_header.set_size(manifest_json.len() as u64);
manifest_header.set_cksum();
tar.append(&manifest_header, manifest_json).unwrap();
let mut wasm_header = tar::Header::new_gnu();
wasm_header.set_path("app.wasm").unwrap();
wasm_header.set_size(10);
wasm_header.set_cksum();
tar.append(&wasm_header, &b"fake wasm"[..]).unwrap();
tar.finish().unwrap();
drop(tar);
let bundle_path_utf8: Utf8PathBuf = bundle_path.try_into().unwrap();
let result = node_client
.install_application_from_path(bundle_path_utf8, vec![], None, None)
.await;
assert!(
result.is_err(),
"Bundle installation should fail for empty appVersion"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("appVersion") || error_msg.contains("version"),
"Error should mention empty appVersion field, got: {}",
error_msg
);
}
#[tokio::test]
async fn test_bundle_validation_missing_app_version() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let bundle_path = temp_dir.path().join("missing-version.mpk");
let bundle_file = fs::File::create(&bundle_path).unwrap();
let encoder = GzEncoder::new(bundle_file, Compression::default());
let mut tar = Builder::new(encoder);
let invalid_manifest_json = r#"{
"version": "1.0",
"package": "com.example.test",
"minRuntimeVersion": "0.1.0",
"wasm": {
"path": "app.wasm",
"size": 10
},
"migrations": []
}"#;
let manifest_json = invalid_manifest_json.as_bytes();
let mut manifest_header = tar::Header::new_gnu();
manifest_header.set_path("manifest.json").unwrap();
manifest_header.set_size(manifest_json.len() as u64);
manifest_header.set_cksum();
tar.append(&manifest_header, manifest_json).unwrap();
let mut wasm_header = tar::Header::new_gnu();
wasm_header.set_path("app.wasm").unwrap();
wasm_header.set_size(10);
wasm_header.set_cksum();
tar.append(&wasm_header, &b"fake wasm"[..]).unwrap();
tar.finish().unwrap();
drop(tar);
let bundle_path_utf8: Utf8PathBuf = bundle_path.try_into().unwrap();
let result = node_client
.install_application_from_path(bundle_path_utf8, vec![], None, None)
.await;
assert!(
result.is_err(),
"Bundle installation should fail for missing appVersion"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("appVersion")
|| error_msg.contains("missing field")
|| error_msg.contains("version")
|| error_msg.contains("parse"),
"Error should mention missing appVersion field, got: {}",
error_msg
);
}
#[tokio::test]
async fn test_bundle_deduplication_different_paths() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, blob_dir) = create_test_node_client(None).await;
let wasm_content = b"shared wasm";
let bundle_path_v1 = create_test_bundle_custom_wasm_path(
&temp_dir,
"com.example.paths",
"1.0.0",
"src/app.wasm",
wasm_content,
);
let _app_id_v1 = node_client
.install_application_from_path(bundle_path_v1, vec![], None, None)
.await
.expect("First bundle installation should succeed");
let bundle_path_v2 = create_test_bundle_custom_wasm_path(
&temp_dir,
"com.example.paths",
"2.0.0",
"lib/app.wasm", wasm_content, );
let _app_id_v2 = node_client
.install_application_from_path(bundle_path_v2, vec![], None, None)
.await
.expect("Second bundle installation should succeed");
let node_root = blob_dir.path().parent().unwrap();
let extract_dir_v1 = node_root
.join("applications")
.join("com.example.paths")
.join("1.0.0")
.join("extracted");
let extract_dir_v2 = node_root
.join("applications")
.join("com.example.paths")
.join("2.0.0")
.join("extracted");
let wasm_path_v1 = extract_dir_v1.join("src/app.wasm");
let wasm_path_v2 = extract_dir_v2.join("lib/app.wasm");
assert!(
wasm_path_v1.exists(),
"V1 WASM should exist at src/app.wasm"
);
assert!(
wasm_path_v2.exists(),
"V2 WASM should exist at lib/app.wasm"
);
let content_v1 = fs::read(&wasm_path_v1).unwrap();
let content_v2 = fs::read(&wasm_path_v2).unwrap();
assert_eq!(content_v1, content_v2, "Both should have same content");
assert_eq!(content_v1, wasm_content, "Content should match original");
}
#[tokio::test]
async fn test_bundle_extract_dir_derived_from_manifest() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, blob_dir) = create_test_node_client(None).await;
let bundle_path = create_test_bundle(
&temp_dir,
"com.example.derived",
"2.5.0",
b"wasm content",
None,
vec![],
);
let application_id = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await
.expect("Bundle installation should succeed");
let application = node_client
.get_application(&application_id)
.expect("Application should exist")
.expect("Application should be found");
assert!(
!application.metadata.is_empty(),
"Bundle metadata should contain package and version extracted from manifest"
);
let metadata_json: serde_json::Value =
serde_json::from_slice(&application.metadata).expect("Metadata should be valid JSON");
assert_eq!(
metadata_json["package"], "com.example.derived",
"Package should match manifest"
);
assert_eq!(
metadata_json["version"], "2.5.0",
"Version should match manifest"
);
let node_root = blob_dir.path().parent().unwrap();
let extract_dir = node_root
.join("applications")
.join("com.example.derived")
.join("2.5.0")
.join("extracted");
assert!(
extract_dir.exists(),
"Extract dir should exist at derived path"
);
}
#[tokio::test]
async fn test_bundle_package_version_extracted_from_manifest() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let bundle_path = create_test_bundle(
&temp_dir,
"com.example.extracted",
"3.7.2",
b"wasm content",
None,
vec![],
);
let application_id = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await
.expect("Bundle installation should succeed");
let _application = node_client
.get_application(&application_id)
.expect("Application should exist")
.expect("Application should be found");
let packages = node_client.list_packages().expect("Should list packages");
assert!(
packages.contains(&"com.example.extracted".to_string()),
"Package should be listed"
);
let versions = node_client
.list_versions("com.example.extracted")
.expect("Should list versions");
assert!(
versions.contains(&"3.7.2".to_string()),
"Version should be listed"
);
}
#[tokio::test]
async fn test_is_bundle_blob() {
let temp_dir = TempDir::new().unwrap();
let (_node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let bundle_path = create_test_bundle(
&temp_dir,
"com.example.test",
"1.0.0",
b"wasm content",
None,
vec![],
);
let bundle_bytes = fs::read(&bundle_path).unwrap();
assert!(
NodeClient::is_bundle_blob(&bundle_bytes),
"Bundle blob should be detected as bundle"
);
let wasm_bytes = b"fake wasm bytecode";
assert!(
!NodeClient::is_bundle_blob(wasm_bytes),
"Regular WASM should not be detected as bundle"
);
let random_bytes = b"random bytes that are not a bundle";
assert!(
!NodeClient::is_bundle_blob(random_bytes),
"Random bytes should not be detected as bundle"
);
}
#[tokio::test]
async fn test_install_application_from_bundle_blob() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, blob_dir) = create_test_node_client(None).await;
let wasm_content = b"bundle wasm bytecode";
let abi_content = b"{\"types\": []}";
let bundle_path = create_test_bundle(
&temp_dir,
"com.example.blob",
"1.0.0",
wasm_content,
Some(abi_content),
vec![],
);
let bundle_data = fs::read(&bundle_path).unwrap();
let cursor = Cursor::new(bundle_data.as_slice());
let (blob_id, _size) = node_client
.add_blob(cursor, Some(bundle_data.len() as u64), None)
.await
.expect("Should add bundle blob");
let source = "file:///test/bundle.mpk".parse().unwrap();
let application_id = node_client
.install_application_from_bundle_blob(&blob_id, &source)
.await
.expect("Should install from bundle blob");
let application = node_client
.get_application(&application_id)
.expect("Application should exist");
assert!(application.is_some(), "Application should be found");
let node_root = blob_dir.path().parent().unwrap();
let extract_dir = node_root
.join("applications")
.join("com.example.blob")
.join("1.0.0")
.join("extracted");
let wasm_path = extract_dir.join("app.wasm");
assert!(wasm_path.exists(), "Extracted WASM should exist");
let abi_path = extract_dir.join("abi.json");
assert!(abi_path.exists(), "Extracted ABI should exist");
let extracted_wasm = fs::read(&wasm_path).unwrap();
assert_eq!(extracted_wasm, wasm_content, "WASM content should match");
let bytes = node_client
.get_application_bytes(&application_id)
.await
.expect("Should get application bytes")
.expect("Application bytes should exist");
assert_eq!(
bytes.as_ref(),
wasm_content,
"Application bytes should match WASM"
);
}
#[tokio::test]
async fn test_install_application_from_bundle_blob_no_metadata() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let bundle_path = create_test_bundle(
&temp_dir,
"com.example.metadata",
"1.0.0",
b"wasm content",
None,
vec![],
);
let bundle_data = fs::read(&bundle_path).unwrap();
let cursor = Cursor::new(bundle_data.as_slice());
let (blob_id, _size) = node_client
.add_blob(cursor, Some(bundle_data.len() as u64), None)
.await
.expect("Should add bundle blob");
let source = "file:///test/bundle.mpk".parse().unwrap();
let application_id = node_client
.install_application_from_bundle_blob(&blob_id, &source)
.await
.expect("Should install from bundle blob without metadata");
let application = node_client
.get_application(&application_id)
.expect("Application should exist")
.expect("Application should be found");
assert!(
!application.metadata.is_empty(),
"Bundle metadata should contain package and version extracted from manifest"
);
let metadata_json: serde_json::Value =
serde_json::from_slice(&application.metadata).expect("Metadata should be valid JSON");
assert_eq!(
metadata_json["package"], "com.example.metadata",
"Package should match manifest"
);
assert_eq!(
metadata_json["version"], "1.0.0",
"Version should match manifest"
);
}
#[tokio::test]
async fn test_install_application_from_bundle_blob_missing_blob() {
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
use calimero_primitives::blobs::BlobId;
let fake_blob_id = BlobId::from([1; 32]);
let source = "file:///test/bundle.mpk".parse().unwrap();
let result = node_client
.install_application_from_bundle_blob(&fake_blob_id, &source)
.await;
assert!(result.is_err(), "Should fail when blob doesn't exist");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("not found") || error_msg.contains("fatal"),
"Error should mention blob not found, got: {}",
error_msg
);
}
#[tokio::test]
async fn test_simple_wasm_installation_still_works() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let wasm_path = temp_dir.path().join("app.wasm");
fs::write(&wasm_path, b"simple wasm bytecode").unwrap();
let wasm_path_utf8: Utf8PathBuf = wasm_path.try_into().unwrap();
let application_id = node_client
.install_application_from_path(wasm_path_utf8, vec![], None, None)
.await
.expect("Single WASM installation should work");
let application = node_client
.get_application(&application_id)
.expect("Application should exist");
assert!(
application.is_some(),
"Single WASM application should be found"
);
let bytes = node_client
.get_application_bytes(&application_id)
.await
.expect("Should get application bytes")
.expect("Application bytes should exist");
assert_eq!(
bytes.as_ref(),
b"simple wasm bytecode",
"Bytes should match"
);
let app = application.unwrap();
let blob_bytes = node_client
.get_blob_bytes(&app.blob.bytecode, None)
.await
.expect("Should get blob bytes")
.expect("Blob bytes should exist");
assert!(
!NodeClient::is_bundle_blob(&blob_bytes),
"Simple WASM should not be detected as bundle"
);
}
#[tokio::test]
async fn test_bundle_blob_sharing_integration() {
let temp_dir = TempDir::new().unwrap();
let (node_client_1, _data_dir_1, _blob_dir_1) = create_test_node_client(None).await;
let (node_client_2, _data_dir_2, blob_dir_2) = create_test_node_client(None).await;
let wasm_content = b"integration test wasm bytecode";
let bundle_path = create_test_bundle(
&temp_dir,
"com.example.integration",
"1.0.0",
wasm_content,
None,
vec![],
);
let application_id_user1 = node_client_1
.install_application_from_path(bundle_path.clone(), vec![], None, None)
.await
.expect("User 1 should install bundle successfully");
let app_user1 = node_client_1
.get_application(&application_id_user1)
.expect("Application should exist")
.expect("Application should be found");
let bundle_blob_id = app_user1.blob.bytecode;
let bundle_size = app_user1.size;
let bundle_source = app_user1.source;
let bundle_data = node_client_1
.get_blob_bytes(&bundle_blob_id, None)
.await
.expect("Should get bundle blob from User 1's blobstore")
.expect("Bundle blob should exist");
let cursor = Cursor::new(bundle_data.as_ref());
let (received_blob_id, received_size) = node_client_2
.add_blob(cursor, Some(bundle_data.len() as u64), None)
.await
.expect("User 2 should receive bundle blob");
assert_eq!(
received_blob_id, bundle_blob_id,
"Received blob ID should match original bundle blob ID"
);
assert_eq!(
received_size, bundle_size,
"Received blob size should match original bundle size"
);
assert!(
!node_client_2
.has_application(&application_id_user1)
.unwrap(),
"User 2 should not have application before sync"
);
let application_id_user2 = node_client_2
.install_application_from_bundle_blob(
&bundle_blob_id,
&bundle_source, )
.await
.expect("User 2 should install from bundle blob");
assert_eq!(
application_id_user1, application_id_user2,
"ApplicationId should be identical (same blob_id, size, source, and metadata from manifest)"
);
let app_user1_final = node_client_1
.get_application(&application_id_user1)
.expect("Application should exist")
.expect("Application should be found");
let app_user2_final = node_client_2
.get_application(&application_id_user2)
.expect("Application should exist")
.expect("Application should be found");
assert_eq!(
app_user1_final.blob.bytecode, app_user2_final.blob.bytecode,
"Blob IDs should be identical (same bundle content)"
);
assert_eq!(
app_user1_final.size, app_user2_final.size,
"Sizes should be identical"
);
assert_eq!(
app_user1_final.source.to_string(),
app_user2_final.source.to_string(),
"Sources should be identical"
);
assert_eq!(
app_user1_final.metadata, app_user2_final.metadata,
"Metadata should be identical (extracted from same bundle manifest)"
);
assert!(
!app_user1_final.metadata.is_empty(),
"User 1 should have metadata extracted from bundle manifest"
);
assert!(
!app_user2_final.metadata.is_empty(),
"User 2 should have metadata extracted from bundle manifest"
);
let metadata_json: serde_json::Value =
serde_json::from_slice(&app_user1_final.metadata).expect("Metadata should be valid JSON");
assert_eq!(
metadata_json["package"], "com.example.integration",
"Package should match manifest"
);
assert_eq!(
metadata_json["version"], "1.0.0",
"Version should match manifest"
);
let bytes_user2 = node_client_2
.get_application_bytes(&application_id_user2)
.await
.expect("Should get application bytes")
.expect("Application bytes should exist");
assert_eq!(
bytes_user2.as_ref(),
wasm_content,
"User 2 should be able to read WASM from bundle"
);
let node_root_2 = blob_dir_2.path().parent().unwrap();
let extract_dir_2 = node_root_2
.join("applications")
.join("com.example.integration")
.join("1.0.0")
.join("extracted");
let wasm_path_2 = extract_dir_2.join("app.wasm");
assert!(
wasm_path_2.exists(),
"User 2 should have extracted WASM file"
);
let extracted_wasm_2 = fs::read(&wasm_path_2).unwrap();
assert_eq!(
extracted_wasm_2, wasm_content,
"Extracted WASM content should match"
);
}
fn create_unsigned_bundle(
temp_dir: &TempDir,
package: &str,
version: &str,
wasm_content: &[u8],
) -> Utf8PathBuf {
let bundle_path = temp_dir
.path()
.join(format!("{}-{}-unsigned.mpk", package, version));
let bundle_file = fs::File::create(&bundle_path).unwrap();
let encoder = GzEncoder::new(bundle_file, Compression::default());
let mut tar = Builder::new(encoder);
let manifest_json = serde_json::json!({
"version": "1.0",
"package": package,
"appVersion": version,
"signerId": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"minRuntimeVersion": "0.1.0",
"wasm": {
"path": "app.wasm",
"size": wasm_content.len()
},
"migrations": []
});
let manifest_bytes = serde_json::to_vec(&manifest_json).unwrap();
let mut manifest_header = tar::Header::new_gnu();
manifest_header.set_path("manifest.json").unwrap();
manifest_header.set_size(manifest_bytes.len() as u64);
manifest_header.set_cksum();
tar.append(&manifest_header, manifest_bytes.as_slice())
.unwrap();
let mut wasm_header = tar::Header::new_gnu();
wasm_header.set_path("app.wasm").unwrap();
wasm_header.set_size(wasm_content.len() as u64);
wasm_header.set_cksum();
tar.append(&wasm_header, wasm_content).unwrap();
tar.finish().unwrap();
bundle_path.try_into().unwrap()
}
fn create_tampered_bundle(
temp_dir: &TempDir,
package: &str,
version: &str,
wasm_content: &[u8],
) -> Utf8PathBuf {
let bundle_path = temp_dir
.path()
.join(format!("{}-{}-tampered.mpk", package, version));
let bundle_file = fs::File::create(&bundle_path).unwrap();
let encoder = GzEncoder::new(bundle_file, Compression::default());
let mut tar = Builder::new(encoder);
let signing_key = SigningKey::generate(&mut OsRng);
let signer_id = derive_signer_id_did_key(signing_key.verifying_key().as_bytes());
let mut original_manifest = serde_json::json!({
"version": "1.0",
"package": package,
"appVersion": version,
"signerId": signer_id,
"minRuntimeVersion": "0.1.0",
"wasm": {
"path": "app.wasm",
"size": wasm_content.len()
},
"migrations": []
});
sign_manifest(&mut original_manifest, &signing_key);
let signature = original_manifest.get("signature").unwrap().clone();
let tampered_manifest = serde_json::json!({
"version": "1.0",
"package": package,
"appVersion": "999.999.999", "signerId": signer_id,
"minRuntimeVersion": "0.1.0",
"wasm": {
"path": "app.wasm",
"size": wasm_content.len()
},
"migrations": [],
"signature": signature });
let manifest_bytes = serde_json::to_vec(&tampered_manifest).unwrap();
let mut manifest_header = tar::Header::new_gnu();
manifest_header.set_path("manifest.json").unwrap();
manifest_header.set_size(manifest_bytes.len() as u64);
manifest_header.set_cksum();
tar.append(&manifest_header, manifest_bytes.as_slice())
.unwrap();
let mut wasm_header = tar::Header::new_gnu();
wasm_header.set_path("app.wasm").unwrap();
wasm_header.set_size(wasm_content.len() as u64);
wasm_header.set_cksum();
tar.append(&wasm_header, wasm_content).unwrap();
tar.finish().unwrap();
bundle_path.try_into().unwrap()
}
fn create_signer_id_mismatch_bundle(
temp_dir: &TempDir,
package: &str,
version: &str,
wasm_content: &[u8],
) -> Utf8PathBuf {
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use calimero_node_primitives::bundle::{canonicalize_manifest, compute_signing_payload};
use ed25519_dalek::Signer;
let bundle_path = temp_dir
.path()
.join(format!("{}-{}-signer-mismatch.mpk", package, version));
let bundle_file = fs::File::create(&bundle_path).unwrap();
let encoder = GzEncoder::new(bundle_file, Compression::default());
let mut tar = Builder::new(encoder);
let signing_key_a = SigningKey::generate(&mut OsRng);
let signing_key_b = SigningKey::generate(&mut OsRng);
let signer_id_b = derive_signer_id_did_key(signing_key_b.verifying_key().as_bytes());
let mut manifest = serde_json::json!({
"version": "1.0",
"package": package,
"appVersion": version,
"signerId": signer_id_b, "minRuntimeVersion": "0.1.0",
"wasm": {
"path": "app.wasm",
"size": wasm_content.len()
},
"migrations": []
});
let canonical_bytes = canonicalize_manifest(&manifest).unwrap();
let signing_payload = compute_signing_payload(&canonical_bytes);
let signature = signing_key_a.sign(&signing_payload);
let public_key_b64 = URL_SAFE_NO_PAD.encode(signing_key_a.verifying_key().as_bytes());
let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
manifest.as_object_mut().unwrap().insert(
"signature".to_string(),
serde_json::json!({
"algorithm": "ed25519",
"publicKey": public_key_b64,
"signature": signature_b64
}),
);
let manifest_bytes = serde_json::to_vec(&manifest).unwrap();
let mut manifest_header = tar::Header::new_gnu();
manifest_header.set_path("manifest.json").unwrap();
manifest_header.set_size(manifest_bytes.len() as u64);
manifest_header.set_cksum();
tar.append(&manifest_header, manifest_bytes.as_slice())
.unwrap();
let mut wasm_header = tar::Header::new_gnu();
wasm_header.set_path("app.wasm").unwrap();
wasm_header.set_size(wasm_content.len() as u64);
wasm_header.set_cksum();
tar.append(&wasm_header, wasm_content).unwrap();
tar.finish().unwrap();
bundle_path.try_into().unwrap()
}
fn create_test_bundle_with_key(
temp_dir: &TempDir,
package: &str,
version: &str,
wasm_content: &[u8],
signing_key: &SigningKey,
) -> Utf8PathBuf {
let bundle_path = temp_dir
.path()
.join(format!("{}-{}-keyed.mpk", package, version));
let bundle_file = fs::File::create(&bundle_path).unwrap();
let encoder = GzEncoder::new(bundle_file, Compression::default());
let mut tar = Builder::new(encoder);
let signer_id = derive_signer_id_did_key(signing_key.verifying_key().as_bytes());
let mut manifest_json: serde_json::Value = serde_json::to_value(&BundleManifest {
version: "1.0".to_string(),
package: package.to_string(),
app_version: version.to_string(),
signer_id: Some(signer_id),
min_runtime_version: "0.1.0".to_string(),
metadata: None,
interfaces: None,
wasm: Some(calimero_node_primitives::bundle::BundleArtifact {
path: "app.wasm".to_string(),
hash: None,
size: wasm_content.len() as u64,
}),
abi: None,
migrations: vec![],
links: None,
signature: None,
})
.unwrap();
sign_manifest(&mut manifest_json, signing_key);
let manifest_bytes = serde_json::to_vec(&manifest_json).unwrap();
let mut manifest_header = tar::Header::new_gnu();
manifest_header.set_path("manifest.json").unwrap();
manifest_header.set_size(manifest_bytes.len() as u64);
manifest_header.set_cksum();
tar.append(&manifest_header, manifest_bytes.as_slice())
.unwrap();
let mut wasm_header = tar::Header::new_gnu();
wasm_header.set_path("app.wasm").unwrap();
wasm_header.set_size(wasm_content.len() as u64);
wasm_header.set_cksum();
tar.append(&wasm_header, wasm_content).unwrap();
tar.finish().unwrap();
bundle_path.try_into().unwrap()
}
#[tokio::test]
async fn test_bundle_installation_fails_without_signature() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let bundle_path =
create_unsigned_bundle(&temp_dir, "com.example.unsigned", "1.0.0", b"wasm content");
let result = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await;
assert!(
result.is_err(),
"Bundle installation should fail without signature"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("signature") || error_msg.contains("missing"),
"Error should mention missing signature, got: {}",
error_msg
);
}
#[tokio::test]
async fn test_bundle_installation_fails_with_invalid_signature() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let bundle_path =
create_tampered_bundle(&temp_dir, "com.example.tampered", "1.0.0", b"wasm content");
let result = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await;
assert!(
result.is_err(),
"Bundle installation should fail with tampered content"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("signature") || error_msg.contains("verification"),
"Error should mention signature verification failure, got: {}",
error_msg
);
}
#[tokio::test]
async fn test_bundle_installation_fails_signer_id_mismatch() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let bundle_path = create_signer_id_mismatch_bundle(
&temp_dir,
"com.example.mismatch",
"1.0.0",
b"wasm content",
);
let result = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await;
assert!(
result.is_err(),
"Bundle installation should fail with signerId mismatch"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("signerId") || error_msg.contains("mismatch"),
"Error should mention signerId mismatch, got: {}",
error_msg
);
}
#[tokio::test]
async fn test_bundle_application_id_derived_from_package_and_signer_id() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let signing_key_s1 = SigningKey::generate(&mut OsRng);
let signing_key_s2 = SigningKey::generate(&mut OsRng);
let signer_id_s1 = derive_signer_id_did_key(signing_key_s1.verifying_key().as_bytes());
let signer_id_s2 = derive_signer_id_did_key(signing_key_s2.verifying_key().as_bytes());
assert_ne!(
signer_id_s1, signer_id_s2,
"Test setup: signerIds should be different"
);
let package = "com.example.appkey-test";
let bundle_a = create_test_bundle_with_key(
&temp_dir,
package,
"1.0.0",
b"wasm v1 signer1",
&signing_key_s1,
);
let app_id_a = node_client
.install_application_from_path(bundle_a, vec![], None, None)
.await
.expect("Bundle A installation should succeed");
let bundle_b = create_test_bundle_with_key(
&temp_dir,
package,
"1.0.0-s2", b"wasm v1 signer2",
&signing_key_s2,
);
let app_id_b = node_client
.install_application_from_path(bundle_b, vec![], None, None)
.await
.expect("Bundle B installation should succeed");
let bundle_c = create_test_bundle_with_key(
&temp_dir,
package,
"2.0.0",
b"wasm v2 signer1",
&signing_key_s1,
);
let app_id_c = node_client
.install_application_from_path(bundle_c, vec![], None, None)
.await
.expect("Bundle C installation should succeed");
assert_ne!(
app_id_a, app_id_b,
"Same package with different signers should produce different ApplicationIds.\n\
app_id_a (signer S1): {}\n\
app_id_b (signer S2): {}",
app_id_a, app_id_b
);
assert_eq!(
app_id_a, app_id_c,
"Same package with same signer should produce same ApplicationId (version upgrade).\n\
app_id_a (v1.0.0): {}\n\
app_id_c (v2.0.0): {}",
app_id_a, app_id_c
);
assert!(
node_client.has_application(&app_id_a).unwrap(),
"Application A should exist"
);
assert!(
node_client.has_application(&app_id_b).unwrap(),
"Application B should exist"
);
let app_a = node_client
.get_application(&app_id_a)
.expect("Should get application A")
.expect("Application A should exist");
let app_b = node_client
.get_application(&app_id_b)
.expect("Should get application B")
.expect("Application B should exist");
let metadata_a: serde_json::Value =
serde_json::from_slice(&app_a.metadata).expect("Metadata A should be valid JSON");
let metadata_b: serde_json::Value =
serde_json::from_slice(&app_b.metadata).expect("Metadata B should be valid JSON");
assert_eq!(
metadata_a["package"], package,
"Application A should have correct package"
);
assert_eq!(
metadata_b["package"], package,
"Application B should have correct package"
);
}
#[tokio::test]
async fn test_bundle_get_application_bytes_fallback() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, blob_dir) = create_test_node_client(None).await;
let wasm_content = b"fallback test wasm bytecode";
let bundle_path = create_test_bundle(
&temp_dir,
"com.example.fallback",
"1.0.0",
wasm_content,
None,
vec![],
);
let application_id = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await
.expect("Bundle installation should succeed");
let node_root = blob_dir.path().parent().unwrap();
let extract_dir = node_root
.join("applications")
.join("com.example.fallback")
.join("1.0.0")
.join("extracted");
let wasm_path = extract_dir.join("app.wasm");
assert!(wasm_path.exists(), "Extracted WASM should exist initially");
fs::remove_file(&wasm_path).expect("Should delete extracted WASM");
assert!(!wasm_path.exists(), "WASM should be deleted");
let bytes = node_client
.get_application_bytes(&application_id)
.await
.expect("Should get application bytes via fallback")
.expect("Application bytes should exist");
assert_eq!(
bytes.as_ref(),
wasm_content,
"Application bytes should match WASM content (re-extracted from bundle blob)"
);
assert!(
wasm_path.exists(),
"WASM file should exist after fallback (fallback extracts bundle to disk)"
);
}
#[tokio::test]
async fn test_get_latest_version_semantic_ordering() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let package = "com.example.versioning";
let versions = vec!["1.0.0", "2.0.0", "10.0.0", "1.5.0", "1.10.0", "2.5.0"];
let mut application_ids = Vec::new();
for version in &versions {
let bundle_path =
create_test_bundle(&temp_dir, package, version, b"wasm content", None, vec![]);
let app_id = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await
.expect("Bundle installation should succeed");
application_ids.push((version.to_string(), app_id));
}
let (latest_version_str, _latest_app_id) = node_client
.get_latest_version(package)
.expect("Should get latest version")
.expect("Latest version should exist");
assert_eq!(
latest_version_str, "10.0.0",
"Latest version should be 10.0.0 (semantic), not 2.5.0 (lexicographic)"
);
}
#[tokio::test]
async fn test_get_latest_version_mixed_semver_and_non_semver() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let package = "com.example.mixed";
let versions = vec!["1.0.0", "invalid-version", "2.0.0", "also-invalid"];
let mut application_ids = Vec::new();
for version in &versions {
let bundle_path =
create_test_bundle(&temp_dir, package, version, b"wasm content", None, vec![]);
let app_id = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await
.expect("Bundle installation should succeed");
application_ids.push((version.to_string(), app_id));
}
let (latest_version_str, _latest_app_id) = node_client
.get_latest_version(package)
.expect("Should get latest version")
.expect("Latest version should exist");
assert_eq!(
latest_version_str, "2.0.0",
"Latest version should be 2.0.0 (semantic), preferring semantic versions over non-semantic"
);
}
fn create_bundle_with_custom_manifest(
temp_dir: &TempDir,
manifest_json: serde_json::Value,
wasm_content: &[u8],
) -> Utf8PathBuf {
let bundle_path = temp_dir.path().join("malicious-bundle.mpk");
let bundle_file = fs::File::create(&bundle_path).unwrap();
let encoder = GzEncoder::new(bundle_file, Compression::default());
let mut tar = Builder::new(encoder);
let manifest_bytes = serde_json::to_vec(&manifest_json).unwrap();
let mut manifest_header = tar::Header::new_gnu();
manifest_header.set_path("manifest.json").unwrap();
manifest_header.set_size(manifest_bytes.len() as u64);
manifest_header.set_cksum();
tar.append(&manifest_header, manifest_bytes.as_slice())
.unwrap();
let mut wasm_header = tar::Header::new_gnu();
wasm_header.set_path("app.wasm").unwrap();
wasm_header.set_size(wasm_content.len() as u64);
wasm_header.set_cksum();
tar.append(&wasm_header, wasm_content).unwrap();
tar.finish().unwrap();
bundle_path.try_into().unwrap()
}
#[tokio::test]
async fn test_bundle_validation_path_traversal_in_package() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let signing_key = SigningKey::generate(&mut OsRng);
let signer_id = derive_signer_id_did_key(signing_key.verifying_key().as_bytes());
let mut manifest = serde_json::json!({
"version": "1.0",
"package": "../../../etc/malicious", "appVersion": "1.0.0",
"signerId": signer_id,
"minRuntimeVersion": "0.1.0",
"wasm": {
"path": "app.wasm",
"size": 12
},
"migrations": []
});
sign_manifest(&mut manifest, &signing_key);
let bundle_path = create_bundle_with_custom_manifest(&temp_dir, manifest, b"wasm content");
let result = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await;
assert!(
result.is_err(),
"Bundle installation should fail with path traversal in package"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("path traversal") || error_msg.contains("'..'"),
"Error should mention path traversal, got: {}",
error_msg
);
}
#[tokio::test]
async fn test_bundle_validation_path_traversal_in_version() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let signing_key = SigningKey::generate(&mut OsRng);
let signer_id = derive_signer_id_did_key(signing_key.verifying_key().as_bytes());
let mut manifest = serde_json::json!({
"version": "1.0",
"package": "com.example.test",
"appVersion": "../../../tmp/malicious", "signerId": signer_id,
"minRuntimeVersion": "0.1.0",
"wasm": {
"path": "app.wasm",
"size": 12
},
"migrations": []
});
sign_manifest(&mut manifest, &signing_key);
let bundle_path = create_bundle_with_custom_manifest(&temp_dir, manifest, b"wasm content");
let result = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await;
assert!(
result.is_err(),
"Bundle installation should fail with path traversal in appVersion"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("path traversal") || error_msg.contains("'..'"),
"Error should mention path traversal, got: {}",
error_msg
);
}
#[tokio::test]
async fn test_bundle_validation_forward_slash_in_package() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let signing_key = SigningKey::generate(&mut OsRng);
let signer_id = derive_signer_id_did_key(signing_key.verifying_key().as_bytes());
let mut manifest = serde_json::json!({
"version": "1.0",
"package": "foo/bar", "appVersion": "1.0.0",
"signerId": signer_id,
"minRuntimeVersion": "0.1.0",
"wasm": {
"path": "app.wasm",
"size": 12
},
"migrations": []
});
sign_manifest(&mut manifest, &signing_key);
let bundle_path = create_bundle_with_custom_manifest(&temp_dir, manifest, b"wasm content");
let result = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await;
assert!(
result.is_err(),
"Bundle installation should fail with forward slash in package"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("directory separator") || error_msg.contains("package"),
"Error should mention directory separator, got: {}",
error_msg
);
}
#[tokio::test]
async fn test_bundle_validation_backslash_in_package() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let signing_key = SigningKey::generate(&mut OsRng);
let signer_id = derive_signer_id_did_key(signing_key.verifying_key().as_bytes());
let mut manifest = serde_json::json!({
"version": "1.0",
"package": "foo\\bar", "appVersion": "1.0.0",
"signerId": signer_id,
"minRuntimeVersion": "0.1.0",
"wasm": {
"path": "app.wasm",
"size": 12
},
"migrations": []
});
sign_manifest(&mut manifest, &signing_key);
let bundle_path = create_bundle_with_custom_manifest(&temp_dir, manifest, b"wasm content");
let result = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await;
assert!(
result.is_err(),
"Bundle installation should fail with backslash in package"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("directory separator") || error_msg.contains("package"),
"Error should mention directory separator, got: {}",
error_msg
);
}
#[tokio::test]
async fn test_bundle_validation_windows_absolute_path_in_package() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let signing_key = SigningKey::generate(&mut OsRng);
let signer_id = derive_signer_id_did_key(signing_key.verifying_key().as_bytes());
let mut manifest = serde_json::json!({
"version": "1.0",
"package": "C:malicious", "appVersion": "1.0.0",
"signerId": signer_id,
"minRuntimeVersion": "0.1.0",
"wasm": {
"path": "app.wasm",
"size": 12
},
"migrations": []
});
sign_manifest(&mut manifest, &signing_key);
let bundle_path = create_bundle_with_custom_manifest(&temp_dir, manifest, b"wasm content");
let result = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await;
assert!(
result.is_err(),
"Bundle installation should fail with Windows absolute path in package"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("absolute path") || error_msg.contains("package"),
"Error should mention absolute path, got: {}",
error_msg
);
}
#[tokio::test]
async fn test_bundle_validation_valid_package_names() {
let temp_dir = TempDir::new().unwrap();
let (node_client, _data_dir, _blob_dir) = create_test_node_client(None).await;
let valid_packages = vec![
"com.example.myapp",
"my-app",
"my_app_v2",
"MyApp",
"app123",
"com.calimero.kv-store",
];
for (i, package) in valid_packages.iter().enumerate() {
let bundle_path = create_test_bundle(
&temp_dir,
package,
&format!("1.0.{}", i), b"wasm content",
None,
vec![],
);
let result = node_client
.install_application_from_path(bundle_path, vec![], None, None)
.await;
assert!(
result.is_ok(),
"Bundle with valid package '{}' should install successfully, got error: {:?}",
package,
result.err()
);
}
}