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>) -> Response> { 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::::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(_) )); }