362 lines
13 KiB
Rust
362 lines
13 KiB
Rust
//! 使用自有 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<String, Vec<serde_json::Value>>,
|
|
downloads: BTreeMap<String, Vec<u8>>,
|
|
cache: RefCell<BTreeMap<String, Vec<u8>>>,
|
|
requests: RefCell<Vec<String>>,
|
|
}
|
|
|
|
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::<BTreeMap<_, _>>(), "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"<h1>binary \0 resource</h1>".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<Vec<u8>, 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<Option<Vec<u8>>, 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"<h1>binary \0 resource</h1>");
|
|
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");
|
|
}
|