|
|
|
@@ -1,408 +0,0 @@
|
|
|
|
|
use wip_http::http::StatusCode;
|
|
|
|
|
use wip_http::wip_protocol::{
|
|
|
|
|
FetchInterfaceRequest, FetchInterfaceResponse, INTERFACE_FORMAT_V1, InterfaceDescriptor,
|
|
|
|
|
Object, ObjectObservation, ObserveRequest, ProtocolError, ProtocolErrorCode,
|
|
|
|
|
};
|
|
|
|
|
use wip_http::{
|
|
|
|
|
BatchMethod, BatchRequest, BatchRequestItem, BatchRequestKind, BatchResponse,
|
|
|
|
|
BatchResponseItem, BatchSuccess, ClientResponseError, DecodedResponse, Endpoint,
|
|
|
|
|
InvalidResponseKind, Limits, TransportFailureKind, decode_batch_request, decode_batch_response,
|
|
|
|
|
encode_batch_error_response, encode_batch_request, encode_batch_response,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fn limits() -> Limits {
|
|
|
|
|
Limits::new(100_000, 100_000, 64, 4).unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn observed_item(validator: Option<Vec<u8>>) -> ObjectObservation {
|
|
|
|
|
ObjectObservation {
|
|
|
|
|
object: Object {
|
|
|
|
|
name: "item".into(),
|
|
|
|
|
description: None,
|
|
|
|
|
interfaces: vec![],
|
|
|
|
|
r#ref: None,
|
|
|
|
|
validator,
|
|
|
|
|
},
|
|
|
|
|
children: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn request() -> BatchRequest {
|
|
|
|
|
BatchRequest {
|
|
|
|
|
requests: vec![
|
|
|
|
|
BatchRequestItem {
|
|
|
|
|
id: "object".into(),
|
|
|
|
|
request: BatchRequestKind::Observe(ObserveRequest {
|
|
|
|
|
path: "/item".into(),
|
|
|
|
|
depth: 0,
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
BatchRequestItem {
|
|
|
|
|
id: "interface".into(),
|
|
|
|
|
request: BatchRequestKind::FetchInterface(FetchInterfaceRequest {
|
|
|
|
|
interface: "interface-v1".into(),
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn batch_round_trips_by_unique_id_with_item_failures_and_validators() {
|
|
|
|
|
let request = request();
|
|
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let response = BatchResponse {
|
|
|
|
|
responses: vec![
|
|
|
|
|
BatchResponseItem {
|
|
|
|
|
id: "interface".into(),
|
|
|
|
|
outcome: Err(ProtocolError {
|
|
|
|
|
code: ProtocolErrorCode::InterfaceNotFound,
|
|
|
|
|
message: "gone".into(),
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
BatchResponseItem {
|
|
|
|
|
id: "object".into(),
|
|
|
|
|
outcome: Ok(BatchSuccess::Observe(observed_item(Some(vec![
|
|
|
|
|
0, 1, 2, 255,
|
|
|
|
|
])))),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
let encoded = encode_batch_response(&request, &response, limits()).unwrap();
|
|
|
|
|
assert_eq!(encoded.status(), StatusCode::OK);
|
|
|
|
|
assert!(
|
|
|
|
|
std::str::from_utf8(encoded.body())
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("AAEC/w==")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
decode_batch_response(&request, &encoded, limits()).unwrap(),
|
|
|
|
|
DecodedResponse::Success(response)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn batch_interface_validator_round_trips_in_item_body() {
|
|
|
|
|
let request = BatchRequest {
|
|
|
|
|
requests: vec![BatchRequestItem {
|
|
|
|
|
id: "interface".into(),
|
|
|
|
|
request: BatchRequestKind::FetchInterface(FetchInterfaceRequest {
|
|
|
|
|
interface: "interface-v1".into(),
|
|
|
|
|
}),
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
let response = BatchResponse {
|
|
|
|
|
responses: vec![BatchResponseItem {
|
|
|
|
|
id: "interface".into(),
|
|
|
|
|
outcome: Ok(BatchSuccess::FetchInterface(FetchInterfaceResponse {
|
|
|
|
|
interface: "interface-v1".into(),
|
|
|
|
|
descriptor: InterfaceDescriptor {
|
|
|
|
|
format: INTERFACE_FORMAT_V1.into(),
|
|
|
|
|
documentation: None,
|
|
|
|
|
types: vec![],
|
|
|
|
|
operations: vec![],
|
|
|
|
|
},
|
|
|
|
|
validator: Some(vec![255, 254, 253]),
|
|
|
|
|
})),
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
let encoded = encode_batch_response(&request, &response, limits()).unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
std::str::from_utf8(encoded.body())
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("//79")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
decode_batch_response(&request, &encoded, limits()).unwrap(),
|
|
|
|
|
DecodedResponse::Success(response)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn malformed_known_method_arguments_are_item_local() {
|
|
|
|
|
let body = br#"{
|
|
|
|
|
"requests": [
|
|
|
|
|
{"id":"valid","method":"observe","arguments":{"path":"/item","depth":0}},
|
|
|
|
|
{"id":"invalid","method":"observe","arguments":{"path":"/","depth":"deep"}}
|
|
|
|
|
]
|
|
|
|
|
}"#;
|
|
|
|
|
let decoded = decode_batch_request(body, limits()).unwrap();
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
decoded.requests[0].request,
|
|
|
|
|
BatchRequestKind::Observe(_)
|
|
|
|
|
));
|
|
|
|
|
let invalid = match &decoded.requests[1].request {
|
|
|
|
|
BatchRequestKind::Invalid { method, error } => {
|
|
|
|
|
assert_eq!(*method, BatchMethod::Observe);
|
|
|
|
|
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::Observe(observed_item(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":"observe","arguments":{"path":"/","depth":0},"extra":true}
|
|
|
|
|
]
|
|
|
|
|
}"#;
|
|
|
|
|
assert!(decode_batch_request(malformed_item, limits()).is_err());
|
|
|
|
|
|
|
|
|
|
let duplicate = br#"{
|
|
|
|
|
"requests": [
|
|
|
|
|
{"id":"same","method":"observe","arguments":{"path":"/a","depth":0}},
|
|
|
|
|
{"id":"same","method":"observe","arguments":{"path":"/b","depth":0}}
|
|
|
|
|
]
|
|
|
|
|
}"#;
|
|
|
|
|
assert!(decode_batch_request(duplicate, limits()).is_err());
|
|
|
|
|
|
|
|
|
|
let operation = br#"{
|
|
|
|
|
"requests": [
|
|
|
|
|
{"id":"call","method":"call_operation","arguments":{}}
|
|
|
|
|
]
|
|
|
|
|
}"#;
|
|
|
|
|
assert!(decode_batch_request(operation, limits()).is_err());
|
|
|
|
|
|
|
|
|
|
let too_many = Limits::new(100_000, 100_000, 64, 1).unwrap();
|
|
|
|
|
assert!(decode_batch_request(duplicate, too_many).is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn resource_limit_is_item_local_and_forbidden_as_a_top_level_batch_error() {
|
|
|
|
|
let error = ProtocolError {
|
|
|
|
|
code: ProtocolErrorCode::ResourceLimitExceeded,
|
|
|
|
|
message: "logical observation limit exceeded".into(),
|
|
|
|
|
};
|
|
|
|
|
assert!(encode_batch_error_response(&error, limits()).is_err());
|
|
|
|
|
|
|
|
|
|
let request = request();
|
|
|
|
|
let response = BatchResponse {
|
|
|
|
|
responses: vec![
|
|
|
|
|
BatchResponseItem {
|
|
|
|
|
id: "object".into(),
|
|
|
|
|
outcome: Err(error.clone()),
|
|
|
|
|
},
|
|
|
|
|
BatchResponseItem {
|
|
|
|
|
id: "interface".into(),
|
|
|
|
|
outcome: Err(ProtocolError {
|
|
|
|
|
code: ProtocolErrorCode::InterfaceNotFound,
|
|
|
|
|
message: "gone".into(),
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
let encoded = encode_batch_response(&request, &response, limits()).unwrap();
|
|
|
|
|
assert_eq!(encoded.status(), StatusCode::OK);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
decode_batch_response(&request, &encoded, limits()).unwrap(),
|
|
|
|
|
DecodedResponse::Success(response)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let top_level = wip_http::http::Response::builder()
|
|
|
|
|
.status(StatusCode::UNPROCESSABLE_ENTITY)
|
|
|
|
|
.header("content-type", "application/json; charset=utf-8")
|
|
|
|
|
.body(br#"{"error":{"code":"resource_limit_exceeded","message":"not allowed"}}"#.to_vec())
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
decode_batch_response(&request, &top_level, limits()).unwrap_err(),
|
|
|
|
|
ClientResponseError::InvalidResponse {
|
|
|
|
|
kind: InvalidResponseKind::InvalidBatch,
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn batch_transport_failures_do_not_become_protocol_errors() {
|
|
|
|
|
let request = request();
|
|
|
|
|
let bare_413 = wip_http::http::Response::builder()
|
|
|
|
|
.status(StatusCode::PAYLOAD_TOO_LARGE)
|
|
|
|
|
.body(Vec::<u8>::new())
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
decode_batch_response(&request, &bare_413, limits()).unwrap_err(),
|
|
|
|
|
ClientResponseError::TransportBinding(failure)
|
|
|
|
|
if failure.status == StatusCode::PAYLOAD_TOO_LARGE
|
|
|
|
|
&& failure.kind == TransportFailureKind::HttpStatus
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let mut encoded = encode_batch_response(
|
|
|
|
|
&request,
|
|
|
|
|
&BatchResponse {
|
|
|
|
|
responses: vec![
|
|
|
|
|
BatchResponseItem {
|
|
|
|
|
id: "object".into(),
|
|
|
|
|
outcome: Ok(BatchSuccess::Observe(observed_item(None))),
|
|
|
|
|
},
|
|
|
|
|
BatchResponseItem {
|
|
|
|
|
id: "interface".into(),
|
|
|
|
|
outcome: Err(ProtocolError {
|
|
|
|
|
code: ProtocolErrorCode::InterfaceNotFound,
|
|
|
|
|
message: "gone".into(),
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
limits(),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
encoded
|
|
|
|
|
.headers_mut()
|
|
|
|
|
.insert("content-encoding", "gzip".parse().unwrap());
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
decode_batch_response(&request, &encoded, limits()).unwrap_err(),
|
|
|
|
|
ClientResponseError::TransportBinding(failure)
|
|
|
|
|
if failure.status == StatusCode::OK
|
|
|
|
|
&& failure.kind == TransportFailureKind::UnsupportedContentCoding
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn batch_success_body_requires_exactly_http_200() {
|
|
|
|
|
let request = request();
|
|
|
|
|
let response = BatchResponse {
|
|
|
|
|
responses: vec![
|
|
|
|
|
BatchResponseItem {
|
|
|
|
|
id: "object".into(),
|
|
|
|
|
outcome: Ok(BatchSuccess::Observe(observed_item(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 {
|
|
|
|
|
code: ProtocolErrorCode::InvalidRequest,
|
|
|
|
|
message: "bad envelope".into(),
|
|
|
|
|
};
|
|
|
|
|
let encoded = encode_batch_error_response(&invalid, limits()).unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
decode_batch_response(&request(), &encoded, limits()).unwrap(),
|
|
|
|
|
DecodedResponse::ProtocolFailure {
|
|
|
|
|
error: invalid,
|
|
|
|
|
status_mismatch: None,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let not_found = wip_http::http::Response::builder()
|
|
|
|
|
.status(StatusCode::NOT_FOUND)
|
|
|
|
|
.header("content-type", "application/json; charset=utf-8")
|
|
|
|
|
.body(br#"{"error":{"code":"not_found","message":"not a batch failure"}}"#.to_vec())
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
decode_batch_response(&request(), ¬_found, limits()).unwrap_err(),
|
|
|
|
|
ClientResponseError::InvalidResponse {
|
|
|
|
|
kind: InvalidResponseKind::InvalidBatch,
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn unsupported_batch_descriptor_waits_for_envelope_correlation() {
|
|
|
|
|
let duplicate = wip_http::http::Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header("content-type", "application/json; charset=utf-8")
|
|
|
|
|
.body(
|
|
|
|
|
br#"{"responses":[{"id":"interface","result":{"interface":"interface-v1","descriptor":{"format":"wip-interface/2","future":true}}},{"id":"interface","result":{"interface":"interface-v1","descriptor":{"format":"wip-interface/2"}}}]}"#
|
|
|
|
|
.to_vec(),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
decode_batch_response(&request(), &duplicate, limits()).unwrap_err(),
|
|
|
|
|
ClientResponseError::InvalidResponse {
|
|
|
|
|
kind: InvalidResponseKind::InvalidBatch,
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let missing = wip_http::http::Response::builder()
|
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
|
.header("content-type", "application/json; charset=utf-8")
|
|
|
|
|
.body(
|
|
|
|
|
br#"{"responses":[{"id":"interface","result":{"interface":"interface-v1","descriptor":{"format":"wip-interface/2"}}}]}"#
|
|
|
|
|
.to_vec(),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
decode_batch_response(&request(), &missing, limits()).unwrap_err(),
|
|
|
|
|
ClientResponseError::InvalidResponse {
|
|
|
|
|
kind: InvalidResponseKind::InvalidBatch,
|
|
|
|
|
..
|
|
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let request = BatchRequest {
|
|
|
|
|
requests: vec![BatchRequestItem {
|
|
|
|
|
id: "interface".into(),
|
|
|
|
|
request: BatchRequestKind::FetchInterface(FetchInterfaceRequest {
|
|
|
|
|
interface: "interface-v1".into(),
|
|
|
|
|
}),
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
decode_batch_response(&request, &missing, limits()).unwrap_err(),
|
|
|
|
|
ClientResponseError::UnsupportedDescriptorFormat { format }
|
|
|
|
|
if format == "wip-interface/2"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn batch_response_must_have_exactly_the_requested_ids() {
|
|
|
|
|
let request = request();
|
|
|
|
|
let missing = BatchResponse {
|
|
|
|
|
responses: vec![BatchResponseItem {
|
|
|
|
|
id: "object".into(),
|
|
|
|
|
outcome: Ok(BatchSuccess::Observe(observed_item(None))),
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
assert!(encode_batch_response(&request, &missing, limits()).is_err());
|
|
|
|
|
}
|