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::(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" ); } }