use std::collections::BTreeMap; use wip_client::{ CallOutcome, Client, ClientErrorKind, ClientLimits, Completion, DiagnosticKind, ObservationState, OutcomeUnknownReason, SecurityContext, }; use wip_https::http::{Response, StatusCode}; use wip_https::{ Limits, encode_fetch_interface_response, encode_fetch_response, encode_protocol_error_response, }; use wip_protocol::{ FetchInterfaceRequest, FetchInterfaceResponse, FetchRequest, INTERFACE_FORMAT_V1, InterfaceDescriptor, Object, OperationDeclaration, ParameterDeclaration, ProtocolError, ProtocolErrorCode, ProtocolInteraction, ReturnDeclaration, TypeExpr, Value, }; fn limits() -> Limits { Limits::new(64 * 1024, 64 * 1024, 64, 64).unwrap() } fn setup() -> (Client, wip_client::SessionId) { let mut client = Client::new(ClientLimits::new(2, 16, 16, 16, 16).unwrap(), limits()); let session = client .open_session("https://example.test/wip", SecurityContext::new("subject")) .unwrap(); (client, session) } fn response(status: StatusCode, body: &[u8]) -> Response> { Response::builder() .status(status) .header("content-type", "application/json; charset=utf-8") .body(body.to_vec()) .unwrap() } fn object() -> Object { Object { name: "item".into(), description: None, interfaces: vec!["math".into()], r#ref: None, validator: Some(b"object-v1".to_vec()), } } fn descriptor(format: &str) -> InterfaceDescriptor { InterfaceDescriptor { format: format.into(), documentation: None, types: vec![], operations: vec![OperationDeclaration { name: "calculate".into(), documentation: None, parameters: vec![ParameterDeclaration { name: "value".into(), required: true, documentation: None, r#type: TypeExpr::Integer, }], returns: ReturnDeclaration { documentation: None, r#type: TypeExpr::Integer, }, }], } } #[test] fn malformed_json_unknown_fields_duplicates_and_invalid_success_are_invalid_response() { let cases: &[&[u8]] = &[ b"{not json", br#"{"name":"item","interfaces":[],"unknown":true}"#, br#"{"name":"item","interfaces":["math","math"]}"#, br#"{"name":"wrong","interfaces":[]}"#, ]; for body in cases { let (mut client, session) = setup(); let request = client.prepare_fetch(&session, "/item").unwrap(); let error = client .complete(request.id, response(StatusCode::OK, body)) .unwrap_err(); assert_eq!(error.kind, ClientErrorKind::InvalidResponse); assert!(matches!( client.object(&session, "/item").unwrap().state, ObservationState::Error(_) )); } } #[test] fn disallowed_code_and_status_success_contradictions_are_invalid_response() { let cases = [ ( StatusCode::NOT_FOUND, br#"{"error":{"code":"interface_not_found","message":"wrong"}}"#.as_slice(), ), ( StatusCode::OK, br#"{"error":{"code":"not_found","message":"wrong status"}}"#.as_slice(), ), ( StatusCode::CREATED, br#"{"name":"item","interfaces":[]}"#.as_slice(), ), ]; for (status, body) in cases { let (mut client, session) = setup(); let request = client.prepare_fetch(&session, "/item").unwrap(); assert_eq!( client .complete(request.id, response(status, body)) .unwrap_err() .kind, ClientErrorKind::InvalidResponse ); } } #[test] fn valid_error_envelope_is_authoritative_and_status_mismatch_is_diagnostic() { let (mut client, session) = setup(); let request = client.prepare_fetch(&session, "/item").unwrap(); 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!( client.complete(request.id, encoded).unwrap(), Completion::ProtocolFailure(error) ); assert_eq!( client.object(&session, "/item").unwrap().state, ObservationState::Stale ); assert_eq!( client.diagnostics(&session).unwrap().back().unwrap().kind, DiagnosticKind::HttpStatusMismatch ); } #[test] fn non_success_without_valid_envelope_remains_transport_failure() { let (mut client, session) = setup(); let request = client.prepare_fetch(&session, "/item").unwrap(); let error = client .complete( request.id, response(StatusCode::BAD_GATEWAY, b"gateway html"), ) .unwrap_err(); assert_eq!(error.kind, ClientErrorKind::Transport); } #[test] fn unsupported_descriptor_format_is_distinct_from_invalid_response() { let (mut client, session) = setup(); let request = client.prepare_interface(&session, "math").unwrap(); let logical = FetchInterfaceRequest { interface: "math".into(), }; let value = FetchInterfaceResponse { interface: "math".into(), descriptor: descriptor("future-format/2"), validator: Some(b"v2".to_vec()), }; let encoded = encode_fetch_interface_response(&logical, &value, limits()).unwrap(); let error = client.complete(request.id, encoded).unwrap_err(); assert_eq!(error.kind, ClientErrorKind::UnsupportedDescriptorFormat); } #[test] fn malformed_call_success_after_dispatch_is_outcome_unknown_and_stales_object() { let (mut client, session) = setup(); let fetch = client.prepare_fetch(&session, "/item").unwrap(); client .complete( fetch.id, encode_fetch_response( &FetchRequest { path: "/item".into(), }, &object(), limits(), ) .unwrap(), ) .unwrap(); let interface = client.prepare_interface(&session, "math").unwrap(); client .complete( interface.id, encode_fetch_interface_response( &FetchInterfaceRequest { interface: "math".into(), }, &FetchInterfaceResponse { interface: "math".into(), descriptor: descriptor(INTERFACE_FORMAT_V1), validator: None, }, limits(), ) .unwrap(), ) .unwrap(); let call = client .prepare_call( &session, "/item", "math", "calculate", BTreeMap::from([("value".into(), Value::Integer(2))]), ) .unwrap(); client.mark_dispatched(call.id).unwrap(); let completion = client .complete( call.id, response( StatusCode::OK, br#"{"result":"not an integer","unknown":true}"#, ), ) .unwrap(); assert!(matches!( completion, Completion::Call(record) if matches!( &record.outcome, CallOutcome::Unknown { reason: OutcomeUnknownReason::ResponseDecode, failure: Some(failure), } if failure.kind == ClientErrorKind::InvalidResponse ) )); assert_eq!( client.object(&session, "/item").unwrap().state, ObservationState::Stale ); assert_eq!(client.in_flight_len(), 0); }