357 lines
11 KiB
Rust
357 lines
11 KiB
Rust
use wip_http::http::{Response, StatusCode};
|
|
use wip_http::wip_protocol::{
|
|
ObserveRequest, ProtocolError, ProtocolErrorCode, ProtocolInteraction,
|
|
};
|
|
use wip_http::{
|
|
ClientResponseError, DecodedResponse, InvalidResponseKind, Limits, TransportFailureKind,
|
|
decode_observe_response, encode_protocol_error_response, error_code_name, is_json_content_type,
|
|
status_for_error,
|
|
};
|
|
|
|
fn limits() -> Limits {
|
|
Limits::new(64 * 1024, 64 * 1024, 32).unwrap()
|
|
}
|
|
|
|
fn request() -> ObserveRequest {
|
|
ObserveRequest {
|
|
path: "/".into(),
|
|
depth: 0,
|
|
}
|
|
}
|
|
|
|
fn response(status: StatusCode, body: impl Into<Vec<u8>>) -> Response<Vec<u8>> {
|
|
Response::builder()
|
|
.status(status)
|
|
.header("content-type", "application/json; charset=utf-8")
|
|
.body(body.into())
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn content_type_accepts_only_utf8_json_syntax() {
|
|
assert!(is_json_content_type("application/json"));
|
|
assert!(is_json_content_type("Application/JSON; Charset=\"UTF-8\""));
|
|
assert!(!is_json_content_type("application/json; charset=utf-16"));
|
|
assert!(!is_json_content_type("application/json; charset=\"utf-8"));
|
|
assert!(!is_json_content_type("application/json; charset=utf-8\""));
|
|
assert!(!is_json_content_type("application/json; profile=other"));
|
|
}
|
|
|
|
#[test]
|
|
fn every_protocol_code_has_stable_name_and_status() {
|
|
let expected = [
|
|
(
|
|
ProtocolErrorCode::InvalidRequest,
|
|
"invalid_request",
|
|
StatusCode::BAD_REQUEST,
|
|
),
|
|
(
|
|
ProtocolErrorCode::NotFound,
|
|
"not_found",
|
|
StatusCode::NOT_FOUND,
|
|
),
|
|
(
|
|
ProtocolErrorCode::InterfaceNotFound,
|
|
"interface_not_found",
|
|
StatusCode::NOT_FOUND,
|
|
),
|
|
(
|
|
ProtocolErrorCode::InterfaceMismatch,
|
|
"interface_mismatch",
|
|
StatusCode::CONFLICT,
|
|
),
|
|
(
|
|
ProtocolErrorCode::OperationNotFound,
|
|
"operation_not_found",
|
|
StatusCode::NOT_FOUND,
|
|
),
|
|
(
|
|
ProtocolErrorCode::InvalidArguments,
|
|
"invalid_arguments",
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
),
|
|
(
|
|
ProtocolErrorCode::ValidatorRequired,
|
|
"validator_required",
|
|
StatusCode::PRECONDITION_REQUIRED,
|
|
),
|
|
(
|
|
ProtocolErrorCode::ValidatorMismatch,
|
|
"validator_mismatch",
|
|
StatusCode::PRECONDITION_FAILED,
|
|
),
|
|
(
|
|
ProtocolErrorCode::InterfaceValidatorRequired,
|
|
"interface_validator_required",
|
|
StatusCode::PRECONDITION_REQUIRED,
|
|
),
|
|
(
|
|
ProtocolErrorCode::InterfaceValidatorMismatch,
|
|
"interface_validator_mismatch",
|
|
StatusCode::PRECONDITION_FAILED,
|
|
),
|
|
(
|
|
ProtocolErrorCode::PermissionDenied,
|
|
"permission_denied",
|
|
StatusCode::FORBIDDEN,
|
|
),
|
|
(
|
|
ProtocolErrorCode::ResourceLimitExceeded,
|
|
"resource_limit_exceeded",
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
),
|
|
(
|
|
ProtocolErrorCode::Internal,
|
|
"internal",
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
),
|
|
(
|
|
ProtocolErrorCode::OperationOutcomeUnknown,
|
|
"operation_outcome_unknown",
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
),
|
|
];
|
|
assert_eq!(expected.len(), ProtocolErrorCode::ALL.len());
|
|
for (code, name, status) in expected {
|
|
assert_eq!(error_code_name(code), name);
|
|
assert_eq!(status_for_error(code), status);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn logical_resource_limit_uses_only_422_and_bare_413_is_transport_failure() {
|
|
let error = ProtocolError {
|
|
code: ProtocolErrorCode::ResourceLimitExceeded,
|
|
message: "logical observation limit exceeded".into(),
|
|
};
|
|
let encoded =
|
|
encode_protocol_error_response(ProtocolInteraction::Observe, &error, limits()).unwrap();
|
|
assert_eq!(encoded.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
|
assert_eq!(
|
|
decode_observe_response(&request(), &encoded, limits()).unwrap(),
|
|
DecodedResponse::ProtocolFailure {
|
|
error: error.clone(),
|
|
status_mismatch: None,
|
|
}
|
|
);
|
|
|
|
let mut noncanonical = encoded;
|
|
*noncanonical.status_mut() = StatusCode::PAYLOAD_TOO_LARGE;
|
|
assert_eq!(
|
|
decode_observe_response(&request(), &noncanonical, limits()).unwrap(),
|
|
DecodedResponse::ProtocolFailure {
|
|
error,
|
|
status_mismatch: Some(wip_http::StatusMismatch {
|
|
actual: StatusCode::PAYLOAD_TOO_LARGE,
|
|
expected: StatusCode::UNPROCESSABLE_ENTITY,
|
|
}),
|
|
}
|
|
);
|
|
|
|
let bare_413 = Response::builder()
|
|
.status(StatusCode::PAYLOAD_TOO_LARGE)
|
|
.body(Vec::<u8>::new())
|
|
.unwrap();
|
|
assert!(matches!(
|
|
decode_observe_response(&request(), &bare_413, limits()).unwrap_err(),
|
|
ClientResponseError::TransportBinding(failure)
|
|
if failure.status == StatusCode::PAYLOAD_TOO_LARGE
|
|
&& failure.kind == TransportFailureKind::HttpStatus
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn rate_limit_status_without_protocol_envelope_stays_transport_failure() {
|
|
let response = Response::builder()
|
|
.status(StatusCode::TOO_MANY_REQUESTS)
|
|
.body(b"rate limited".to_vec())
|
|
.unwrap();
|
|
assert!(matches!(
|
|
decode_observe_response(&request(), &response, limits()).unwrap_err(),
|
|
ClientResponseError::TransportBinding(failure)
|
|
if failure.status == StatusCode::TOO_MANY_REQUESTS
|
|
&& failure.kind == TransportFailureKind::HttpStatus
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_response_content_coding_is_transport_failure() {
|
|
let mut encoded = encode_protocol_error_response(
|
|
ProtocolInteraction::Observe,
|
|
&ProtocolError {
|
|
code: ProtocolErrorCode::NotFound,
|
|
message: "missing".into(),
|
|
},
|
|
limits(),
|
|
)
|
|
.unwrap();
|
|
encoded
|
|
.headers_mut()
|
|
.insert("content-encoding", "gzip".parse().unwrap());
|
|
assert!(matches!(
|
|
decode_observe_response(&request(), &encoded, limits()).unwrap_err(),
|
|
ClientResponseError::TransportBinding(failure)
|
|
if failure.status == StatusCode::NOT_FOUND
|
|
&& failure.kind == TransportFailureKind::UnsupportedContentCoding
|
|
));
|
|
|
|
let success = response(StatusCode::OK, br#"{"object":{"name":"","interfaces":[]}}"#);
|
|
let mut identity = success.clone();
|
|
identity
|
|
.headers_mut()
|
|
.insert("content-encoding", "identity".parse().unwrap());
|
|
assert!(matches!(
|
|
decode_observe_response(&request(), &identity, limits()).unwrap(),
|
|
DecodedResponse::Success(_)
|
|
));
|
|
|
|
let mut unsupported = success;
|
|
unsupported
|
|
.headers_mut()
|
|
.insert("content-encoding", "br".parse().unwrap());
|
|
assert!(matches!(
|
|
decode_observe_response(&request(), &unsupported, limits()).unwrap_err(),
|
|
ClientResponseError::TransportBinding(failure)
|
|
if failure.status == StatusCode::OK
|
|
&& failure.kind == TransportFailureKind::UnsupportedContentCoding
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn valid_success_body_requires_exactly_http_200() {
|
|
let success_body = br#"{"object":{"name":"","interfaces":[]}}"#;
|
|
for status in [StatusCode::CREATED, StatusCode::ACCEPTED] {
|
|
assert!(matches!(
|
|
decode_observe_response(&request(), &response(status, success_body), limits())
|
|
.unwrap_err(),
|
|
ClientResponseError::InvalidResponse {
|
|
kind: InvalidResponseKind::NonCanonicalSuccessStatus,
|
|
..
|
|
}
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn server_encoder_rejects_codes_disallowed_for_the_interaction() {
|
|
let error = ProtocolError {
|
|
code: ProtocolErrorCode::InterfaceNotFound,
|
|
message: "not valid for observe".into(),
|
|
};
|
|
assert!(
|
|
encode_protocol_error_response(ProtocolInteraction::Observe, &error, limits()).is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn valid_envelope_code_is_authoritative_when_status_mismatches() {
|
|
let error = ProtocolError {
|
|
code: ProtocolErrorCode::NotFound,
|
|
message: "missing".into(),
|
|
};
|
|
let mut encoded =
|
|
encode_protocol_error_response(ProtocolInteraction::Observe, &error, limits()).unwrap();
|
|
*encoded.status_mut() = StatusCode::IM_A_TEAPOT;
|
|
|
|
assert_eq!(
|
|
decode_observe_response(&request(), &encoded, limits()).unwrap(),
|
|
DecodedResponse::ProtocolFailure {
|
|
error,
|
|
status_mismatch: Some(wip_http::StatusMismatch {
|
|
actual: StatusCode::IM_A_TEAPOT,
|
|
expected: StatusCode::NOT_FOUND,
|
|
}),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn envelope_and_status_contradictions_are_invalid_responses() {
|
|
let error_body = br#"{"error":{"code":"not_found","message":"missing"}}"#;
|
|
let on_success =
|
|
decode_observe_response(&request(), &response(StatusCode::OK, error_body), limits())
|
|
.unwrap_err();
|
|
assert!(matches!(
|
|
on_success,
|
|
ClientResponseError::InvalidResponse {
|
|
kind: InvalidResponseKind::ErrorEnvelopeOnSuccessStatus,
|
|
..
|
|
}
|
|
));
|
|
|
|
let success_body = br#"{"object":{"name":"","interfaces":[]}}"#;
|
|
let on_error = decode_observe_response(
|
|
&request(),
|
|
&response(StatusCode::BAD_GATEWAY, success_body),
|
|
limits(),
|
|
)
|
|
.unwrap_err();
|
|
assert!(matches!(
|
|
on_error,
|
|
ClientResponseError::InvalidResponse {
|
|
kind: InvalidResponseKind::SuccessBodyOnErrorStatus,
|
|
..
|
|
}
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn disallowed_protocol_code_is_invalid_instead_of_reclassified() {
|
|
let body = br#"{"error":{"code":"interface_not_found","message":"wrong interaction"}}"#;
|
|
let error =
|
|
decode_observe_response(&request(), &response(StatusCode::NOT_FOUND, body), limits())
|
|
.unwrap_err();
|
|
assert!(matches!(
|
|
error,
|
|
ClientResponseError::InvalidResponse {
|
|
kind: InvalidResponseKind::DisallowedProtocolCode,
|
|
..
|
|
}
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn proxy_authentication_and_endpoint_failures_stay_transport_failures() {
|
|
let cases = [
|
|
(
|
|
StatusCode::BAD_GATEWAY,
|
|
TransportFailureKind::HttpStatus,
|
|
b"proxy failure".as_slice(),
|
|
),
|
|
(
|
|
StatusCode::UNAUTHORIZED,
|
|
TransportFailureKind::AuthenticationRequired,
|
|
b"login required".as_slice(),
|
|
),
|
|
(
|
|
StatusCode::FORBIDDEN,
|
|
TransportFailureKind::EndpointForbidden,
|
|
b"denied".as_slice(),
|
|
),
|
|
];
|
|
for (status, expected_kind, body) in cases {
|
|
let response = Response::builder()
|
|
.status(status)
|
|
.body(body.to_vec())
|
|
.unwrap();
|
|
let error = decode_observe_response(&request(), &response, limits()).unwrap_err();
|
|
assert!(matches!(
|
|
error,
|
|
ClientResponseError::TransportBinding(failure)
|
|
if failure.status == status && failure.kind == expected_kind
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_non_success_envelope_is_transport_binding_failure() {
|
|
let malformed = response(
|
|
StatusCode::BAD_GATEWAY,
|
|
br#"{"error":{"code":"not_found","message":null}}"#,
|
|
);
|
|
assert!(matches!(
|
|
decode_observe_response(&request(), &malformed, limits()).unwrap_err(),
|
|
ClientResponseError::TransportBinding(_)
|
|
));
|
|
}
|