Files
networkSetup/src/crates/package-infra/tests/workflow.rs
T

314 lines
12 KiB
Rust

use package_domain::Project;
use package_engine::PackageBackend;
use package_infra::{self as infra, MsixBackend};
use std::{
fs,
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
fn png(path: &Path, size: u32) {
let file = fs::File::create(path).unwrap();
let mut encoder = png::Encoder::new(file, size, size);
encoder.set_color(png::ColorType::Rgba);
let mut writer = encoder.write_header().unwrap();
writer
.write_image_data(&[50, 100, 220, 255].repeat((size * size) as usize))
.unwrap();
}
fn fixture() -> (tempfile::TempDir, PathBuf) {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("InstallerProject.isproj");
let mut doc = infra::create_project(&path).unwrap();
let payload = temp.path().join("payload");
fs::create_dir_all(payload.join("Assets")).unwrap();
let mut pe = vec![0; 256];
pe[..2].copy_from_slice(b"MZ");
pe[60] = 64;
pe[64..68].copy_from_slice(b"PE\0\0");
pe[68..70].copy_from_slice(&0x8664u16.to_le_bytes());
fs::write(payload.join("SampleTool.exe"), pe).unwrap();
png(&payload.join("Assets/Logo44.png"), 44);
png(&payload.join("Assets/Logo150.png"), 150);
fs::write(payload.join("ignore.pdb"), "debug").unwrap();
doc.project.files[0].exclude = vec!["**/*.pdb".into()];
doc.local
.sources
.insert("appPayload".into(), "payload".into());
infra::save_project(&doc).unwrap();
(temp, path)
}
#[test]
fn deterministic_resolution_and_exclusions() {
let (_temp, path) = fixture();
let doc = infra::open_project(&path).unwrap();
let first = MsixBackend
.resolve(&doc.project, &doc.local, doc.path.parent().unwrap())
.unwrap();
let second = MsixBackend
.resolve(&doc.project, &doc.local, doc.path.parent().unwrap())
.unwrap();
assert_eq!(first, second);
assert_eq!(first.len(), 3);
assert_eq!(first[0].target, "Assets/Logo150.png");
}
#[test]
fn signing_defaults_and_local_selection_are_backward_compatible() {
use package_domain::SigningMode;
let (_temp, path) = fixture();
let mut json: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
json.as_object_mut().unwrap().remove("signing");
fs::write(&path, serde_json::to_vec_pretty(&json).unwrap()).unwrap();
let mut doc = infra::open_project(&path).unwrap();
assert_eq!(doc.project.signing.mode, SigningMode::Unsigned);
assert!(doc.local.developer_certificate_thumbprint.is_none());
doc.project.signing.mode = SigningMode::Development;
doc.local.developer_certificate_thumbprint = Some("A".repeat(40));
let saved = infra::save_project(&doc).unwrap();
assert_eq!(saved.project.signing.mode, SigningMode::Development);
assert_eq!(
saved.local.developer_certificate_thumbprint,
Some("A".repeat(40))
);
assert!(!fs::read_to_string(&path).unwrap().contains(&"A".repeat(40)));
json["signing"] = serde_json::json!({"mode":"production"});
assert!(serde_json::from_value::<Project>(json).is_err());
}
#[test]
fn development_signing_requires_explicit_certificate_without_unsigned_fallback() {
use package_domain::SigningMode;
let (_temp, path) = fixture();
let mut doc = infra::open_project(&path).unwrap();
assert!(MsixBackend
.signing_plan(&doc.project, &doc.local)
.unwrap()
.is_none());
doc.project.signing.mode = SigningMode::Development;
assert!(MsixBackend
.signing_plan(&doc.project, &doc.local)
.unwrap_err()
.to_string()
.contains("MissingDeveloperCertificate"));
doc.local.developer_certificate_thumbprint = Some("not-a-thumbprint".into());
assert!(MsixBackend
.signing_plan(&doc.project, &doc.local)
.unwrap_err()
.to_string()
.contains("InvalidCertificateThumbprint"));
}
#[test]
fn save_rejects_stale_revision_and_preserves_backup() {
let (_temp, path) = fixture();
let mut stale = infra::open_project(&path).unwrap();
let mut edited = stale.clone();
edited.project.product.display_name = "new name".into();
infra::save_project(&edited).unwrap();
stale.project.product.display_name = "old editor".into();
assert!(infra::save_project(&stale)
.unwrap_err()
.to_string()
.contains("RevisionConflict"));
let backup: Project =
serde_json::from_slice(&fs::read(infra::appended_path(&path, ".bak")).unwrap()).unwrap();
assert_ne!(backup.product.display_name, "new name");
}
#[test]
fn collision_missing_asset_and_bad_pe_are_rejected() {
let (temp, path) = fixture();
let mut doc = infra::open_project(&path).unwrap();
let mut duplicate = doc.project.files[0].clone();
duplicate.id = "other".into();
doc.project.files.push(duplicate);
assert!(MsixBackend
.resolve(&doc.project, &doc.local, doc.path.parent().unwrap())
.unwrap_err()
.to_string()
.contains("Collision"));
doc.project.files.pop();
png(&temp.path().join("payload/Assets/Logo44.png"), 45);
assert!(MsixBackend
.resolve(&doc.project, &doc.local, doc.path.parent().unwrap())
.unwrap_err()
.to_string()
.contains("InvalidAssetSize"));
fs::write(
temp.path().join("payload/SampleTool.exe"),
"not an executable",
)
.unwrap();
assert!(MsixBackend
.resolve(&doc.project, &doc.local, doc.path.parent().unwrap())
.unwrap_err()
.to_string()
.contains("InvalidPE"));
}
#[test]
fn source_may_not_contain_work_or_release_outputs() {
let (_temp, path) = fixture();
let mut doc = infra::open_project(&path).unwrap();
doc.local.sources.insert("appPayload".into(), ".".into());
assert!(MsixBackend
.resolve(&doc.project, &doc.local, doc.path.parent().unwrap())
.unwrap_err()
.to_string()
.contains("OutputInsideSource"));
}
#[test]
fn project_lock_blocks_concurrent_writers() {
let (_temp, path) = fixture();
let doc = infra::open_project(&path).unwrap();
let _lock = infra::lock_project(path.parent().unwrap()).unwrap();
assert!(infra::save_project(&doc)
.unwrap_err()
.to_string()
.contains("JobLocked"));
}
#[test]
fn registry_edits_round_trip_and_old_projects_default_to_empty() {
let (_temp, path) = fixture();
let mut value: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
value.as_object_mut().unwrap().remove("registry");
fs::write(&path, serde_json::to_vec(&value).unwrap()).unwrap();
let mut doc = infra::open_project(&path).unwrap();
assert!(doc.project.registry.is_empty());
doc.project.registry = serde_json::from_value(serde_json::json!([
{"id":"max", "hive":"HKLM", "key":"Software\\Baron", "name":"Maximum", "view":"64", "valueType":"REG_QWORD", "data":"18446744073709551615"}
])).unwrap();
let mut saved = infra::save_project(&doc).unwrap();
assert_eq!(saved.project.registry[0].data, "18446744073709551615");
assert_ne!(saved.revision, doc.revision);
saved.project.registry.clear();
infra::save_project(&saved).unwrap();
assert!(infra::open_project(&path)
.unwrap()
.project
.registry
.is_empty());
}
#[test]
fn source_cannot_override_generated_registry_hives() {
for relative in [
"Registry.dat",
"registry.dat/nested.txt",
"User.dat",
"User.Classes.dat",
] {
let (temp, path) = fixture();
let file = temp.path().join("payload").join(relative);
fs::create_dir_all(file.parent().unwrap()).unwrap();
fs::write(&file, "untrusted hive").unwrap();
let doc = infra::open_project(&path).unwrap();
assert!(MsixBackend
.resolve(&doc.project, &doc.local, path.parent().unwrap())
.unwrap_err()
.to_string()
.contains("ReservedPath"));
}
}
#[test]
fn opening_unsupported_schema_does_not_modify_original() {
let (_temp, path) = fixture();
let mut value: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
value["schemaVersion"] = serde_json::json!(1);
let original = serde_json::to_vec(&value).unwrap();
fs::write(&path, &original).unwrap();
assert!(infra::open_project(&path)
.unwrap_err()
.to_string()
.contains("UnsupportedSchema"));
assert_eq!(fs::read(path).unwrap(), original);
}
#[test]
fn schema_file_matches_rust_contract() {
// The CLI emits this same schema; this catches a stale checked-in schema.
let actual = serde_json::to_value(schemars::schema_for!(Project)).unwrap();
let expected: serde_json::Value =
serde_json::from_str(include_str!("../../../schemas/package-project.schema.json")).unwrap();
assert_eq!(actual, expected);
}
#[test]
#[ignore = "Requires a real Windows SDK: set MSIX_TEST_MAKEAPPX and run --ignored"]
fn real_sdk_packages_two_products_and_rejects_stale_plans() {
let tool = std::env::var("MSIX_TEST_MAKEAPPX").expect("MSIX_TEST_MAKEAPPX is required");
for name in ["FixtureAlpha", "FixtureBeta"] {
let (temp, path) = fixture();
let mut doc = infra::open_project(&path).unwrap();
doc.project.formats.msix.identity.name = format!("ExamplePublisher.{name}");
doc.project.product.display_name = name.into();
doc.local.make_appx = Some(tool.clone());
doc.project.registry = serde_json::from_value(serde_json::json!([
{"id":"caption", "hive":"HKLM", "key":"Software\\MSIXStudioTest", "name":"", "view":"64", "valueType":"REG_SZ", "data":"한글 기본값"},
{"id":"mode", "hive":"HKLM", "key":"Software\\MSIXStudioTest", "name":"Mode", "view":"32", "valueType":"REG_DWORD", "data":"0xFFFFFFFF"}
])).unwrap();
// A real compiler-produced PE; this checks packaging, not app installation or launch.
fs::copy(
std::env::current_exe().unwrap(),
temp.path().join("payload/SampleTool.exe"),
)
.unwrap();
infra::save_project(&doc).unwrap();
let plan = infra::plan_release(&path, "msix-direct").unwrap();
fs::write(temp.path().join("payload/changed.txt"), "changed").unwrap();
assert!(infra::build(&path, &plan.id, &AtomicBool::new(false))
.unwrap_err()
.to_string()
.contains("StalePlan"));
let plan = infra::plan_release(&path, "msix-direct").unwrap();
let result = infra::build(&path, &plan.id, &AtomicBool::new(false)).unwrap();
let registry_report: serde_json::Value = serde_json::from_slice(
&fs::read(
result
.artifact
.parent()
.unwrap()
.join("registry-report.json"),
)
.unwrap(),
)
.unwrap();
assert_eq!(registry_report["entryCount"], 2);
assert_eq!(registry_report["status"], "verified");
assert!(temp
.path()
.join(".work/jobs")
.join(&plan.id)
.join("inspection/registry.dat")
.is_file());
assert_eq!(result.status, "ArtifactVerified");
assert!(!result.deployment_ready);
assert_eq!(result.installation_test, "notRun");
assert_eq!(infra::hash_file(&result.artifact).unwrap(), result.sha256);
assert!(infra::build(&path, &plan.id, &AtomicBool::new(false))
.unwrap_err()
.to_string()
.contains("DuplicateBuild"));
let export = temp.path().join("exported");
infra::export_release(&path, &plan.id, &export).unwrap();
assert!(export.join("registry-report.json").is_file());
assert!(infra::export_release(&path, &plan.id, &export).is_err());
fs::write(result.artifact, "tamper").unwrap();
assert!(
infra::export_release(&path, &plan.id, &temp.path().join("tampered"))
.unwrap_err()
.to_string()
.contains("ArtifactMismatch")
);
let cancel_plan = infra::plan_release(&path, "msix-direct").unwrap();
assert!(infra::build(&path, &cancel_plan.id, &AtomicBool::new(true))
.unwrap_err()
.to_string()
.contains("Cancelled"));
assert_eq!(
infra::job_status(&path, &cancel_plan.id).unwrap().status,
"Cancelled"
);
}
}