//! Public API tests: no HTTP types, codec, endpoint parser, or JSON dependency. use std::collections::BTreeMap; use wip_client::direct::{Client, PreparedRequest, Request, Response}; use wip_client::{ CallOutcome, ClientErrorKind, ClientLimits, Completion, DispatchedTransportFailure, ObservationState, OutcomeUnknownReason, SecurityContext, SessionId, }; use wip_protocol::{ CallOperationResponse, FetchInterfaceResponse, INTERFACE_FORMAT_V1, InterfaceDescriptor, InterfaceReference, Object, ObjectObservation, OperationDeclaration, ParameterDeclaration, ProtocolError, ProtocolErrorCode, ReturnDeclaration, TypeExpr, Value, }; fn setup() -> (Client, SessionId) { let mut client = Client::new(ClientLimits::new(4, 16, 8, 8, 4).unwrap()); let session = client .open_session("in-process world", SecurityContext::new("alice")) .unwrap(); (client, session) } fn reference() -> InterfaceReference { InterfaceReference { scope: "/scope".into(), name: "math".into(), } } fn object(name: &str, identity: Option<&str>) -> ObjectObservation { ObjectObservation { object: Object { name: name.into(), description: None, interfaces: vec![reference()], r#ref: identity.map(str::to_owned), validator: None, }, children: None, } } fn descriptor() -> InterfaceDescriptor { InterfaceDescriptor { format: INTERFACE_FORMAT_V1.into(), documentation: None, types: vec![], operations: vec![OperationDeclaration { name: "double".into(), documentation: None, parameters: vec![ParameterDeclaration { name: "n".into(), documentation: None, required: true, r#type: TypeExpr::Integer, }], returns: ReturnDeclaration { documentation: None, r#type: TypeExpr::Integer, }, }], } } fn interface(scope_ref: Option<&str>) -> FetchInterfaceResponse { FetchInterfaceResponse { interface: reference(), scope_ref: scope_ref.map(str::to_owned), validator: None, descriptor: descriptor(), } } fn failure(code: ProtocolErrorCode) -> ProtocolError { ProtocolError { code, message: "test failure".into(), } } fn observe(client: &mut Client, session: &SessionId, path: &str, value: ObjectObservation) { let request = client.refresh_object(session, path).unwrap(); assert!( matches!(&request.request, Request::Observe(value) if value.path == path && value.depth == 0) ); client .complete(request.id, Response::Observe(Ok(value))) .unwrap(); } fn ready(client: &mut Client, session: &SessionId) { observe(client, session, "/scope/item", object("item", None)); let request = client.prepare_interface(session, reference()).unwrap(); assert!( matches!(request.request, Request::FetchInterface(value) if value.interface == reference()) ); client .complete(request.id, Response::FetchInterface(Ok(interface(None)))) .unwrap(); } fn call(client: &mut Client, session: &SessionId) -> PreparedRequest { client .prepare_call( session, "/scope/item", &reference(), "double", BTreeMap::from([("n".into(), Value::Integer(3))]), ) .unwrap() } #[test] fn typed_interactions_succeed_with_optional_identity_and_validators_absent() { let (mut client, session) = setup(); ready(&mut client, &session); assert!( client .ensure_observed(&session, "/scope/item", 0) .unwrap() .is_none() ); assert!( client .ensure_interface(&session, reference()) .unwrap() .is_none() ); let request = call(&mut client, &session); let Request::CallOperation(logical) = request.request else { panic!("wrong request") }; assert_eq!(logical.target.path, "/scope/item"); assert_eq!(logical.target.validator, None); assert_eq!(logical.interface.scope_ref, None); assert_eq!(logical.interface.validator, None); assert_eq!(logical.arguments["n"], Value::Integer(3)); client.mark_dispatched(request.id).unwrap(); let result = CallOperationResponse { result: Value::Integer(6), validator: Some(vec![2]), }; let completion = client .complete(request.id, Response::CallOperation(Ok(result.clone()))) .unwrap(); assert!( matches!(completion, Completion::Call(record) if record.outcome == CallOutcome::Success(result)) ); assert_eq!( client.object(&session, "/scope/item").unwrap().validator, Some(vec![2]) ); assert_eq!(client.in_flight_len(), 0); assert_eq!( client .complete( request.id, Response::CallOperation(Ok(CallOperationResponse { result: Value::Integer(6), validator: None, })) ) .unwrap_err() .kind, ClientErrorKind::InvalidRequest ); } #[test] fn worldspace_and_security_identities_partition_observations_without_urls() { let (mut client, session) = setup(); ready(&mut client, &session); let other_world = client .open_session("another world", SecurityContext::new("alice")) .unwrap(); let other_security = client .open_session("in-process world", SecurityContext::new("bob")) .unwrap(); for other in [&other_world, &other_security] { assert!(client.object(other, "/scope/item").is_none()); assert!(client.interface(other, &reference()).is_none()); } assert_eq!( client .open_session("in-process world", SecurityContext::new("alice")) .unwrap(), session ); } #[test] fn typed_observations_validate_name_depth_duplicate_children_and_interface_scope() { let (mut client, session) = setup(); let mut wrong_scope = object("item", None); wrong_scope.object.interfaces[0].scope = "/elsewhere".into(); let mut unexpected_children = object("item", None); unexpected_children.children = Some(vec![]); let mut duplicate = object("item", None); duplicate.children = Some(vec![object("child", None), object("child", None)]); for (depth, response) in [ (0, object("wrong-name", None)), (1, object("item", None)), (0, unexpected_children), (0, wrong_scope), (1, duplicate), ] { let request = client .refresh_observed(&session, "/scope/item", depth) .unwrap(); let error = client .complete(request.id, Response::Observe(Ok(response))) .unwrap_err(); assert_eq!(error.kind, ClientErrorKind::InvalidResponse); assert!(matches!( client.object(&session, "/scope/item").unwrap().state, ObservationState::Error(_) )); } } #[test] fn typed_descriptor_correspondence_structure_and_format_are_checked() { let (mut client, session) = setup(); let mut mismatched = interface(None); mismatched.interface.name = "other".into(); let mut malformed = interface(None); malformed .descriptor .operations .push(malformed.descriptor.operations[0].clone()); let mut unsupported = interface(None); unsupported.descriptor.format = "future".into(); for (value, kind) in [ (mismatched, ClientErrorKind::InvalidResponse), (malformed, ClientErrorKind::InvalidResponse), (unsupported, ClientErrorKind::UnsupportedDescriptorFormat), ] { let request = client.prepare_interface(&session, reference()).unwrap(); assert_eq!( client .complete(request.id, Response::FetchInterface(Ok(value))) .unwrap_err() .kind, kind ); } } #[test] fn typed_arguments_and_results_use_the_fixed_descriptor() { let (mut client, session) = setup(); ready(&mut client, &session); let invalid = client .prepare_call( &session, "/scope/item", &reference(), "double", BTreeMap::from([("n".into(), Value::String("not integer".into()))]), ) .unwrap_err(); assert_eq!(invalid.kind, ClientErrorKind::InvalidRequest); assert_eq!(client.in_flight_len(), 0); let request = call(&mut client, &session); client.mark_dispatched(request.id).unwrap(); let completion = client .complete( request.id, Response::CallOperation(Ok(CallOperationResponse { result: Value::String("not integer".into()), validator: None, })), ) .unwrap(); assert!( matches!(completion, Completion::Call(record) if matches!(record.outcome, CallOutcome::Unknown { reason: OutcomeUnknownReason::ResponseDecode, .. })) ); assert_eq!( client.object(&session, "/scope/item").unwrap().state, ObservationState::Stale ); assert!( client .ensure_observed(&session, "/scope/item", 0) .unwrap() .is_none() ); assert_eq!(client.in_flight_len(), 0); } #[test] fn typed_protocol_failures_validate_interaction_and_apply_recovery() { let (mut client, session) = setup(); let request = client.refresh_object(&session, "/scope/item").unwrap(); assert!(matches!( client .complete( request.id, Response::Observe(Err(failure(ProtocolErrorCode::NotFound))) ) .unwrap(), Completion::ProtocolFailure(_) )); let request = client.prepare_interface(&session, reference()).unwrap(); assert!(matches!( client .complete( request.id, Response::FetchInterface(Err(failure(ProtocolErrorCode::InterfaceNotFound))) ) .unwrap(), Completion::ProtocolFailure(_) )); let request = client.refresh_object(&session, "/scope/item").unwrap(); assert_eq!( client .complete( request.id, Response::Observe(Err(failure(ProtocolErrorCode::OperationOutcomeUnknown))) ) .unwrap_err() .kind, ClientErrorKind::InvalidResponse ); let request = client.prepare_interface(&session, reference()).unwrap(); assert_eq!( client .complete( request.id, Response::FetchInterface(Err(failure(ProtocolErrorCode::ValidatorMismatch))) ) .unwrap_err() .kind, ClientErrorKind::InvalidResponse ); ready(&mut client, &session); let request = call(&mut client, &session); let completion = client .complete( request.id, Response::CallOperation(Err(failure(ProtocolErrorCode::InterfaceMismatch))), ) .unwrap(); assert!( matches!(completion, Completion::Call(record) if matches!(record.outcome, CallOutcome::ProtocolFailure(_))) ); assert_eq!( client.object(&session, "/scope/item").unwrap().state, ObservationState::Stale ); assert_eq!( client.interface(&session, &reference()).unwrap().state, ObservationState::Stale ); } #[test] fn mismatched_interactions_are_not_trusted_and_calls_remain_unknown() { let (mut client, session) = setup(); let request = client.refresh_object(&session, "/scope/item").unwrap(); assert_eq!( client .complete(request.id, Response::FetchInterface(Ok(interface(None)))) .unwrap_err() .kind, ClientErrorKind::InvalidResponse ); let request = client.prepare_interface(&session, reference()).unwrap(); assert_eq!( client .complete(request.id, Response::Observe(Ok(object("item", None)))) .unwrap_err() .kind, ClientErrorKind::InvalidResponse ); ready(&mut client, &session); let request = call(&mut client, &session); let completion = client .complete( request.id, Response::Observe(Err(failure(ProtocolErrorCode::NotFound))), ) .unwrap(); assert!( matches!(completion, Completion::Call(record) if matches!(record.outcome, CallOutcome::Unknown { .. })) ); } #[test] fn typed_stale_responses_cannot_replace_new_observations() { let (mut client, session) = setup(); let old = client.refresh_object(&session, "/scope/item").unwrap(); assert!( client .ensure_observed(&session, "/scope/item", 0) .unwrap() .is_none() ); let new = client.refresh_object(&session, "/scope/item").unwrap(); client .complete(new.id, Response::Observe(Ok(object("item", Some("new"))))) .unwrap(); assert_eq!( client .complete(old.id, Response::Observe(Ok(object("item", Some("old"))))) .unwrap(), Completion::StaleResponseRejected(old.id) ); assert_eq!( client .object(&session, "/scope/item") .unwrap() .object .unwrap() .r#ref .as_deref(), Some("new") ); } #[test] fn scope_replacement_retires_descriptors_and_pending_fetch_but_not_call_snapshot() { let (mut client, session) = setup(); observe( &mut client, &session, "/scope", object("scope", Some("old")), ); ready(&mut client, &session); let call = call(&mut client, &session); let fetch = client.prepare_interface(&session, reference()).unwrap(); observe( &mut client, &session, "/scope", object("scope", Some("new")), ); assert!( client .interface(&session, &reference()) .unwrap() .descriptor .is_none() ); assert_eq!( client .complete( fetch.id, Response::FetchInterface(Ok(interface(Some("old")))) ) .unwrap(), Completion::StaleResponseRejected(fetch.id) ); assert_eq!( client.pending_call_context(call.id).unwrap().descriptor(), &descriptor() ); assert_eq!( client .pending_call_context(call.id) .unwrap() .request() .interface .scope_ref, None ); // A newer descriptor has a different return type; the prepared call still uses Integer. let fetch = client.prepare_interface(&session, reference()).unwrap(); let mut next = interface(Some("new")); next.descriptor.operations[0].returns.r#type = TypeExpr::String; client .complete(fetch.id, Response::FetchInterface(Ok(next))) .unwrap(); let completion = client .complete( call.id, Response::CallOperation(Ok(CallOperationResponse { result: Value::Integer(6), validator: None, })), ) .unwrap(); assert!( matches!(completion, Completion::Call(record) if matches!(record.outcome, CallOutcome::Success(_))) ); } #[test] fn scope_not_found_retires_interfaces_and_late_fetch_cannot_restore_them() { let (mut client, session) = setup(); ready(&mut client, &session); let old = client.prepare_interface(&session, reference()).unwrap(); let missing = client.refresh_object(&session, "/scope").unwrap(); client .complete( missing.id, Response::Observe(Err(failure(ProtocolErrorCode::NotFound))), ) .unwrap(); assert!(client.interface(&session, &reference()).is_none()); assert_eq!( client .complete(old.id, Response::FetchInterface(Ok(interface(None)))) .unwrap(), Completion::StaleResponseRejected(old.id) ); } #[test] fn failures_before_and_after_dispatch_never_retry_operations() { let (mut client, session) = setup(); ready(&mut client, &session); let before = call(&mut client, &session); assert!( matches!(client.fail_transport(before.id, "not handed to Host").unwrap(), Completion::Call(record) if matches!(record.outcome, CallOutcome::NotDispatched(_))) ); assert_eq!( client.object(&session, "/scope/item").unwrap().state, ObservationState::Fresh ); let after = call(&mut client, &session); client.mark_dispatched(after.id).unwrap(); assert!( matches!(client.fail_dispatched_call(after.id, DispatchedTransportFailure::Timeout, "deadline").unwrap(), Completion::Call(record) if matches!(record.outcome, CallOutcome::Unknown { reason: OutcomeUnknownReason::Timeout, .. })) ); assert_eq!(client.in_flight_len(), 0); assert!( client .ensure_observed(&session, "/scope/item", 0) .unwrap() .is_none() ); ready(&mut client, &session); let unknown = call(&mut client, &session); assert!( matches!(client.complete(unknown.id, Response::CallOperation(Err(failure(ProtocolErrorCode::OperationOutcomeUnknown)))).unwrap(), Completion::Call(record) if matches!(record.outcome, CallOutcome::Unknown { reason: OutcomeUnknownReason::Protocol, .. })) ); assert_eq!(client.in_flight_len(), 0); assert_eq!(client.call_history(&session).unwrap().len(), 3); } #[test] fn direct_limits_bound_sessions_inflight_cache_and_history() { let mut client = Client::new(ClientLimits::new(1, 1, 1, 1, 1).unwrap()); let session = client .open_session("world", SecurityContext::new("alice")) .unwrap(); assert_eq!( client .open_session("other", SecurityContext::new("alice")) .unwrap_err() .kind, ClientErrorKind::Capacity ); let first = client.refresh_object(&session, "/scope/item").unwrap(); assert!( client .ensure_observed(&session, "/scope/other", 0) .unwrap() .is_none() ); assert_eq!( client .refresh_object(&session, "/scope/other") .unwrap_err() .kind, ClientErrorKind::Capacity ); client .complete(first.id, Response::Observe(Ok(object("item", None)))) .unwrap(); observe(&mut client, &session, "/scope/other", object("other", None)); assert_eq!(client.known_space(&session).unwrap().len(), 1); for _ in 0..3 { let request = client.refresh_object(&session, "/scope/other").unwrap(); client .complete(request.id, Response::Observe(Ok(object("wrong", None)))) .unwrap_err(); } assert_eq!(client.diagnostics(&session).unwrap().len(), 1); } #[test] fn invalid_direct_requests_do_not_acquire_pending_state() { let (mut client, session) = setup(); assert_eq!( client .refresh_object(&session, "not/absolute") .unwrap_err() .kind, ClientErrorKind::InvalidRequest ); assert_eq!( client .prepare_interface( &session, InterfaceReference { scope: "/".into(), name: "".into() } ) .unwrap_err() .kind, ClientErrorKind::InvalidRequest ); assert_eq!(client.in_flight_len(), 0); assert!(client.known_space(&session).unwrap().is_empty()); }