use std::path::{Path, PathBuf};
use anyhow::Context;
use syncweb_core::{
folder::{CollectionEntry, CollectionManifest, PackageManager},
storage::node_db::NodeDatabase,
};
use crate::test_utils::TestDirectory;
fn make_source(dir: &Path, name: &str, data: &[u8]) -> anyhow::Result<PathBuf> {
let path = dir.join(name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, data)?;
Ok(path)
}
fn make_manifest(
collection_id: uuid::Uuid,
version: &str,
files: &[(&str, &[u8])],
) -> anyhow::Result<CollectionManifest> {
let mut manifest = CollectionManifest::new(collection_id, version);
for (name, data) in files {
manifest.entries.push(CollectionEntry::new(
iroh_blobs::Hash::new(data),
PathBuf::from(name),
u64::try_from(data.len())?,
)?);
}
Ok(manifest)
}
fn write_source_files(base: &Path, files: &[(&str, &[u8])]) -> anyhow::Result<PathBuf> {
let source = base.join(format!("source-{}", uuid::Uuid::new_v4()));
for (name, data) in files {
make_source(&source, name, data)?;
}
Ok(source)
}
#[tokio::test]
async fn test_package_lifecycle() -> anyhow::Result<()> {
let directory = TestDirectory::new("syncweb-package-test")?;
let collection_id = uuid::Uuid::new_v4();
let node_db = NodeDatabase::open(directory.path().join("node.db"))?;
let packages = PackageManager::new(directory.path().join("packages"), node_db);
let v1_tool: &[u8] = b"tool-v1";
let v1_readme: &[u8] = b"readme v1";
let v1_files = &[("bin/tool", v1_tool), ("README.md", v1_readme)];
let v1 = make_manifest(collection_id, "1.0.0", v1_files)?;
let source_v1 = write_source_files(directory.path(), v1_files)?;
packages.install(&v1, &source_v1)?;
let v1_state = packages.state()?;
let v1_installed = v1_state.current(collection_id).context("v1 should be installed")?;
anyhow::ensure!(v1_installed.current == "1.0.0");
anyhow::ensure!(v1_installed.versions.contains_key("1.0.0"));
let v2_tool: &[u8] = b"tool-v2";
let v2_readme: &[u8] = b"readme v2";
let v2_files = &[("bin/tool", v2_tool), ("README.md", v2_readme)];
let v2 = make_manifest(collection_id, "2.0.0", v2_files)?;
let source_v2 = write_source_files(directory.path(), v2_files)?;
packages.install(&v2, &source_v2)?;
let v2_state = packages.state()?;
let v2_installed = v2_state.current(collection_id).context("v2 should be current")?;
anyhow::ensure!(v2_installed.current == "2.0.0");
anyhow::ensure!(v2_installed.versions.contains_key("1.0.0"));
anyhow::ensure!(v2_installed.versions.contains_key("2.0.0"));
packages.switch(collection_id, "1.0.0")?;
let current_path = packages.root().join(collection_id.to_string()).join("current/bin/tool");
anyhow::ensure!(std::fs::read(current_path)? == b"tool-v1");
packages.verify(&v1)?;
packages.verify(&v2)?;
packages.switch(collection_id, "2.0.0")?;
packages.remove(collection_id, "1.0.0")?;
let final_state = packages.state()?;
let final_installed = final_state
.current(collection_id)
.context("collection should still exist")?;
anyhow::ensure!(!final_installed.versions.contains_key("1.0.0"));
anyhow::ensure!(final_installed.versions.contains_key("2.0.0"));
anyhow::ensure!(final_installed.current == "2.0.0");
packages.verify(&v2)?;
Ok(())
}
#[test]
fn test_multi_version_coexistence() -> anyhow::Result<()> {
let directory = TestDirectory::new("syncweb-package-test")?;
let collection_id = uuid::Uuid::new_v4();
let node_db = NodeDatabase::open(directory.path().join("node.db"))?;
let packages = PackageManager::new(directory.path().join("packages"), node_db);
let v1_content: &[u8] = b"content-v1";
let v1_files = &[("data/file.txt", v1_content)];
let v1 = make_manifest(collection_id, "1.0.0", v1_files)?;
let source_v1 = write_source_files(directory.path(), v1_files)?;
packages.install(&v1, &source_v1)?;
let v2_content: &[u8] = b"content-v2";
let v2_files = &[("data/file.txt", v2_content)];
let v2 = make_manifest(collection_id, "2.0.0", v2_files)?;
let source_v2 = write_source_files(directory.path(), v2_files)?;
packages.install(&v2, &source_v2)?;
let collection_dir = packages.root().join(collection_id.to_string());
anyhow::ensure!(collection_dir.join("1.0.0").join("data/file.txt").exists());
anyhow::ensure!(collection_dir.join("2.0.0").join("data/file.txt").exists());
let initial_link = std::fs::read_link(collection_dir.join("current"))?;
anyhow::ensure!(initial_link == Path::new("2.0.0"));
packages.switch(collection_id, "1.0.0")?;
let v1_link = std::fs::read_link(collection_dir.join("current"))?;
anyhow::ensure!(v1_link == Path::new("1.0.0"));
anyhow::ensure!(std::fs::read(collection_dir.join("current/data/file.txt"))? == b"content-v1");
packages.switch(collection_id, "2.0.0")?;
let v2_link = std::fs::read_link(collection_dir.join("current"))?;
anyhow::ensure!(v2_link == Path::new("2.0.0"));
anyhow::ensure!(std::fs::read(collection_dir.join("current/data/file.txt"))? == b"content-v2");
let state = packages.state()?;
let installed = state.current(collection_id).context("should have collection")?;
anyhow::ensure!(installed.versions.len() == 2);
anyhow::ensure!(installed.current == "2.0.0");
Ok(())
}
#[test]
fn test_atomic_upgrade() -> anyhow::Result<()> {
let directory = TestDirectory::new("syncweb-package-test")?;
let collection_id = uuid::Uuid::new_v4();
let node_db = NodeDatabase::open(directory.path().join("node.db"))?;
let packages = PackageManager::new(directory.path().join("packages"), node_db);
let v1_data: &[u8] = b"v1-binary";
let v1_files = &[("app.bin", v1_data)];
let v1 = make_manifest(collection_id, "1.0.0", v1_files)?;
let source_v1 = write_source_files(directory.path(), v1_files)?;
packages.install(&v1, &source_v1)?;
let collection_dir = packages.root().join(collection_id.to_string());
anyhow::ensure!(std::fs::read(collection_dir.join("current/app.bin"))? == b"v1-binary");
anyhow::ensure!(std::fs::read_link(collection_dir.join("current"))? == Path::new("1.0.0"));
let v2_data: &[u8] = b"v2-binary-upgraded";
let v2_files = &[("app.bin", v2_data)];
let v2 = make_manifest(collection_id, "2.0.0", v2_files)?;
let source_v2 = write_source_files(directory.path(), v2_files)?;
packages.install(&v2, &source_v2)?;
anyhow::ensure!(std::fs::read_link(collection_dir.join("current"))? == Path::new("2.0.0"));
anyhow::ensure!(std::fs::read(collection_dir.join("current/app.bin"))? == b"v2-binary-upgraded");
let staging_artifacts: Vec<_> = std::fs::read_dir(&collection_dir)?
.filter_map(std::result::Result::ok)
.filter(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| name.starts_with(".stage-") || name.starts_with(".current-"))
})
.collect();
anyhow::ensure!(
staging_artifacts.is_empty(),
"staging artifacts should be cleaned up: {staging_artifacts:?}"
);
let state = packages.state()?;
let installed = state.current(collection_id).context("should exist")?;
anyhow::ensure!(installed.current == "2.0.0");
anyhow::ensure!(installed.versions.len() == 2);
anyhow::ensure!(std::fs::read(collection_dir.join("1.0.0/app.bin"))? == b"v1-binary");
Ok(())
}
#[test]
fn test_package_integrity() -> anyhow::Result<()> {
let directory = TestDirectory::new("syncweb-package-test")?;
let collection_id = uuid::Uuid::new_v4();
let node_db = NodeDatabase::open(directory.path().join("node.db"))?;
let packages = PackageManager::new(directory.path().join("packages"), node_db);
let original: &[u8] = b"original content";
let files = &[("data.txt", original)];
let manifest = make_manifest(collection_id, "1.0.0", files)?;
let source = write_source_files(directory.path(), files)?;
packages.install(&manifest, &source)?;
packages.verify(&manifest)?;
let file_path = packages.root().join(collection_id.to_string()).join("1.0.0/data.txt");
std::fs::write(&file_path, b"corrupted content")?;
let err = packages
.verify(&manifest)
.expect_err("verify should fail after corruption");
anyhow::ensure!(
format!("{err}").contains("does not match manifest"),
"unexpected error: {err}"
);
let wrong_manifest = make_manifest(collection_id, "9.9.9", files)?;
anyhow::ensure!(packages.verify(&wrong_manifest).is_err());
let missing_manifest = make_manifest(uuid::Uuid::new_v4(), "1.0.0", files)?;
anyhow::ensure!(packages.verify(&missing_manifest).is_err());
Ok(())
}