feat: classify authorization-required errors (#1056)
This commit is contained in:
parent
3240b6e782
commit
65f05e9924
1 changed files with 142 additions and 0 deletions
85
crates/rmcp/tests/test_auth_error_classification.rs
Normal file
85
crates/rmcp/tests/test_auth_error_classification.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
use std::{any::TypeId, error::Error};
|
||||
|
||||
use rmcp::{
|
||||
service::ClientInitializeError,
|
||||
transport::{
|
||||
AuthError, DynamicTransportError,
|
||||
streamable_http_client::{AuthRequiredError, InsufficientScopeError, StreamableHttpError},
|
||||
},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
type TestHttpError = StreamableHttpError<std::io::Error>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("outer transport wrapper")]
|
||||
struct OuterError(#[source] TestHttpError);
|
||||
|
||||
fn initialization_error(error: impl Error + Send + Sync + 'static) -> ClientInitializeError {
|
||||
ClientInitializeError::TransportError {
|
||||
error: DynamicTransportError::from_parts(
|
||||
"test transport",
|
||||
TypeId::of::<()>(),
|
||||
Box::new(error),
|
||||
),
|
||||
context: "initialize".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_local_authorization_required() {
|
||||
let error = TestHttpError::Auth(AuthError::AuthorizationRequired);
|
||||
|
||||
assert!(initialization_error(error).is_authorization_required());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_http_authorization_challenge() {
|
||||
let error =
|
||||
TestHttpError::AuthRequired(AuthRequiredError::new("Bearer realm=\"mcp\"".to_owned()));
|
||||
|
||||
assert!(initialization_error(error).is_authorization_required());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_authorization_required_through_multiple_sources() {
|
||||
let error = OuterError(TestHttpError::Auth(AuthError::AuthorizationRequired));
|
||||
|
||||
assert!(initialization_error(error).is_authorization_required());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_classify_unrelated_transport_errors() {
|
||||
let closed = TestHttpError::TransportChannelClosed;
|
||||
let refresh = TestHttpError::Auth(AuthError::TokenRefreshFailed("timeout".to_owned()));
|
||||
let scope = TestHttpError::InsufficientScope(InsufficientScopeError::new(
|
||||
"Bearer error=\"insufficient_scope\"".to_owned(),
|
||||
Some("admin".to_owned()),
|
||||
));
|
||||
|
||||
assert!(!initialization_error(closed).is_authorization_required());
|
||||
assert!(!initialization_error(refresh).is_authorization_required());
|
||||
assert!(!initialization_error(scope).is_authorization_required());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_classify_non_transport_initialization_errors() {
|
||||
assert!(!ClientInitializeError::Cancelled.is_authorization_required());
|
||||
assert!(
|
||||
!ClientInitializeError::ConnectionClosed("server closed the connection".to_owned())
|
||||
.is_authorization_required()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_challenge_remains_available_as_an_error_source() {
|
||||
let error =
|
||||
TestHttpError::AuthRequired(AuthRequiredError::new("Bearer realm=\"mcp\"".to_owned()));
|
||||
|
||||
let source = error.source().expect("auth challenge should be a source");
|
||||
let challenge = source
|
||||
.downcast_ref::<AuthRequiredError>()
|
||||
.expect("source should retain the challenge type");
|
||||
|
||||
assert_eq!(challenge.www_authenticate_header, "Bearer realm=\"mcp\"");
|
||||
}
|
||||
Loading…
Reference in a new issue