use std::time::Duration;
use super::builder::{clean_env_value, resolve_api_key, resolve_lookup, resolve_optional_api_key};
use super::core::Client;
use super::transport::is_user_overridable;
use crate::error::Error;
#[test]
fn resolve_lookup_precedence() {
let primary = "OPENKIND_API_KEY";
let fallback = "TYPESAFE_API_KEY";
let env = |wanted: &'static str, value: &'static str| {
move |name: &str| (name == wanted).then(|| value.to_owned())
};
assert_eq!(
resolve_lookup(
Some("explicit".into()),
env(primary, "p"),
primary,
fallback
),
Some("explicit".into())
);
assert_eq!(
resolve_lookup(None, env(primary, "p"), primary, fallback),
Some("p".into())
);
assert_eq!(
resolve_lookup(None, env(fallback, "f"), primary, fallback),
Some("f".into())
);
assert_eq!(resolve_lookup(None, |_| None, primary, fallback), None);
assert_eq!(clean_env_value(" ".into()), None);
assert_eq!(clean_env_value("".into()), None);
assert_eq!(
clean_env_value(" real-value ".into()),
Some("real-value".into())
);
}
#[test]
fn api_key_resolution_matches_python_sdk() {
let env_key = |_: &str| clean_env_value(" env-key ".to_owned());
for padding in ["", "\n", "\r\n", " \t\r\n "] {
let key = resolve_api_key(Some(format!("{padding}test-key{padding}")), |_| None).unwrap();
assert_eq!(key, "test-key");
}
assert_eq!(resolve_api_key(None, env_key).unwrap(), "env-key");
for key in ["", " \t\r\n ", "\0private", "private\0"] {
let err = resolve_api_key(Some(key.to_owned()), env_key).unwrap_err();
assert!(!err.contains("env-key"), "{err}");
}
for character in ["\n", "\r", "\t", "\u{1f}", "\u{7f}", " ", "é", "\u{200b}"] {
let explicit = resolve_api_key(Some(format!("ts_live_private{character}suffix")), |_| None)
.unwrap_err();
let from_env = resolve_api_key(None, |_| Some(format!("ts_live_private{character}suffix")))
.unwrap_err();
for err in [explicit, from_env] {
assert!(err.contains("printable ASCII"), "{err}");
assert!(!err.contains("ts_live_private"), "{err}");
}
}
let err = resolve_api_key(None, |_| None).unwrap_err();
assert!(err.contains("OPENKIND_API_KEY"), "{err}");
assert!(err.contains("TYPESAFE_API_KEY"), "{err}");
}
#[test]
fn client_debug_does_not_leak_api_key() {
let client = Client::new("super-secret-key").unwrap();
let rendered = format!("{client:?}");
assert!(!rendered.contains("super-secret-key"), "{rendered}");
}
#[test]
fn unauthenticated_mode_only_allows_a_missing_key() {
assert_eq!(resolve_optional_api_key(None, |_| None).unwrap(), None);
assert!(resolve_api_key(None, |_| None).is_err());
assert_eq!(
resolve_optional_api_key(None, |_| Some("env-key".into())).unwrap(),
Some("env-key".into())
);
assert_eq!(
resolve_optional_api_key(Some("explicit".into()), |_| Some("env-key".into())).unwrap(),
Some("explicit".into())
);
for key in ["", " ", "invalid key", "invalid\nkey", "sécret"] {
assert!(resolve_optional_api_key(Some(key.into()), |_| Some("valid".into())).is_err());
assert!(Client::builder()
.allow_unauthenticated()
.api_key(key)
.build()
.is_err());
}
}
#[test]
fn base_url_trailing_slashes_trimmed() {
let client = Client::builder()
.api_key("k")
.base_url("http://example.test/prefix///")
.build()
.unwrap();
assert_eq!(client.base_url(), "http://example.test/prefix");
}
#[test]
fn base_url_surrounding_whitespace_is_trimmed_before_validation() {
let client = Client::builder()
.api_key("k")
.base_url(" http://example.test/prefix/ ")
.build()
.unwrap();
assert_eq!(client.base_url(), "http://example.test/prefix");
}
#[test]
fn invalid_base_url_rejected_at_build() {
let err = Client::builder()
.api_key("k")
.base_url("ftp://example.test")
.build()
.unwrap_err();
assert!(matches!(err, Error::Config(_)), "{err:?}");
}
#[test]
fn base_url_rejects_components_that_hide_endpoint_paths_or_credentials() {
for url in [
"http://example.test?query=1",
"http://example.test/#fragment",
"http://user:password@example.test",
] {
assert!(matches!(
Client::builder().api_key("k").base_url(url).build(),
Err(Error::Config(_))
));
}
}
#[test]
fn base_url_with_invalid_port_rejected_at_build() {
let err = Client::builder()
.api_key("k")
.base_url("http://example.test:not-a-port")
.build()
.unwrap_err();
assert!(matches!(err, Error::Config(_)), "{err:?}");
assert!(
err.to_string().contains("invalid base URL"),
"unexpected error: {err}"
);
}
#[test]
fn protected_headers_are_not_user_overridable() {
for name in [
"authorization",
"accept",
"user-agent",
"x-typesafe-sdk",
"x-typesafe-runtime",
"x-typesafe-retry-count",
"host",
"content-length",
"transfer-encoding",
] {
assert!(!is_user_overridable(name, false), "{name} with no body");
assert!(!is_user_overridable(name, true), "{name} with body");
}
assert!(!is_user_overridable("content-type", true));
assert!(is_user_overridable("content-type", false));
assert!(is_user_overridable("x-custom", true));
assert!(is_user_overridable("x-custom", false));
}
#[test]
fn zero_timeout_rejected_at_build() {
let err = Client::builder()
.api_key("k")
.base_url("http://example.test")
.timeout(Duration::ZERO)
.build()
.unwrap_err();
assert!(matches!(err, Error::Config(_)), "{err:?}");
}