use std::path::{Path, PathBuf};
use std::time::Duration;
use super::super::*;
use crate::auth::command::{self, KeyCommandError};
use crate::config::LlmConfig;
use crate::test_support::write_executable;
fn stub_printing(dir: &Path, name: &str, stdout: &str) -> PathBuf {
let path = dir.join(name);
write_executable(&path, format!("#!/bin/sh\nprintf '%s' '{stdout}'\n"));
path
}
fn config_running(argv: &[&PathBuf]) -> Config {
Config {
max_review_rounds: crate::config::DEFAULT_MAX_REVIEW_ROUNDS,
llm: vec![LlmConfig {
endpoint: Some("https://gateway.example/v1".to_owned()),
model: Some("m".to_owned()),
api_key_command: Some(
argv.iter()
.map(|p| p.to_string_lossy().into_owned())
.collect(),
),
..LlmConfig::default()
}],
}
}
fn generous() -> Duration {
Duration::from_secs(command::TIMEOUT_SECS)
}
#[tokio::test]
async fn a_command_supplies_the_key_when_the_config_names_none() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = stub_printing(dir.path(), "print-token", "minted-token");
let mut config = config_running(&[&stub]);
let sources = resolve(&mut config, &AuthStore::new())
.await
.expect("the command succeeds");
assert_eq!(config.llm[0].api_key.as_deref(), Some("minted-token"));
assert_eq!(sources, vec![KeySource::Command]);
}
#[tokio::test]
async fn an_explicit_api_key_wins_and_the_command_never_runs() {
let dir = tempfile::tempdir().expect("tempdir");
let sentinel = dir.path().join("ran");
let stub = dir.path().join("print-token");
write_executable(
&stub,
format!(
"#!/bin/sh\n: > '{}'\nprintf '%s' 'from-command'\n",
sentinel.display()
),
);
let mut config = config_running(&[&stub]);
config.llm[0].api_key = Some("from-config".to_owned());
let sources = resolve(&mut config, &AuthStore::new())
.await
.expect("nothing to fail");
assert_eq!(config.llm[0].api_key.as_deref(), Some("from-config"));
assert_eq!(sources, vec![KeySource::Config]);
assert!(
!sentinel.exists(),
"the command must not run when the file already says where the key comes from"
);
}
#[tokio::test]
async fn a_command_wins_over_a_stored_key_for_the_same_endpoint() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = stub_printing(dir.path(), "print-token", "from-command");
let mut config = config_running(&[&stub]);
let mut store = AuthStore::new();
store
.set("https://gateway.example/v1", "from-store")
.expect("set");
let sources = resolve(&mut config, &store)
.await
.expect("the stub succeeds");
assert_eq!(config.llm[0].api_key.as_deref(), Some("from-command"));
assert_eq!(sources, vec![KeySource::Command]);
}
#[tokio::test]
async fn a_disabled_entry_never_runs_its_command() {
let dir = tempfile::tempdir().expect("tempdir");
let sentinel = dir.path().join("ran");
let stub = dir.path().join("print-token");
write_executable(
&stub,
format!(
"#!/bin/sh\n: > '{}'\nprintf '%s' 'tok'\n",
sentinel.display()
),
);
let mut config = config_running(&[&stub]);
config.llm[0].enabled = false;
let sources = resolve(&mut config, &AuthStore::new())
.await
.expect("a parked entry cannot fail");
assert_eq!(config.llm[0].api_key, None);
assert_eq!(sources, vec![KeySource::Missing]);
assert!(!sentinel.exists(), "a parked entry spends nothing");
}
#[tokio::test]
async fn a_failing_command_is_fatal_and_names_the_entry_in_file_order() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = dir.path().join("print-token");
write_executable(&stub, "#!/bin/sh\nexit 3\n");
let mut config = config_running(&[&stub]);
config.llm.insert(
0,
LlmConfig {
endpoint: Some("http://localhost:1234/v1".to_owned()),
model: Some("first".to_owned()),
..LlmConfig::default()
},
);
let err = resolve(&mut config, &AuthStore::new())
.await
.expect_err("a broken credential path is fatal, not a provider failure");
let message = err.to_string();
assert!(message.contains("#2 in file order"), "got {message}");
assert!(message.contains("api_key_command"), "got {message}");
}
#[tokio::test]
async fn a_trailing_newline_is_not_part_of_the_credential() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = dir.path().join("print-token");
write_executable(&stub, "#!/bin/sh\necho tok\n");
let key = command::run(&[stub.to_string_lossy().into_owned()], generous())
.await
.expect("a newline-terminated token is the normal case");
assert_eq!(key, "tok");
}
#[tokio::test]
async fn a_command_that_prints_nothing_is_an_error_naming_the_program() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = stub_printing(dir.path(), "silent", "");
let err = command::run(&[stub.to_string_lossy().into_owned()], generous())
.await
.expect_err("an empty credential is not a credential");
assert!(matches!(err, KeyCommandError::Empty { .. }), "got {err:?}");
assert!(err.to_string().contains("silent"), "got {err}");
}
#[tokio::test]
async fn a_failing_command_reports_the_program_and_status_but_never_its_output() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = dir.path().join("noisy");
write_executable(
&stub,
"#!/bin/sh\nprintf '%s' 'sk-live-sekrit'\nprintf '%s' 'sk-live-sekrit' >&2\nexit 7\n",
);
let err = command::run(&[stub.to_string_lossy().into_owned()], generous())
.await
.expect_err("a nonzero exit is a failure");
let message = err.to_string();
assert!(message.contains("noisy"), "got {message}");
assert!(message.contains('7'), "got {message}");
assert!(
!message.contains("sk-live-sekrit"),
"the credential reached the diagnostic: {message}"
);
assert!(
!format!("{err:?}").contains("sk-live-sekrit"),
"the credential reached Debug: {err:?}"
);
}
#[tokio::test]
async fn a_program_that_does_not_exist_is_reported_as_not_found() {
let err = command::run(
&["drep-no-such-credential-helper-xyz".to_owned()],
generous(),
)
.await
.expect_err("a missing program cannot mint a key");
assert!(
matches!(err, KeyCommandError::NotFound { .. }),
"got {err:?}"
);
assert!(err.to_string().contains("PATH"), "got {err}");
}
#[tokio::test]
async fn an_empty_argv_is_reported_rather_than_panicking() {
let err = command::run(&[], generous())
.await
.expect_err("there is no program to run");
assert!(matches!(err, KeyCommandError::NoProgram), "got {err:?}");
}
#[tokio::test]
async fn a_hung_command_is_bounded_by_the_supplied_timeout() {
let err = command::run(
&["/bin/sh".to_owned(), "-c".to_owned(), "sleep 30".to_owned()],
Duration::from_millis(100),
)
.await
.expect_err("an unbounded child would hang the gate");
assert!(
matches!(err, KeyCommandError::Timeout { .. }),
"got {err:?}"
);
}
#[tokio::test]
async fn an_interior_newline_makes_the_credential_unusable() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = dir.path().join("two-lines");
write_executable(&stub, "#!/bin/sh\nprintf 'a\\nb\\n'\n");
let err = command::run(&[stub.to_string_lossy().into_owned()], generous())
.await
.expect_err("a header value cannot carry a newline");
assert!(
matches!(err, KeyCommandError::Unusable { .. }),
"got {err:?}"
);
}
#[tokio::test]
async fn stdout_is_taken_whole_rather_than_scanned_for_a_token_shaped_line() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = stub_printing(dir.path(), "structured", "Bearer abc.def-ghi");
let key = command::run(&[stub.to_string_lossy().into_owned()], generous())
.await
.expect("the whole trimmed output is the credential");
assert_eq!(key, "Bearer abc.def-ghi");
}
#[tokio::test]
async fn surrounding_whitespace_is_trimmed_at_both_ends() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = dir.path().join("padded");
write_executable(&stub, "#!/bin/sh\nprintf ' %s\\n' tok\n");
let key = command::run(&[stub.to_string_lossy().into_owned()], generous())
.await
.expect("padding is the helper's, not the credential's");
assert_eq!(key, "tok");
}
#[cfg(unix)]
#[tokio::test]
async fn a_helper_that_cannot_be_executed_is_reported_separately_from_a_missing_one() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = dir.path().join("not-executable");
std::fs::write(&stub, "#!/bin/sh\nprintf '%s' tok\n").expect("a file with no execute bit");
let err = command::run(&[stub.to_string_lossy().into_owned()], generous())
.await
.expect_err("a file that cannot be executed cannot mint a key");
assert!(matches!(err, KeyCommandError::Spawn { .. }), "got {err:?}");
let message = err.to_string();
assert!(message.contains("could not be started"), "got {message}");
assert!(
!message.contains("PATH"),
"the program was found; naming PATH sends the reader to the wrong fix: {message}"
);
}
#[cfg(unix)]
#[tokio::test]
async fn a_helper_killed_by_a_signal_says_so_rather_than_inventing_a_status() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = dir.path().join("suicidal");
write_executable(&stub, "#!/bin/sh\nkill -TERM $$\n");
let err = command::run(&[stub.to_string_lossy().into_owned()], generous())
.await
.expect_err("a helper that did not finish did not mint a key");
assert!(matches!(err, KeyCommandError::Signal { .. }), "got {err:?}");
assert!(err.to_string().contains("signal"), "got {err}");
}
#[tokio::test]
async fn output_that_is_not_utf8_is_refused_rather_than_replaced_with_placeholders() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = dir.path().join("binary");
write_executable(&stub, "#!/bin/sh\nprintf '\\377\\376'\n");
let err = command::run(&[stub.to_string_lossy().into_owned()], generous())
.await
.expect_err("a credential drep cannot read is not a credential");
assert!(
matches!(err, KeyCommandError::NotUtf8 { .. }),
"got {err:?}"
);
assert!(err.to_string().contains("binary"), "got {err}");
}
#[cfg(unix)]
#[tokio::test]
async fn a_grandchild_inheriting_stdout_cannot_hold_credential_resolution_open() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = dir.path().join("background-child");
write_executable(
&stub,
"#!/bin/sh\ncapture=$(dirname \"$0\")\nsleep 30 &\nprintf '%s' \"$!\" > \"$capture/grandchild.pid\"\nprintf '%s' token\n",
);
let result = command::run(
&[stub.to_string_lossy().into_owned()],
std::time::Duration::from_secs(10),
)
.await;
let pid = std::fs::read_to_string(dir.path().join("grandchild.pid")).expect("grandchild pid");
let running = crate::test_support::probe_and_stop_process(&pid);
let key = result.expect("the direct helper exited successfully");
assert_eq!(key, "token");
assert!(
running,
"credential resolution waited for the unrelated grandchild to exit"
);
}
#[tokio::test]
async fn output_past_the_ceiling_is_refused_rather_than_read_whole() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = dir.path().join("flood");
write_executable(&stub, "#!/bin/sh\nhead -c 70000 /dev/zero | tr '\\0' a\n");
let err = command::run(&[stub.to_string_lossy().into_owned()], generous())
.await
.expect_err("70000 bytes is not a credential");
assert!(
matches!(err, KeyCommandError::TooMuchOutput { limit, .. } if limit == 64 * 1024),
"got {err:?}"
);
let message = err.to_string();
assert!(message.contains("65536"), "names the ceiling: {message}");
assert!(!message.contains("aaaa"), "and never the output: {message}");
}
#[tokio::test]
async fn output_exactly_at_the_ceiling_is_still_accepted() {
let dir = tempfile::tempdir().expect("tempdir");
let stub = dir.path().join("brim");
write_executable(&stub, "#!/bin/sh\nhead -c 65536 /dev/zero | tr '\\0' a\n");
let key = command::run(&[stub.to_string_lossy().into_owned()], generous())
.await
.expect("exactly at the ceiling is within it");
assert_eq!(key.len(), 64 * 1024);
}