You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
synctv/synctv-cluster/tests/cluster_auth_interceptor_te...

186 lines
5.8 KiB
Rust

//! `ClusterAuthInterceptor` tests
//!
//! - Correct secret passes
//! - Missing header -> unauthenticated
//! - Wrong secret -> unauthenticated
//! - `constant_time_eq`: equal, not-equal same length, different lengths
#![allow(clippy::unwrap_used)]
use tonic::Request;
use synctv_cluster::grpc::ClusterAuthInterceptor;
/// Helper: create a Request with a specific x-cluster-secret value.
fn make_request_with_secret(secret: &str) -> Request<()> {
let mut request = Request::new(());
request.metadata_mut().insert(
synctv_cluster::grpc::CLUSTER_SECRET_METADATA_KEY,
secret.parse().unwrap(),
);
request
}
/// Helper: create a Request with no x-cluster-secret header.
fn make_request_no_secret() -> Request<()> {
Request::new(())
}
// Correct secret passes
#[test]
fn test_correct_secret_passes() {
let interceptor = ClusterAuthInterceptor::new("my-secret-key".to_string());
let request = make_request_with_secret("my-secret-key");
let result = interceptor.validate(request);
assert!(result.is_ok(), "Correct secret should pass validation");
}
// Missing header -> unauthenticated
#[test]
fn test_missing_header_unauthenticated() {
let interceptor = ClusterAuthInterceptor::new("my-secret-key".to_string());
let request = make_request_no_secret();
let result = interceptor.validate(request);
assert!(result.is_err(), "Missing header should fail");
let status = result.unwrap_err();
assert_eq!(
status.code(),
tonic::Code::Unauthenticated,
"Error code should be Unauthenticated"
);
assert!(
status.message().contains("Missing"),
"Error message should mention missing header, got: {}",
status.message()
);
}
// Wrong secret -> unauthenticated
#[test]
fn test_wrong_secret_unauthenticated() {
let interceptor = ClusterAuthInterceptor::new("correct-secret".to_string());
let request = make_request_with_secret("wrong-secret");
let result = interceptor.validate(request);
assert!(result.is_err(), "Wrong secret should fail");
let status = result.unwrap_err();
assert_eq!(
status.code(),
tonic::Code::Unauthenticated,
"Error code should be Unauthenticated"
);
assert!(
status.message().contains("Invalid"),
"Error message should mention invalid secret, got: {}",
status.message()
);
}
// constant_time_eq tests
/// We test the `constant_time_eq` behavior through the interceptor by verifying
/// that the comparison is correct for equal, not-equal same-length, and
/// different-length secrets.
#[test]
fn test_constant_time_eq_same_secret() {
let interceptor = ClusterAuthInterceptor::new("abc123".to_string());
let request = make_request_with_secret("abc123");
assert!(interceptor.validate(request).is_ok());
}
#[test]
fn test_constant_time_eq_different_same_length() {
let interceptor = ClusterAuthInterceptor::new("abc123".to_string());
let request = make_request_with_secret("xyz789");
assert!(
interceptor.validate(request).is_err(),
"Different secret of same length should fail"
);
}
#[test]
fn test_constant_time_eq_different_lengths() {
let interceptor = ClusterAuthInterceptor::new("short".to_string());
let request = make_request_with_secret("this-is-a-much-longer-secret");
assert!(
interceptor.validate(request).is_err(),
"Different length secrets should fail"
);
}
#[test]
fn test_constant_time_eq_longer_expected() {
let interceptor = ClusterAuthInterceptor::new("this-is-a-longer-expected-secret".to_string());
let request = make_request_with_secret("short");
assert!(
interceptor.validate(request).is_err(),
"Shorter provided secret should fail"
);
}
#[test]
fn test_constant_time_eq_empty_secret() {
let interceptor = ClusterAuthInterceptor::new(String::new());
let request = make_request_with_secret("");
let status = interceptor
.validate(request)
.expect_err("empty configured secret must fail closed");
assert_eq!(status.code(), tonic::Code::Unauthenticated);
assert!(
status.message().contains("not configured"),
"misconfiguration error should be explicit, got: {}",
status.message()
);
}
#[test]
fn test_constant_time_eq_one_empty() {
let interceptor = ClusterAuthInterceptor::new("notempty".to_string());
let request = make_request_with_secret("");
assert!(
interceptor.validate(request).is_err(),
"Empty provided vs non-empty expected should fail"
);
}
/// Test that the Debug impl does not leak the secret.
#[test]
fn test_debug_does_not_leak_secret() {
let interceptor = ClusterAuthInterceptor::new("super-secret-value".to_string());
let debug_output = format!("{interceptor:?}");
assert!(
!debug_output.contains("super-secret-value"),
"Debug output should not contain the secret, got: {debug_output}"
);
}
/// Test with a non-ASCII metadata value (binary/garbage) -> Unauthenticated.
#[test]
fn test_invalid_metadata_value_unauthenticated() {
let interceptor = ClusterAuthInterceptor::new("valid-secret".to_string());
// Insert a binary metadata value
let mut request = Request::new(());
let metadata = request.metadata_mut();
// Use MetadataMap's insert_bin for binary metadata - but x-cluster-secret
// is not a -bin suffix key, so invalid UTF-8 would fail to_str().
// We can test this by manually inserting a valid ASCII but wrong value.
metadata.insert(
synctv_cluster::grpc::CLUSTER_SECRET_METADATA_KEY,
"valid\x01secret".parse().unwrap_or_else(|_| {
// If parsing fails (non-visible ASCII), just use a regular wrong value
"wrong".parse().unwrap()
}),
);
let result = interceptor.validate(request);
assert!(result.is_err(), "Invalid/wrong secret should fail");
}