use super::*;
fn ctx_with_header(header: &str, value: &str) -> CallContext {
CallContext::new("SendMessage").with_http_header(header, value)
}
#[tokio::test]
async fn a_labelled_api_key_names_its_caller() {
let auth =
ApiKeyAuthInterceptor::with_labelled_keys([("key-acme", "acme"), ("key-globex", "globex")]);
let ctx = ctx_with_header("x-api-key", "key-globex");
auth.before(&ctx).await.expect("a valid key is accepted");
assert_eq!(
ctx.caller_identity(),
Some("globex"),
"the second key's label, so the match is by credential and not by position"
);
}
#[tokio::test]
async fn the_credential_never_becomes_the_identity() {
let auth = ApiKeyAuthInterceptor::with_labelled_keys([("sekrit-key", "acme")]);
let ctx = ctx_with_header("x-api-key", "sekrit-key");
auth.before(&ctx).await.expect("accepted");
let identity = ctx.caller_identity().expect("an identity was recorded");
assert_eq!(identity, "acme");
assert!(
!identity.contains("sekrit"),
"the credential must not appear in the identity"
);
}
#[tokio::test]
async fn an_unlabelled_key_authenticates_without_naming_anyone() {
let auth = ApiKeyAuthInterceptor::new(["plain-key"]);
let ctx = ctx_with_header("x-api-key", "plain-key");
auth.before(&ctx).await.expect("accepted");
assert_eq!(ctx.caller_identity(), None);
}
#[tokio::test]
async fn a_rejected_key_names_nobody() {
let auth = ApiKeyAuthInterceptor::with_labelled_keys([("key-acme", "acme")]);
let ctx = ctx_with_header("x-api-key", "wrong-key");
auth.before(&ctx).await.expect_err("rejected");
assert_eq!(
ctx.caller_identity(),
None,
"a failed authentication must not establish an identity"
);
}
#[tokio::test]
async fn a_labelled_bearer_token_names_its_caller() {
let auth = BearerTokenAuthInterceptor::with_labelled_tokens([
("tok-a", "team-a"),
("tok-b", "team-b"),
]);
let ctx = ctx_with_header("authorization", "Bearer tok-b");
auth.before(&ctx).await.expect("accepted");
assert_eq!(ctx.caller_identity(), Some("team-b"));
}
#[tokio::test]
async fn authentication_gives_each_caller_its_own_budget() {
use crate::interceptor::ServerInterceptorChain;
use crate::rate_limit::{RateLimitConfig, RateLimitInterceptor};
use std::sync::Arc;
const LIMIT: u64 = 2;
let mut chain = ServerInterceptorChain::new();
chain.push(Arc::new(ApiKeyAuthInterceptor::with_labelled_keys([
("key-acme", "acme"),
("key-globex", "globex"),
])));
chain.push(Arc::new(
RateLimitInterceptor::new(RateLimitConfig {
requests_per_window: LIMIT,
window_secs: 300,
..RateLimitConfig::default()
})
.expect("limiter builds"),
));
let mut acme_admitted = 0;
for _ in 0..=LIMIT {
let ctx = ctx_with_header("x-api-key", "key-acme");
if chain.run_before(&ctx).await.is_ok() {
acme_admitted += 1;
}
}
assert_eq!(acme_admitted, LIMIT, "acme is limited to its own budget");
let ctx = ctx_with_header("x-api-key", "key-globex");
chain
.run_before(&ctx)
.await
.expect("a different caller has a different budget");
assert_eq!(ctx.caller_identity(), Some("globex"));
}
#[test]
fn the_matcher_distinguishes_no_match_from_an_unlabelled_match() {
let allowed = vec![
(b"labelled".to_vec(), Some("name".to_string())),
(b"bare".to_vec(), None),
];
assert_eq!(
labelled_constant_time_match(b"labelled", &allowed),
CredentialMatch::Named("name")
);
assert_eq!(
labelled_constant_time_match(b"bare", &allowed),
CredentialMatch::Unnamed,
"matched, but nameless — not the same as no match"
);
assert_eq!(
labelled_constant_time_match(b"absent", &allowed),
CredentialMatch::NoMatch
);
}
#[test]
fn the_matcher_returns_the_label_of_the_entry_that_matched() {
let allowed = vec![
(b"first".to_vec(), Some("one".to_string())),
(b"second".to_vec(), Some("two".to_string())),
(b"third".to_vec(), Some("three".to_string())),
];
assert_eq!(
labelled_constant_time_match(b"first", &allowed),
CredentialMatch::Named("one")
);
assert_eq!(
labelled_constant_time_match(b"second", &allowed),
CredentialMatch::Named("two")
);
assert_eq!(
labelled_constant_time_match(b"third", &allowed),
CredentialMatch::Named("three")
);
}
#[test]
fn the_matcher_handles_an_empty_allow_list() {
assert_eq!(
labelled_constant_time_match(b"anything", &[]),
CredentialMatch::NoMatch
);
}