Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion crates/cli/src/commands/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ async fn execute_set(args: SetArgs, manager: &AliasManager, formatter: &Formatte
}

if let Err(e) = validate_alias_endpoint(&args.endpoint) {
formatter.error(&alias_endpoint_error_message(e));
formatter.error_with_code(ExitCode::UsageError, &alias_endpoint_error_message(e));
return ExitCode::UsageError;
}
Comment thread
overtrue marked this conversation as resolved.

Expand Down
66 changes: 65 additions & 1 deletion crates/cli/tests/error_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! These tests cover local validation paths that do not require a running S3
//! backend but still need stable JSON error metadata.

use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};

fn rc_binary() -> PathBuf {
Expand Down Expand Up @@ -33,6 +33,14 @@ fn run_rc(args: &[&str]) -> Output {
.expect("failed to execute rc")
}

Comment thread
overtrue marked this conversation as resolved.
fn run_rc_with_config(args: &[&str], config_dir: &Path) -> Output {
Command::new(rc_binary())
.args(args)
.env("RC_CONFIG_DIR", config_dir)
.output()
.expect("failed to execute rc")
}
Comment thread
overtrue marked this conversation as resolved.

#[test]
fn cp_local_to_local_json_error_reports_usage_metadata() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
Expand Down Expand Up @@ -73,3 +81,59 @@ fn cp_local_to_local_json_error_reports_usage_metadata() {
"Use your local shell cp command when both paths are on the filesystem."
);
}

#[test]
fn alias_set_json_error_rejects_embedded_endpoint_credentials() {
let config_dir = tempfile::tempdir().expect("create temp config dir");

let output = run_rc_with_config(
&[
"alias",
"set",
"bad",
"http://ACCESS_KEY:SECRET_KEY@localhost:9000",
"accesskey",
"secretkey",
"--json",
],
config_dir.path(),
);

assert_eq!(
output.status.code(),
Some(2),
"stdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
output.stdout.is_empty(),
"usage JSON errors should be emitted on stderr"
);

let stderr = String::from_utf8(output.stderr).expect("stderr should be UTF-8");
assert!(!stderr.contains("SECRET_KEY"));

let json: serde_json::Value = serde_json::from_str(&stderr).expect("stderr is valid JSON");
assert_eq!(
json["error"],
"Endpoint must not include credentials; pass access key and secret key as separate arguments"
);
assert_eq!(json["code"], 2);
assert_eq!(json["details"]["type"], "usage_error");
assert_eq!(json["details"]["retryable"], false);

let list_output = run_rc_with_config(&["alias", "list", "--json"], config_dir.path());
assert!(
list_output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&list_output.stderr)
);

let stdout = String::from_utf8(list_output.stdout).expect("stdout should be UTF-8");
let payload: serde_json::Value = serde_json::from_str(&stdout).expect("stdout is valid JSON");
assert_eq!(
payload["aliases"].as_array().expect("aliases array").len(),
0
);
}
Loading