fix(https): align canonical binding semantics
This commit is contained in:
@@ -4,9 +4,10 @@ use wip_https::wip_protocol::{
|
||||
InterfaceDescriptor, Object, ProtocolError, ProtocolErrorCode,
|
||||
};
|
||||
use wip_https::{
|
||||
BatchRequest, BatchRequestItem, BatchRequestKind, BatchResponse, BatchResponseItem,
|
||||
BatchSuccess, ClientResponseError, DecodedResponse, Endpoint, InvalidResponseKind, Limits,
|
||||
decode_batch_request, decode_batch_response, encode_batch_error_response, encode_batch_request,
|
||||
BatchMethod, BatchRequest, BatchRequestItem, BatchRequestKind, BatchResponse,
|
||||
BatchResponseItem, BatchSuccess, ClientResponseError, DecodedResponse, Endpoint,
|
||||
InvalidResponseKind, Limits, ResourceLimitKind, decode_batch_request, decode_batch_response,
|
||||
encode_batch_error_response, encode_batch_request, encode_batch_resource_limit_error_response,
|
||||
encode_batch_response,
|
||||
};
|
||||
|
||||
@@ -39,6 +40,7 @@ fn batch_round_trips_by_unique_id_with_item_failures_and_validators() {
|
||||
let endpoint = Endpoint::parse("https://example.test/wip").unwrap();
|
||||
let encoded_request = encode_batch_request(&endpoint, &request, limits()).unwrap();
|
||||
assert_eq!(encoded_request.uri().path(), "/wip/v1/batch");
|
||||
assert_eq!(encoded_request.headers()["accept"], "application/json");
|
||||
assert_eq!(
|
||||
decode_batch_request(encoded_request.body(), limits()).unwrap(),
|
||||
request
|
||||
@@ -115,8 +117,65 @@ fn batch_interface_validator_round_trips_in_item_body() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_known_method_arguments_are_item_local() {
|
||||
let body = br#"{
|
||||
"requests": [
|
||||
{"id":"valid","method":"fetch","arguments":{"path":"/item"}},
|
||||
{"id":"invalid","method":"fetch_tree","arguments":{"path":"/","depth":"deep","limit":1}}
|
||||
]
|
||||
}"#;
|
||||
let decoded = decode_batch_request(body, limits()).unwrap();
|
||||
assert!(matches!(
|
||||
decoded.requests[0].request,
|
||||
BatchRequestKind::Fetch(_)
|
||||
));
|
||||
let invalid = match &decoded.requests[1].request {
|
||||
BatchRequestKind::Invalid { method, error } => {
|
||||
assert_eq!(*method, BatchMethod::FetchTree);
|
||||
assert_eq!(error.code, ProtocolErrorCode::InvalidRequest);
|
||||
error.clone()
|
||||
}
|
||||
other => panic!("expected item-local invalid request, got {other:?}"),
|
||||
};
|
||||
|
||||
let response = BatchResponse {
|
||||
responses: vec![
|
||||
BatchResponseItem {
|
||||
id: "valid".into(),
|
||||
outcome: Ok(BatchSuccess::Fetch(Object {
|
||||
name: "item".into(),
|
||||
description: None,
|
||||
interfaces: vec![],
|
||||
r#ref: None,
|
||||
validator: None,
|
||||
})),
|
||||
},
|
||||
BatchResponseItem {
|
||||
id: "invalid".into(),
|
||||
outcome: Err(invalid),
|
||||
},
|
||||
],
|
||||
};
|
||||
let encoded = encode_batch_response(&decoded, &response, limits()).unwrap();
|
||||
assert_eq!(encoded.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
decode_batch_response(&decoded, &encoded, limits()).unwrap(),
|
||||
DecodedResponse::Success(response)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_batch_envelopes_are_rejected_at_top_level() {
|
||||
assert!(decode_batch_request(br#"{}"#, limits()).is_err());
|
||||
|
||||
let malformed_item = br#"{
|
||||
"requests": [
|
||||
{"id":"item","method":"fetch","arguments":{"path":"/"},"extra":true}
|
||||
]
|
||||
}"#;
|
||||
assert!(decode_batch_request(malformed_item, limits()).is_err());
|
||||
|
||||
let duplicate = br#"{
|
||||
"requests": [
|
||||
{"id":"same","method":"fetch","arguments":{"path":"/a"}},
|
||||
@@ -136,6 +195,67 @@ fn invalid_batch_envelopes_are_rejected_at_top_level() {
|
||||
assert!(decode_batch_request(duplicate, too_many).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_resource_limits_use_413_or_422_and_never_429() {
|
||||
let error = ProtocolError {
|
||||
code: ProtocolErrorCode::ResourceLimitExceeded,
|
||||
message: "bounded".into(),
|
||||
};
|
||||
for (kind, status) in [
|
||||
(
|
||||
ResourceLimitKind::RepresentationBytes,
|
||||
StatusCode::PAYLOAD_TOO_LARGE,
|
||||
),
|
||||
(ResourceLimitKind::Logical, StatusCode::UNPROCESSABLE_ENTITY),
|
||||
] {
|
||||
let encoded = encode_batch_resource_limit_error_response(&error, kind, limits()).unwrap();
|
||||
assert_eq!(encoded.status(), status);
|
||||
assert_ne!(encoded.status(), StatusCode::TOO_MANY_REQUESTS);
|
||||
assert_eq!(
|
||||
decode_batch_response(&request(), &encoded, limits()).unwrap(),
|
||||
DecodedResponse::ProtocolFailure {
|
||||
error: error.clone(),
|
||||
status_mismatch: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_success_body_requires_exactly_http_200() {
|
||||
let request = request();
|
||||
let response = BatchResponse {
|
||||
responses: vec![
|
||||
BatchResponseItem {
|
||||
id: "object".into(),
|
||||
outcome: Ok(BatchSuccess::Fetch(Object {
|
||||
name: "item".into(),
|
||||
description: None,
|
||||
interfaces: vec![],
|
||||
r#ref: None,
|
||||
validator: None,
|
||||
})),
|
||||
},
|
||||
BatchResponseItem {
|
||||
id: "interface".into(),
|
||||
outcome: Err(ProtocolError {
|
||||
code: ProtocolErrorCode::InterfaceNotFound,
|
||||
message: "gone".into(),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
let encoded = encode_batch_response(&request, &response, limits()).unwrap();
|
||||
for status in [StatusCode::CREATED, StatusCode::ACCEPTED] {
|
||||
let mut noncanonical = encoded.clone();
|
||||
*noncanonical.status_mut() = status;
|
||||
assert!(matches!(
|
||||
decode_batch_response(&request, &noncanonical, limits()).unwrap_err(),
|
||||
ClientResponseError::InvalidResponse { .. }
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_batch_errors_use_the_batch_allowed_code_set() {
|
||||
let invalid = ProtocolError {
|
||||
|
||||
Reference in New Issue
Block a user