use super::{REDACTED, Secret};
use crate::completion::CompletionRequest;
use crate::operation::Completion;
use crate::providers::anthropic::wire::AnthropicConfig;
use crate::providers::cohere::wire::CohereConfig;
use crate::providers::gemini::GeminiConfig;
use crate::providers::openai::wire::OpenAIConfig;
use crate::wire::{Mode, Wire};
#[test]
fn a_secret_never_renders_or_serializes_its_value() {
let secret = Secret::from("sk-live-do-not-print");
assert_eq!(format!("{secret:?}"), "[redacted]");
assert_eq!(
serde_json::to_string(&secret).expect("a string serializes"),
"\"[redacted]\""
);
assert_eq!(secret.expose(), "sk-live-do-not-print");
}
#[test]
fn secrets_compare_by_value() {
assert_eq!(Secret::from("k"), Secret::from("k"));
assert_ne!(Secret::from("k"), Secret::from("j"));
}
#[test]
fn a_secret_deserializes_from_a_bare_string() {
let secret: Secret = serde_json::from_str("\"k\"").expect("a string deserializes");
assert_eq!(secret.expose(), "k");
}
#[test]
fn the_redaction_sentinel_reloads_as_no_credential() {
let stored =
serde_json::to_string(&Secret::from("sk-live-do-not-print")).expect("a secret serializes");
let reloaded: Secret = serde_json::from_str(&stored).expect("the sentinel deserializes");
assert!(reloaded.is_empty());
assert_eq!(reloaded, Secret::default());
}
#[test]
fn a_reloaded_wire_sends_no_credential_sentinel() {
sends_no_sentinel(
&OpenAIConfig::new("sk-bearer-key").chat("gpt-5.2"),
"sk-bearer-key",
);
sends_no_sentinel(
&OpenAIConfig::new("sk-bearer-key").responses("gpt-5.2"),
"sk-bearer-key",
);
sends_no_sentinel(
&AnthropicConfig::new("sk-header-key").completion("claude-haiku-4-5"),
"sk-header-key",
);
sends_no_sentinel(
&GeminiConfig::new("AIzaSyQUERY-KEY").completion("gemini-2.5-flash"),
"AIzaSyQUERY-KEY",
);
sends_no_sentinel(
&CohereConfig::new("cohere-bearer-key").completion("command-a-03-2025"),
"cohere-bearer-key",
);
assert!(
request_envelope(&OpenAIConfig::new(REDACTED).chat("gpt-5.2"))
.contains("Bearer [redacted]")
);
}
fn probe_request() -> CompletionRequest {
CompletionRequest::new("probe")
}
pub(crate) fn request_envelope<W: Wire<Op = Completion, Payload = crate::wire::Encoded>>(
wire: &W,
) -> String {
let encoded = wire
.encode(probe_request(), Mode::Unary)
.expect("the request encodes");
std::iter::once(&encoded.request)
.map(|request| {
let headers: String = request
.headers()
.iter()
.map(|(name, value)| format!("{name}: {}\n", value.to_str().unwrap_or_default()))
.collect();
format!("{}\n{headers}", request.uri())
})
.collect()
}
pub(crate) fn a_config_reloads_without_its_credential<C>(
config: &C,
key: &str,
credential: impl Fn(&C) -> &Secret,
) where
C: std::fmt::Debug + serde::Serialize + serde::de::DeserializeOwned,
{
assert!(
!credential(config).is_empty(),
"the fixture must carry a credential for this to prove anything"
);
let json = serde_json::to_string(config).expect("a config serializes");
assert!(
!json.contains(key),
"a config a host may persist must not carry the credential: {json}"
);
assert!(json.contains(REDACTED), "{json}");
assert!(
!format!("{config:?}").contains(key),
"`Debug` leaked the credential"
);
let reloaded: C = serde_json::from_str(&json).expect("a config reloads");
assert!(
credential(&reloaded).is_empty(),
"a reloaded config must report no credential, not the sentinel: {reloaded:?}"
);
}
fn sends_no_sentinel<W>(wire: &W, key: &str)
where
W: Wire<Op = Completion, Payload = crate::wire::Encoded>
+ serde::Serialize
+ serde::de::DeserializeOwned,
{
let sent = request_envelope(wire);
assert!(
sent.contains(key),
"the wire never sent the credential, so reloading it proves nothing: {sent}"
);
let json = serde_json::to_string(wire).expect("a wire serializes");
let reloaded: W = serde_json::from_str(&json).expect("a wire reloads");
let sent = request_envelope(&reloaded);
assert!(
!sent.contains("redacted"),
"a reloaded wire sent the redaction sentinel as a credential: {sent}"
);
}
#[test]
fn chatgpt_auth_context_debug_redacts_retained_credential() {
let key = "synthetic-chatgpt-context-token";
let context = crate::providers::chatgpt::auth::AuthContext {
access_token: key.into(),
account_id: Some("synthetic-account".into()),
};
assert_eq!(context.access_token.expose(), key);
for debug in [format!("{context:?}"), format!("{context:#?}")] {
assert!(!debug.contains(key), "AuthContext leaked its credential");
}
}
#[test]
fn copilot_auth_context_debug_redacts_retained_credential() {
let key = "synthetic-copilot-context-key";
let context = crate::providers::copilot::auth::AuthContext {
api_key: key.into(),
api_base: Some("https://copilot.example.invalid".into()),
};
assert_eq!(context.api_key.expose(), key);
for debug in [format!("{context:?}"), format!("{context:#?}")] {
assert!(!debug.contains(key), "AuthContext leaked its credential");
}
}
#[test]
fn gemini_encoded_debug_redacts_query_without_removing_authentication() -> anyhow::Result<()> {
let key = "synthetic-gemini-query-key";
let encoded = GeminiConfig::new(key)
.completion("gemini-2.5-flash")
.encode(probe_request(), Mode::Unary)?;
anyhow::ensure!(
encoded.request.uri().query() == Some("key=synthetic-gemini-query-key"),
"Gemini query credential changed"
);
for debug in [format!("{encoded:?}"), format!("{encoded:#?}")] {
anyhow::ensure!(!debug.contains(key), "Encoded leaked its query credential");
anyhow::ensure!(debug.contains("POST"), "Encoded omitted the method");
anyhow::ensure!(
debug.contains("/v1beta/models/gemini-2.5-flash:generateContent"),
"Encoded omitted the path"
);
}
Ok(())
}
#[test]
fn encoded_debug_excludes_every_non_path_request_surface() -> anyhow::Result<()> {
use crate::http_client::framing::Framing;
use crate::wire::{Body, Encoded};
let encoded = Encoded::new(
http::Request::post("https://synthetic-authority.invalid/first?unknown=synthetic-query")
.header("authorization", "synthetic-header")
.body(Body::Bytes(b"synthetic-body".to_vec()))?,
Framing::Whole,
);
for debug in [format!("{encoded:?}"), format!("{encoded:#?}")] {
for marker in [
"synthetic-authority",
"synthetic-query",
"synthetic-header",
"synthetic-body",
"https://",
] {
anyhow::ensure!(!debug.contains(marker), "Encoded leaked {marker}");
}
for useful in ["POST", "/first"] {
anyhow::ensure!(debug.contains(useful), "Encoded omitted {useful}");
}
}
Ok(())
}