use super::*;
fn ctx_with(header: &str, value: &str) -> CallContext {
CallContext::new("message/send").with_http_header(header, value)
}
#[test]
fn constant_time_eq_matches_and_rejects() {
assert!(constant_time_eq(b"abc", b"abc"));
assert!(!constant_time_eq(b"abc", b"abd"));
assert!(!constant_time_eq(b"abc", b"ab"));
assert!(constant_time_eq(b"", b""));
}
#[test]
fn extract_bearer_variants() {
assert_eq!(extract_bearer("Bearer tok"), Some("tok"));
assert_eq!(extract_bearer("bearer tok"), Some("tok"));
assert_eq!(extract_bearer("BEARER tok "), Some("tok"));
assert_eq!(extract_bearer("Basic tok"), None);
assert_eq!(extract_bearer("Bearer "), None);
assert_eq!(extract_bearer("Bearer"), None);
assert_eq!(extract_bearer(""), None);
}
#[tokio::test]
async fn api_key_accepts_allowed_and_rejects_others() {
let i = ApiKeyAuthInterceptor::new(["key-1", "key-2"]);
assert!(i.before(&ctx_with("x-api-key", "key-1")).await.is_ok());
assert!(i.before(&ctx_with("x-api-key", "key-2")).await.is_ok());
assert!(i.before(&ctx_with("x-api-key", "nope")).await.is_err());
assert!(i.before(&CallContext::new("m")).await.is_err());
}
#[tokio::test]
async fn api_key_custom_header() {
let i = ApiKeyAuthInterceptor::new(["k"]).with_header("X-Company-Key");
assert!(i.before(&ctx_with("x-company-key", "k")).await.is_ok());
assert!(i.before(&ctx_with("x-api-key", "k")).await.is_err());
}
#[tokio::test]
async fn bearer_accepts_allowed_and_rejects_others() {
let i = BearerTokenAuthInterceptor::new(["tok-a", "tok-b"]);
assert!(i
.before(&ctx_with("authorization", "Bearer tok-a"))
.await
.is_ok());
assert!(i
.before(&ctx_with("authorization", "bearer tok-b"))
.await
.is_ok());
assert!(i
.before(&ctx_with("authorization", "Bearer wrong"))
.await
.is_err());
assert!(i
.before(&ctx_with("authorization", "Basic tok-a"))
.await
.is_err());
assert!(i.before(&CallContext::new("m")).await.is_err());
}
#[tokio::test]
async fn rejection_message_is_generic() {
let i = BearerTokenAuthInterceptor::new(["tok"]);
let missing = i.before(&CallContext::new("m")).await.unwrap_err();
let wrong = i
.before(&ctx_with("authorization", "Bearer nope"))
.await
.unwrap_err();
assert_eq!(missing.message, wrong.message);
assert_eq!(missing.message, "authentication required");
}
#[test]
fn debug_impls_render_type_and_redact_secrets() {
let api = ApiKeyAuthInterceptor::new(["super-secret-api-key"]).with_header("X-Company-Key");
let api_dbg = format!("{api:?}");
assert!(
api_dbg.contains("ApiKeyAuthInterceptor"),
"ApiKey Debug: {api_dbg}"
);
assert!(
api_dbg.contains("x-company-key"),
"header name is shown (lowercased)"
);
assert!(
!api_dbg.contains("super-secret-api-key"),
"raw API keys must never appear in Debug output"
);
let bearer = BearerTokenAuthInterceptor::new(["super-secret-bearer-token"]);
let bearer_dbg = format!("{bearer:?}");
assert!(
bearer_dbg.contains("BearerTokenAuthInterceptor"),
"Bearer Debug: {bearer_dbg}"
);
assert!(
!bearer_dbg.contains("super-secret-bearer-token"),
"raw bearer tokens must never appear in Debug output"
);
}
#[test]
fn api_key_interceptor_declares_that_it_authenticates() {
let interceptor = ApiKeyAuthInterceptor::new(["k1"]);
assert!(
interceptor.authenticates(),
"an auth interceptor must declare itself as one, or a chain \
containing only it reports no authenticator"
);
let mut chain = crate::interceptor::ServerInterceptorChain::new();
chain.push(std::sync::Arc::new(ApiKeyAuthInterceptor::new(["k1"])));
assert!(
chain.has_authenticator(),
"a chain guarded by an API-key interceptor must satisfy the \
extended-agent-card authentication requirement"
);
}