//! 使用自有 registry 和缓存验证完整命令;不访问生产 daemon 或真实 registry。 use super::fixture::digest; use super::{Fixture, app, command, write}; use dever::compiler::project::{ Project, dependency::{self, Registry, Store}, }; use std::cell::RefCell; use std::collections::BTreeMap; use std::io::{Cursor, Write}; #[derive(Default)] pub(super) struct Services { indexes: BTreeMap>, downloads: BTreeMap>, cache: RefCell>>, requests: RefCell>, } impl Services { pub(super) fn release(&mut self, name: &str, version: &str, dependencies: &[(&str, &str)]) { let manifest = serde_json::to_vec(&serde_json::json!({ "format":"dever-package-v1", "name":name, "version":version, "dependencies":dependencies.iter().copied().collect::>(), "lib":[] })) .unwrap(); let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new())); for (path, bytes) in [ ("dever-package.json".into(), manifest), ( format!("module/{name}/value/app.dever"), app("input + 1").into_bytes(), ), ( format!("module/{name}/assets/home.html"), b"

binary \0 resource

".to_vec(), ), ] { zip.start_file(path, zip::write::SimpleFileOptions::default()) .unwrap(); zip.write_all(&bytes).unwrap(); } let bytes = zip.finish().unwrap().into_inner(); self.indexes.entry(name.into()).or_default().push( serde_json::json!({ "version":version,"sha256":digest(&bytes),"bytes":bytes.len() }), ); self.downloads .insert(format!("/v1/packages/{name}/{version}.zip"), bytes); } } impl Registry for Services { fn get(&self, path: &str, limit: usize) -> Result, String> { self.requests.borrow_mut().push(path.into()); let bytes = if let Some(name) = path .strip_prefix("/v1/packages/") .and_then(|path| path.strip_suffix("/index.json")) { serde_json::to_vec(&serde_json::json!({"format":"dever-package-index-v1","name":name,"versions":self.indexes.get(name).ok_or("missing test index")?})).unwrap() } else { self.downloads .get(path) .ok_or("missing test archive")? .clone() }; assert!(bytes.len() <= limit); Ok(bytes) } } impl Store for Services { fn get(&self, sha256: &str, _bytes: u64) -> Result>, String> { Ok(self.cache.borrow().get(sha256).cloned()) } fn put(&self, bytes: &[u8]) -> Result<(), String> { self.cache .borrow_mut() .insert(digest(bytes), bytes.to_vec()); Ok(()) } } fn versions(root: &std::path::Path) -> Vec<(String, String)> { let lock: serde_json::Value = serde_json::from_slice(&std::fs::read(root.join("dever.lock")).unwrap()).unwrap(); lock["packages"].as_array().map_or(vec![], |packages| { packages .iter() .map(|package| { ( package["name"].as_str().unwrap().into(), package["version"].as_str().unwrap().into(), ) }) .collect() }) } #[test] fn add_backtracks_then_update_and_remove_keep_a_valid_offline_closure() { let fixture = Fixture::new(&[]); let root = &fixture.directory.0; let mut services = Services::default(); services.release("alpha", "1.0.0", &[("common_dep", "^1")]); services.release("alpha", "2.0.0", &[("common_dep", "^2")]); services.release("beta", "1.0.0", &[("common_dep", "^1")]); services.release("common_dep", "1.0.0", &[]); services.release("common_dep", "2.0.0", &[]); dependency::execute_with( "add", root, &["alpha@*".into(), "beta@^1".into()], &services, &services, ) .unwrap(); assert_eq!( versions(root), vec![ ("alpha".into(), "1.0.0".into()), ("beta".into(), "1.0.0".into()), ("common_dep".into(), "1.0.0".into()) ] ); services.requests.borrow_mut().clear(); dependency::execute_with("remove", root, &["beta".into()], &services, &services).unwrap(); assert!(services.requests.borrow().is_empty()); assert_eq!(versions(root)[0].1, "1.0.0"); dependency::execute_with("update", root, &[], &services, &services).unwrap(); assert_eq!(versions(root)[0].1, "2.0.0"); dependency::execute_with("doctor", root, &[], &services, &services).unwrap(); write( root, "module/demo/value/app.dever", app("alpha.value.value(common_dep.value.value(input))"), ); write(root, "module/demo/value/api.dever", command()); Project::load_with_artifacts(root, |sha256, bytes| { Store::get(&services, sha256, bytes).map(Option::unwrap) }) .unwrap() .check() .unwrap(); } #[test] fn restore_uses_exact_archives_without_resolving_or_rewriting_and_export_preserves_bytes() { let fixture = Fixture::new(&[]); let root = &fixture.directory.0; let mut services = Services::default(); services.release("alpha", "1.0.0", &[]); dependency::execute_with("add", root, &["alpha@^1".into()], &services, &services).unwrap(); let lock = std::fs::read(root.join("dever.lock")).unwrap(); let setting = std::fs::read(root.join("config/setting.json")).unwrap(); services.cache.borrow_mut().clear(); services.requests.borrow_mut().clear(); services.indexes.clear(); dependency::install_with(root, &services, &services).unwrap(); assert_eq!( &*services.requests.borrow(), &["/v1/packages/alpha/1.0.0.zip"] ); assert_eq!(std::fs::read(root.join("dever.lock")).unwrap(), lock); assert_eq!( std::fs::read(root.join("config/setting.json")).unwrap(), setting ); services.requests.borrow_mut().clear(); let output = root.join("export"); let report = dependency::export_with(root, "alpha", "assets", &output, &services).unwrap(); let report: serde_json::Value = serde_json::from_str(&report).unwrap(); let bytes = std::fs::read(output.join("home.html")).unwrap(); assert_eq!(bytes, b"

binary \0 resource

"); assert_eq!(report["files"][0]["sha256"], digest(&bytes)); assert!( dependency::export_with(root, "alpha", "assets", &output, &services) .unwrap_err() .contains("already exists") ); assert!(services.requests.borrow().is_empty()); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; assert_eq!( std::fs::metadata(&output).unwrap().permissions().mode() & 0o777, 0o700 ); assert_eq!( std::fs::metadata(output.join("home.html")) .unwrap() .permissions() .mode() & 0o777, 0o600 ); } } #[test] fn resolution_integrity_and_collision_errors_preserve_both_project_inputs() { for failure in ["conflict", "corrupt", "cycle", "collision"] { let fixture = Fixture::new(&[]); let root = &fixture.directory.0; let mut services = Services::default(); services.release("alpha", "1.0.0", &[]); dependency::execute_with("add", root, &["alpha@^1".into()], &services, &services).unwrap(); let setting = std::fs::read(root.join("config/setting.json")).unwrap(); let lock = std::fs::read(root.join("dever.lock")).unwrap(); services.release( "beta", "1.0.0", if failure == "cycle" { &[("beta", "^1")] } else if failure == "conflict" { &[("alpha", "^2")] } else { &[] }, ); if failure == "corrupt" { services .downloads .get_mut("/v1/packages/beta/1.0.0.zip") .unwrap()[0] ^= 1; } if failure == "collision" { write(root, "module/beta/private.txt", "user content"); } assert!( dependency::execute_with("add", root, &["beta@^1".into()], &services, &services) .is_err(), "accepted {failure}" ); assert_eq!( std::fs::read(root.join("config/setting.json")).unwrap(), setting ); assert_eq!(std::fs::read(root.join("dever.lock")).unwrap(), lock); } } #[test] fn corrupt_cache_is_an_error_not_a_download_and_export_rejects_escaping_paths() { let fixture = Fixture::new(&[]); let root = &fixture.directory.0; let mut services = Services::default(); services.release("alpha", "1.0.0", &[]); dependency::execute_with("add", root, &["alpha@^1".into()], &services, &services).unwrap(); for relative in [ "../assets", "/assets", "assets//icons", "assets/.", "assets\\x", ] { assert!( dependency::export_with(root, "alpha", relative, &root.join("out"), &services).is_err() ); } services.requests.borrow_mut().clear(); services.cache.borrow_mut().values_mut().next().unwrap()[0] ^= 1; assert!( dependency::install_with(root, &services, &services) .unwrap_err() .contains("SHA-256") ); assert!(services.requests.borrow().is_empty()); } #[test] fn registry_transport_rejects_redirect_and_oversize_without_following() { use std::io::Read; for (reply, limit, success) in [ ( "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}", 2, true, ), ( "HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\n123", 2, false, ), ( "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:1/forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", 8, false, ), ] { let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); let origin = format!("http://{}", listener.local_addr().unwrap()); let worker = std::thread::spawn(move || { let (mut peer, _) = listener.accept().unwrap(); peer.set_read_timeout(Some(std::time::Duration::from_secs(5))) .unwrap(); let mut received = Vec::new(); let mut byte = [0]; while !received.ends_with(b"\r\n\r\n") { peer.read_exact(&mut byte).unwrap(); received.push(byte[0]); assert!(received.len() < 4096); } assert!(received.starts_with(b"GET /v1/packages/alpha/index.json HTTP/1.1\r\n")); peer.write_all(reply.as_bytes()).unwrap(); }); let registry = dependency::HttpRegistry::loopback(&origin).unwrap(); assert_eq!( registry.get("/v1/packages/alpha/index.json", limit).is_ok(), success ); worker.join().unwrap(); } assert!(dependency::HttpRegistry::new("http://127.0.0.1:1").is_err()); assert!(dependency::HttpRegistry::new("https://example.test/root").is_err()); assert!(dependency::HttpRegistry::new("https://secret@example.test").is_err()); } #[test] fn mutation_guard_rejects_busy_and_symlink_locks_without_changing_settings() { let fixture = Fixture::new(&[]); let root = &fixture.directory.0; let before = std::fs::read(root.join("config/setting.json")).unwrap(); let path = root.join(".dever-project.lock"); let lock = std::fs::File::create(&path).unwrap(); use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap(); lock.lock().unwrap(); let services = Services::default(); let error = dependency::execute_with("add", root, &["alpha@^1".into()], &services, &services) .unwrap_err(); assert!(error.contains("busy"), "{error}"); lock.unlock().unwrap(); drop(lock); std::fs::remove_file(&path).unwrap(); std::os::unix::fs::symlink("config/setting.json", &path).unwrap(); assert!( dependency::execute_with("add", root, &["alpha@^1".into()], &services, &services).is_err() ); assert_eq!( std::fs::read(root.join("config/setting.json")).unwrap(), before ); assert!(services.requests.borrow().is_empty()); } #[test] fn update_reconciles_changed_requests_and_preserves_unrelated_settings() { let fixture = Fixture::new(&[]); let root = &fixture.directory.0; let mut services = Services::default(); services.release("alpha", "1.0.0", &[]); services.release("alpha", "2.0.0", &[]); dependency::execute_with("add", root, &["alpha@^1".into()], &services, &services).unwrap(); write(root, "config/setting.json", br#"{"package":{"registry":"https://packages.example.test","use":["alpha@^2"]},"business":{"secret":"unchanged"}}"#); dependency::execute_with("update", root, &[], &services, &services).unwrap(); assert_eq!(versions(root)[0].1, "2.0.0"); let setting: serde_json::Value = serde_json::from_slice(&std::fs::read(root.join("config/setting.json")).unwrap()).unwrap(); assert_eq!(setting["business"]["secret"], "unchanged"); }