237 lines
7.2 KiB
Rust
237 lines
7.2 KiB
Rust
use wip_https::http::{Response, StatusCode};
|
|
use wip_https::wip_protocol::{
|
|
FetchRequest, ProtocolError, ProtocolErrorCode, ProtocolInteraction,
|
|
};
|
|
use wip_https::{
|
|
ClientResponseError, DecodedResponse, InvalidResponseKind, Limits, TransportFailureKind,
|
|
decode_fetch_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, 16).unwrap()
|
|
}
|
|
|
|
fn request() -> FetchRequest {
|
|
FetchRequest { path: "/".into() }
|
|
}
|
|
|
|
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::TOO_MANY_REQUESTS,
|
|
),
|
|
(
|
|
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 server_encoder_rejects_codes_disallowed_for_the_interaction() {
|
|
let error = ProtocolError {
|
|
code: ProtocolErrorCode::InterfaceNotFound,
|
|
message: "not valid for fetch".into(),
|
|
};
|
|
assert!(encode_protocol_error_response(ProtocolInteraction::Fetch, &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::Fetch, &error, limits()).unwrap();
|
|
*encoded.status_mut() = StatusCode::IM_A_TEAPOT;
|
|
|
|
assert_eq!(
|
|
decode_fetch_response(&request(), &encoded, limits()).unwrap(),
|
|
DecodedResponse::ProtocolFailure {
|
|
error,
|
|
status_mismatch: Some(wip_https::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_fetch_response(&request(), &response(StatusCode::OK, error_body), limits())
|
|
.unwrap_err();
|
|
assert!(matches!(
|
|
on_success,
|
|
ClientResponseError::InvalidResponse {
|
|
kind: InvalidResponseKind::ErrorEnvelopeOnSuccessStatus,
|
|
..
|
|
}
|
|
));
|
|
|
|
let success_body = br#"{"name":"","interfaces":[]}"#;
|
|
let on_error = decode_fetch_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_fetch_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_fetch_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_fetch_response(&request(), &malformed, limits()).unwrap_err(),
|
|
ClientResponseError::TransportBinding(_)
|
|
));
|
|
}
|