use super::*;
#[test]
fn effective_auth_token_never_yields_empty() {
let _env = crate::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
crate::test_env::set_var("LEAN_CTX_DATA_DIR", tmp.path());
assert_eq!(effective_auth_token(Some("tok".into())), "tok");
let auto = effective_auth_token(None);
assert!(!auto.trim().is_empty(), "None must auto-resolve a token");
let blank = effective_auth_token(Some(" ".into()));
assert!(!blank.trim().is_empty(), "blank tokens must be replaced");
crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
}
#[test]
fn installs_default_crypto_provider_for_ws_passthrough() {
install_default_crypto_provider();
assert!(
rustls::crypto::CryptoProvider::get_default().is_some(),
"WS passthrough needs a process-default CryptoProvider"
);
}
#[test]
fn is_provider_route_v1() {
assert!(is_provider_route("/v1/chat/completions"));
assert!(is_provider_route("/v1/messages"));
assert!(is_provider_route("/v1/completions"));
}
#[test]
fn is_provider_route_anthropic_subpaths() {
assert!(is_provider_route("/v1/messages/count_tokens"));
assert!(is_provider_route("/v1/messages/batches"));
assert!(is_provider_route("/v1/messages/batches/batch_123"));
}
#[test]
fn is_provider_route_v1beta() {
assert!(is_provider_route("/v1beta/models"));
}
#[test]
fn is_provider_route_chat() {
assert!(is_provider_route("/chat/completions"));
}
#[test]
fn is_provider_route_chatgpt_backend_api() {
assert!(is_provider_route("/backend-api/codex/responses"));
assert!(is_provider_route("/backend-api/codex/responses/resp_123"));
assert!(is_provider_route("/backend-api/wham/session"));
assert!(is_provider_route("/backend-api/ps/mcp"));
assert!(is_provider_route("/backend-api/codex_apps"));
assert!(is_provider_route("/backend-api/codex_apps/mcp"));
assert!(is_provider_route("/backend-api/mcp/codex_apps"));
assert!(is_provider_route("/backend-api/apps/codex_apps/mcp"));
}
#[test]
fn is_provider_route_rejects_non_provider() {
assert!(!is_provider_route("/health"));
assert!(!is_provider_route("/api/v2/test"));
assert!(!is_provider_route("/"));
}
#[test]
fn is_provider_route_model_catalog() {
assert!(is_provider_route("/v1/models"));
assert!(is_provider_route("/models"));
assert!(!is_provider_route("/modelsx"));
}
#[cfg(feature = "gateway-server")]
#[test]
fn me_shell_is_public_but_data_api_stays_guarded() {
assert!(me_shell_path("/me"));
assert!(me_shell_path("/me/static/me.js"));
assert!(!me_shell_path("/api/me/usage"));
assert!(!me_shell_path("/v1/messages"));
}
fn build_request(headers: &[(&str, &str)], path: &str) -> axum::extract::Request {
let mut builder = axum::http::Request::builder().uri(path);
for (k, v) in headers {
builder = builder.header(*k, *v);
}
builder.body(axum::body::Body::empty()).unwrap()
}
#[test]
fn has_provider_api_key_x_api_key() {
let req = build_request(&[("x-api-key", "sk-ant-abc123")], "/v1/messages");
assert!(has_provider_api_key(&req));
}
#[test]
fn has_provider_api_key_x_goog() {
let req = build_request(&[("x-goog-api-key", "AIzaSyAbc")], "/v1beta/models");
assert!(has_provider_api_key(&req));
}
#[test]
fn has_provider_api_key_azure() {
let req = build_request(&[("api-key", "deadbeef")], "/v1/completions");
assert!(has_provider_api_key(&req));
}
#[test]
fn has_provider_api_key_bearer_sk() {
let req = build_request(
&[("authorization", "Bearer sk-proj-abc123")],
"/v1/chat/completions",
);
assert!(has_provider_api_key(&req));
}
#[test]
fn has_provider_api_key_empty_rejected() {
let req = build_request(&[("x-api-key", " ")], "/v1/messages");
assert!(!has_provider_api_key(&req));
}
#[test]
fn has_provider_api_key_no_headers() {
let req = build_request(&[], "/v1/messages");
assert!(!has_provider_api_key(&req));
}
#[test]
fn has_provider_api_key_accepts_non_sk_bearer() {
for key in [
"Bearer or-v1-9f8e7d6c", "Bearer gsk_live_1234", "Bearer abc.def.ghi", "Bearer 0123456789", ] {
let req = build_request(&[("authorization", key)], "/v1/responses");
assert!(
has_provider_api_key(&req),
"non-sk Bearer must count as a provider credential: {key}"
);
}
}
#[test]
fn has_provider_api_key_empty_bearer_rejected() {
for bad in ["Bearer ", "", "Bearer", "bearer", " "] {
let req = build_request(&[("authorization", bad)], "/responses");
assert!(
!has_provider_api_key(&req),
"blank/scheme-only Authorization must not authenticate: {bad:?}"
);
}
}
#[test]
fn provider_key_fallback_allowed_in_default_mode() {
assert!(provider_key_fallback_allowed(false, true, true));
}
#[test]
fn provider_key_fallback_denied_in_strict_mode() {
assert!(!provider_key_fallback_allowed(true, true, true));
}
#[test]
fn provider_key_fallback_requires_key_and_provider_route() {
assert!(!provider_key_fallback_allowed(false, false, true));
assert!(!provider_key_fallback_allowed(false, true, false));
assert!(!provider_key_fallback_allowed(true, false, true));
}
#[test]
fn proxy_require_token_defaults_off() {
assert!(!crate::core::config::Config::default().proxy_require_token);
}
#[test]
fn attach_gateway_tags_header_overrides_default_project() {
let mut req = build_request(&[("x-leanctx-project", "billing")], "/v1/messages");
attach_gateway_tags(
&mut req,
gateway_identity::GatewayTags {
person: Some("yves".into()),
team: Some("platform".into()),
project: Some("ai-gateway".into()),
},
);
let tags = req
.extensions()
.get::<gateway_identity::GatewayTags>()
.expect("tags attached");
assert_eq!(tags.person.as_deref(), Some("yves"));
assert_eq!(tags.project.as_deref(), Some("billing"));
}
#[test]
fn attach_gateway_tags_header_works_without_key_identity() {
let mut req = build_request(&[("x-leanctx-project", "side-quest")], "/v1/messages");
attach_gateway_tags(&mut req, gateway_identity::GatewayTags::default());
let tags = req
.extensions()
.get::<gateway_identity::GatewayTags>()
.expect("tags attached");
assert_eq!(tags.person, None);
assert_eq!(tags.project.as_deref(), Some("side-quest"));
}
#[test]
fn attach_gateway_tags_empty_leaves_no_extension() {
let mut req = build_request(&[], "/v1/messages");
attach_gateway_tags(&mut req, gateway_identity::GatewayTags::default());
assert!(
req.extensions()
.get::<gateway_identity::GatewayTags>()
.is_none()
);
}
#[test]
fn attach_gateway_tags_rejects_oversized_or_blank_header() {
let long = "p".repeat(200);
for bad in ["", " ", long.as_str()] {
let mut req = build_request(&[("x-leanctx-project", bad)], "/v1/messages");
attach_gateway_tags(
&mut req,
gateway_identity::GatewayTags {
person: Some("yves".into()),
team: None,
project: Some("default-proj".into()),
},
);
let tags = req
.extensions()
.get::<gateway_identity::GatewayTags>()
.expect("tags attached");
assert_eq!(
tags.project.as_deref(),
Some("default-proj"),
"bad header {bad:?} must not override"
);
}
}
#[test]
fn attach_gateway_tags_rejects_control_characters() {
let mut req = build_request(&[("x-leanctx-project", "bil\tling")], "/v1/messages");
attach_gateway_tags(
&mut req,
gateway_identity::GatewayTags {
person: Some("yves".into()),
team: None,
project: Some("default-proj".into()),
},
);
let tags = req
.extensions()
.get::<gateway_identity::GatewayTags>()
.expect("tags attached");
assert_eq!(
tags.project.as_deref(),
Some("default-proj"),
"control chars must not override"
);
}
#[test]
fn x_leanctx_project_never_forwarded_upstream() {
assert!(!forward::is_allowed_request_header("x-leanctx-project"));
}
#[test]
fn is_provider_route_bare_responses_and_messages() {
assert!(is_provider_route("/responses"));
assert!(is_provider_route("/responses/resp_123/input_items"));
assert!(is_provider_route("/messages"));
}
#[test]
fn canonical_provider_path_rewrites_bare_endpoints() {
assert_eq!(
canonical_provider_path("/responses").as_deref(),
Some("/v1/responses")
);
assert_eq!(
canonical_provider_path("/chat/completions").as_deref(),
Some("/v1/chat/completions")
);
assert_eq!(
canonical_provider_path("/messages").as_deref(),
Some("/v1/messages")
);
}
#[test]
fn canonical_provider_path_preserves_subpaths() {
assert_eq!(
canonical_provider_path("/responses/resp_abc/cancel").as_deref(),
Some("/v1/responses/resp_abc/cancel")
);
assert_eq!(
canonical_provider_path("/messages/batches/batch_1").as_deref(),
Some("/v1/messages/batches/batch_1")
);
}
#[test]
fn canonical_provider_path_ignores_already_canonical_and_unknown() {
assert_eq!(canonical_provider_path("/v1/responses"), None);
assert_eq!(canonical_provider_path("/v1/chat/completions"), None);
assert_eq!(canonical_provider_path("/health"), None);
assert_eq!(canonical_provider_path("/responsesx"), None);
assert_eq!(canonical_provider_path("/"), None);
}
#[test]
fn canonical_provider_path_collapses_double_v1_prefix() {
assert_eq!(
canonical_provider_path("/v1/v1/responses").as_deref(),
Some("/v1/responses")
);
assert_eq!(
canonical_provider_path("/v1/v1/chat/completions").as_deref(),
Some("/v1/chat/completions")
);
}
#[test]
fn normalized_provider_uri_rewrites_path_and_preserves_query() {
use axum::http::Uri;
let uri: Uri = "/responses?stream=true".parse().unwrap();
let rewritten = normalized_provider_uri(&uri).expect("bare /responses must rewrite");
assert_eq!(rewritten.path(), "/v1/responses");
assert_eq!(rewritten.query(), Some("stream=true"));
assert_eq!(
rewritten
.path_and_query()
.map(axum::http::uri::PathAndQuery::as_str),
Some("/v1/responses?stream=true")
);
}
#[test]
fn normalized_provider_uri_noop_for_canonical() {
use axum::http::Uri;
let uri: Uri = "/v1/responses".parse().unwrap();
assert!(normalized_provider_uri(&uri).is_none());
}
#[test]
fn resolved_bind_host_defaults_to_loopback_and_never_opens_on_typo() {
let _lock = crate::core::data_dir::test_env_lock();
crate::test_env::remove_var("LEAN_CTX_PROXY_BIND_HOST");
let cfg = crate::core::config::Config::default();
assert!(cfg.resolved_proxy_bind_host().is_loopback());
let typo = crate::core::config::Config {
proxy_bind_host: Some("all-interfaces-please".into()),
..Default::default()
};
assert!(typo.resolved_proxy_bind_host().is_loopback());
let open = crate::core::config::Config {
proxy_bind_host: Some("0.0.0.0".into()),
..Default::default()
};
assert!(!open.resolved_proxy_bind_host().is_loopback());
}
#[test]
fn host_allowed_loopback_always_passes_and_allowlist_extends() {
let allowed = vec!["gateway.example.com".to_string()];
for h in ["127.0.0.1", "127.0.0.1:4444", "localhost:9999", "[::1]:80"] {
assert!(host_allowed(h, &allowed), "loopback host must pass: {h}");
}
assert!(host_allowed("gateway.example.com", &allowed));
assert!(host_allowed("Gateway.Example.COM:443", &allowed));
assert!(host_allowed("gateway.example.com.:443", &allowed));
assert!(!host_allowed("evil.example.com", &allowed));
assert!(!host_allowed("gateway.example.com.evil.io", &allowed));
assert!(!host_allowed("gateway.example.com", &[]));
}
#[tokio::test]
async fn rate_limiter_enforces_burst_then_recovers() {
let limiter = RateLimiter::new(10, 3);
assert!(limiter.allow().await);
assert!(limiter.allow().await);
assert!(limiter.allow().await);
assert!(!limiter.allow().await, "burst of 3 must exhaust the bucket");
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
assert!(limiter.allow().await, "bucket must refill at max_rps");
}