Skip to content
Merged
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
108 changes: 108 additions & 0 deletions crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2588,6 +2588,114 @@ async fn gem_hosted_group_block_pin_survives_a_refused_vendored_takeover() {
);
}
}
// #1761: the standalone `vendor` eject of the same pin. Its dry run
// must preview the refusal the wet run raises, not `eject_planned`.
mount_vendored_grant(&fx).await;
for dry_run in [true, false] {
vendor_eject_keeps_the_hosted_group_pin(&fx, dry_run);
}
}

/// #1761 on a CHECKSUMS-less lock that an unfrozen `bundle install`
/// converged (the lock now resolves the hosted gem from the patch
/// registry): `vendor --dry-run` previewed `eject_planned` (exit 0) while
/// the wet eject refused `gemfile_declaration_not_editable`.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "host capstone: shells out to a real ruby/gem/bundler (>= 2.2); \
the unpinned `test` job skips it, an e2e job with a pinned toolchain runs it via --ignored"]
async fn gem_hosted_group_block_pin_survives_a_refused_eject_of_a_converged_lock() {
let Some(fx) = redirect_scanned_project(
"group-block converged eject",
Spelling::Gemfile,
false,
true,
None,
Driver::ScanVexGroupBlock,
)
.await
else {
return;
};
if !fx.bundler.at_least(2, 2) {
println!(
"SKIP e2e_redirect_gem_build (group-block converged eject): bundler {} merges \
every rubygems source into one GEM section",
fx.bundler.version
);
return;
}
let install = bundle(&fx.proj, &["install"]);
assert!(
install.status.success(),
"converging install:\n{}",
String::from_utf8_lossy(&install.stderr)
);
let lock = std::fs::read_to_string(fx.proj.join(fx.lock_name)).unwrap();
assert!(
lock.contains(&format!("remote: {}", fx.index_url)),
"converged:\n{lock}"
);
mount_vendored_grant(&fx).await;
for dry_run in [true, false] {
vendor_eject_keeps_the_hosted_group_pin(&fx, dry_run);
}
}

/// One refused `vendor` eject of the group-block fixture (#1761): dry or
/// wet, the eject is refused whole with `gemfile_declaration_not_editable`
/// and `eject_refused` (exit 1), and the hosted `Gemfile` /
/// `Gemfile.lock` stay byte-untouched.
fn vendor_eject_keeps_the_hosted_group_pin(fx: &RedirectFixture, dry_run: bool) {
let before: Vec<Vec<u8>> = ["Gemfile", "Gemfile.lock"]
.iter()
.map(|f| std::fs::read(fx.proj.join(f)).unwrap())
.collect();
let proj = fx.proj.to_str().expect("utf8 tmp path");
let api = fx._server.uri();
let mut argv: Vec<&str> = vec![
"vendor",
"--json",
"--yes",
"--cwd",
proj,
"--api-url",
&api,
"--org",
ORG,
"--api-token",
"fake",
"--patch-server-url",
&api,
];
if dry_run {
argv.push("--dry-run");
}
let label = format!("vendor eject (dry_run={dry_run})");
let upstream = format!("{api}/upstream");
let (code, stdout, stderr) =
run_socket_env(&fx.proj, &argv, &[("SOCKET_RUBYGEMS_URL", &upstream)]);
let env: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("{label}: not JSON: {e}\nstdout:\n{stdout}\nstderr:\n{stderr}"));
assert_eq!(code, 1, "{label} must refuse: {env}\nstderr:\n{stderr}");
let events = env["events"].as_array().expect("events");
assert_eq!(events.len(), 1, "{label}: {env}");
assert_eq!(events[0]["action"], "failed", "{label}: {env}");
assert_eq!(
events[0]["errorCode"], "gemfile_declaration_not_editable",
"{label}: {env}"
);
assert_eq!(env["error"]["code"], "eject_refused", "{label}: {env}");
assert!(
!stdout.contains("eject_planned") && !stdout.contains("eject_rolled_back"),
"{label}: refused before any restore (or a preview of one): {env}"
);
for (file, before) in ["Gemfile", "Gemfile.lock"].iter().zip(before) {
assert_eq!(
std::fs::read(fx.proj.join(file)).unwrap(),
before,
"{label}: {file} keeps its hosted wiring"
);
}
}

/// #1056: a gem the user declared inside their own `source "…" do` block
Expand Down
27 changes: 27 additions & 0 deletions crates/socket-patch-core/src/vendor/gem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,14 @@ pub async fn vendor_gem<'a>(
}

if dry_run {
// An unwired gem previews the Gemfile and lock edit refusals the wet
// run raises (a declaration inside a `group` block, #1761), so a
// takeover or eject preview over the restored text refuses too.
if !lock_wired {
Comment thread
mikolalysenko marked this conversation as resolved.
Resolved
if let Err(outcome) = gem_edits(purl, &prelude) {
return *outcome;
}
}
if let Err(outcome) =
super::service_fetch::preview_service(service, record, extract_gem_data).await
{
Expand Down Expand Up @@ -3663,6 +3671,25 @@ mod tests {
assert!(detail.contains("path:"), "{detail}");
}

/// #1761: a dry run raises the same declaration refusal as the wet run,
/// so a `vendor` eject preview does not report a refused gem as planned.
#[tokio::test]
async fn test_dry_run_previews_declaration_refusal() {
let grouped =
"source \"https://rubygems.org\"\n\ngroup :test do\n gem \"rack\", \"~> 3.1\"\nend\n";
for dry_run in [true, false] {
let (_tmp, root, installed, blobs, record) = fixture(grouped, LOCK_DIRECT).await;
let (code, detail) =
unwrap_refused(run_vendor(&root, &blobs, &installed, &record, dry_run).await);
assert_eq!(
code, "gemfile_declaration_not_editable",
"dry_run={dry_run}"
);
assert!(detail.contains("indented"), "dry_run={dry_run}: {detail}");
assert!(!root.join(".socket").exists(), "dry_run={dry_run}");
}
}

/// SECURITY: a traversal uuid (tampered manifest) must be refused before
/// any disk access.
#[tokio::test]
Expand Down
Loading