Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 81 additions & 4 deletions crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ struct BerryRedirectFixture {
/// The root `package.json` BEFORE the hosted rewrite (#404 option C
/// pins through its `resolutions`, so a revert restores both files).
registry_pkg: Vec<u8>,
/// The dependency is declared `"catalog:"` (#632): `.yarnrc.yml` keeps
/// the catalog in every checkout.
catalog: bool,
_server: MockServer,
}

Expand All @@ -298,6 +301,17 @@ async fn berry_hosted_project(
tag: &str,
tamper_served_tarball: bool,
driver: HostedDriver,
) -> Option<BerryRedirectFixture> {
berry_hosted_project_with(tag, tamper_served_tarball, driver, false).await
}

/// [`berry_hosted_project`], with the dependency declared through the
/// default yarn catalog when `catalog` is set (#632).
async fn berry_hosted_project_with(
tag: &str,
tamper_served_tarball: bool,
driver: HostedDriver,
catalog: bool,
) -> Option<BerryRedirectFixture> {
if !has_corepack_pm(yarn_berry()) {
skip!(
Expand All @@ -317,13 +331,17 @@ async fn berry_hosted_project(
std::fs::write(
proj.join("package.json"),
format!(
r#"{{"name":"redirect-berry-capstone","version":"0.0.0","private":true,"dependencies":{{"{DEP}":"{DEP_VERSION}"}}}}"#
r#"{{"name":"redirect-berry-capstone","version":"0.0.0","private":true,"dependencies":{{"{DEP}":"{}"}}}}"#,
if catalog { "catalog:" } else { DEP_VERSION }
),
)
.unwrap();
std::fs::write(
proj.join(".yarnrc.yml"),
"nodeLinker: node-modules\nenableGlobalCache: false\n",
format!(
"nodeLinker: node-modules\nenableGlobalCache: false\n{}",
catalog_yarnrc(catalog)
),
)
.unwrap();

Expand Down Expand Up @@ -580,6 +598,13 @@ async fn berry_hosted_project(
&& v.as_str() == Some(hosted_url.as_str()))),
"package.json must route {DEP} to the hosted tarball: {root_pkg}"
);
if catalog {
assert_eq!(
root_pkg["resolutions"][format!("{DEP}@catalog:")],
hosted_url.as_str(),
"#632: the catalog descriptor yarn matches is routed too: {root_pkg}"
);
}
assert!(
!lock.contains("__archiveUrl"),
"the hosted pin must not be an npm: locator; got:\n{lock}"
Expand Down Expand Up @@ -608,6 +633,7 @@ async fn berry_hosted_project(
host,
registry_lock,
registry_pkg,
catalog,
_server: server,
})
}
Expand All @@ -620,11 +646,21 @@ fn fresh_yarnrc(fx: &BerryRedirectFixture) -> String {
format!(
"nodeLinker: node-modules\nenableGlobalCache: false\n\
unsafeHttpWhitelist:\n - \"{}\"\n\
npmRegistryServer: \"http://127.0.0.1:1\"\n",
fx.host.split(':').next().unwrap_or("127.0.0.1")
npmRegistryServer: \"http://127.0.0.1:1\"\n{}",
fx.host.split(':').next().unwrap_or("127.0.0.1"),
catalog_yarnrc(fx.catalog)
)
}

/// The `.yarnrc.yml` default catalog of a `"catalog:"` fixture (#632).
fn catalog_yarnrc(catalog: bool) -> String {
if catalog {
format!("catalog:\n {DEP}: {DEP_VERSION}\n")
} else {
String::new()
}
}

/// Fresh dir with only the committable files, then `yarn install --immutable
/// --check-cache` offline-from-registry (the wiremock host is whitelisted for
/// http). The install runs with an npm registry token that yarn must apply to
Expand Down Expand Up @@ -790,6 +826,47 @@ async fn berry_redirect_fresh_checkout_installs_patched_bytes() {
hosted_manifestless_vex_matrix(&fx, HostedDriver::Scan);
}

/// #632: a dependency declared through a yarn catalog (`"catalog:"`, yarn
/// >= 4.10). Yarn matches `resolutions` before it expands the catalog, so a
/// pin routing only the expanded `npm:` descriptor left the fresh
/// `--immutable` install failing YN0028 (and a mutable one unpatched). The
/// fresh checkout must install the patched bytes from the hosted tarball.
#[tokio::test(flavor = "multi_thread")]
#[serial_test::serial]
async fn berry_redirect_catalog_dependency_fresh_checkout_installs() {
let release = yarn_berry().strip_prefix("yarn@").unwrap_or(yarn_berry());
let minor: Vec<u32> = release
.split('.')
.take(2)
.filter_map(|p| p.parse().ok())
.collect();
if minor.as_slice() < [4, 10].as_slice() {
// Not a skip of an available toolchain: catalogs do not exist before
// yarn 4.10, so there is nothing to exercise.
println!("SKIP berry catalog e2e: {release} predates yarn catalogs (4.10)");
return;
}
let Some(fx) = berry_hosted_project_with("catalog", false, HostedDriver::Scan, true).await
else {
return;
};

let (fresh, ci) = fresh_checkout_yarn_install(&fx);
assert!(
ci.status.success(),
"fresh-checkout `yarn install --immutable --check-cache` of a catalog dependency \
must succeed from the hosted patch tarball.\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&ci.stdout),
String::from_utf8_lossy(&ci.stderr),
);
let installed = std::fs::read(fresh.join("node_modules").join(DEP).join("index.js")).unwrap();
assert_eq!(
installed, fx.patched,
"fresh install of the catalog dependency must be the patched content"
);
assert_patch_host_got_no_auth(&fx).await;
}

/// get-driven hosted twin (v4.0): `get <uuid> --mode hosted --json --yes`
/// routes through the SAME hosted engine as `scan --mode hosted`, so the
/// berry chain must hold unchanged — including the `10c0` cacheKey bootstrap
Expand Down
88 changes: 88 additions & 0 deletions crates/socket-patch-cli/tests/in_process_redirect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,94 @@ async fn scan_redirect_rewrites_crlf_and_bom_yarn_berry_locks_and_rollback_resto
}

/// #404 upgrade path: a lock pinned by an earlier release carries the old
/// #632: a dependency declared through a yarn catalog (`"catalog:"`) is
/// matched by yarn's `resolutions` before the catalog is expanded, so the
/// hosted pin must also route `<name>@catalog:`; `rollback` must drop every
/// selector it wrote and restore the lock's expanded `npm:` key, leaving
/// package.json byte-identical to the pristine one.
#[tokio::test]
#[serial]
async fn yarn_berry_catalog_dependency_is_pinned_and_rolled_back() {
let server = MockServer::start().await;
mock_discovery(&server).await;
let hosted_url = HOSTED_URL.replace("http://patch.test", &server.uri());
mock_reference_with_berry_url(&server, &hosted_url).await;
mock_view(&server).await;
let tarball = upstream_tarball();
mock_npm_registry(
&server,
&vlt_hosted_common::sha512_sri(&tarball),
Some(tarball),
)
.await;

let tmp = tempfile::tempdir().unwrap();
write_berry_project(tmp.path());
let pkg_path = tmp.path().join("package.json");
std::fs::write(
&pkg_path,
format!(
"{{\n \"name\": \"consumer\",\n \"version\": \"0.0.0\",\n \
\"dependencies\": {{\n \"{NAME}\": \"catalog:\"\n }}\n}}\n"
),
)
.unwrap();
std::fs::write(
tmp.path().join(".yarnrc.yml"),
format!("nodeLinker: node-modules\ncatalog:\n {NAME}: ^{VERSION}\n"),
)
.unwrap();
let pristine_pkg = std::fs::read_to_string(&pkg_path).unwrap();
let lock_path = tmp.path().join("yarn.lock");
let pristine_lock = std::fs::read_to_string(&lock_path).unwrap();

let env = run_redirect_subprocess_with(
tmp.path(),
&server.uri(),
&["--patch-server-url", &server.uri()],
);
assert_eq!(env["redirect"]["redirected"], 1, "{env:#}");
assert!(warning_codes(&env).is_empty(), "{env:#}");
let pkg: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&pkg_path).unwrap()).unwrap();
assert_eq!(
pkg["resolutions"],
serde_json::json!({
format!("{NAME}@npm:^{VERSION}"): hosted_url,
format!("{NAME}@catalog:"): hosted_url,
}),
"{pkg}"
);
let lock = std::fs::read_to_string(&lock_path).unwrap();
assert!(
lock.contains(&format!("\"{NAME}@{hosted_url}\":")),
"the entry is keyed by the tarball descriptor: {lock}"
);

let (code, env) = rollback_json_with_origin(tmp.path(), &server, &server.uri());
assert_eq!(code, Some(0), "rollback: {env:#}");
assert_eq!(
env["hosted"]["reverted"],
serde_json::json!([PURL]),
"{env:#}"
);
assert_eq!(
std::fs::read_to_string(&pkg_path).unwrap(),
pristine_pkg,
"rollback drops both selectors"
);
let restored = std::fs::read_to_string(&lock_path).unwrap();
let checksum = berry_checksum_of(&restored);
assert_eq!(
restored,
pristine_lock.replace(
&format!("10c0/{}", "3".repeat(128)),
&format!("10c0/{checksum}")
),
"rollback restores the expanded npm: key"
);
}

/// `npm:<v>::__archiveUrl=<url>` resolution, which makes yarn's npm fetcher
/// send registry auth to the patch host. `rollback` must still recognize and
/// restore that legacy pin, and a repeat hosted `scan` must re-pin it to the
Expand Down
Loading
Loading