fix(rust-client): async CA file read; typed ping/session-state/worker-info/drain-events
This commit is contained in:
@@ -40,7 +40,7 @@ prost-types = "0.13.5"
|
|||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "sync", "time"] }
|
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "sync", "time", "fs"] }
|
||||||
tokio-stream = { version = "0.1.17", features = ["net"] }
|
tokio-stream = { version = "0.1.17", features = ["net"] }
|
||||||
tonic = { version = "0.13.1", features = ["transport", "tls-ring", "tls-native-roots"] }
|
tonic = { version = "0.13.1", features = ["transport", "tls-ring", "tls-native-roots"] }
|
||||||
tonic-build = "0.13.1"
|
tonic-build = "0.13.1"
|
||||||
|
|||||||
@@ -178,6 +178,18 @@ redaction. Per-item bulk failures are reported inside each result entry
|
|||||||
`invoke_raw` / `client.invoke_raw` escape hatch performs neither check and
|
`invoke_raw` / `client.invoke_raw` escape hatch performs neither check and
|
||||||
returns the unvalidated reply.
|
returns the unvalidated reply.
|
||||||
|
|
||||||
|
### Diagnostic commands
|
||||||
|
|
||||||
|
`Session` wraps the worker diagnostic commands as typed calls: `ping(message)`
|
||||||
|
(returns `()` on success), `get_session_state()` (returns `SessionState`),
|
||||||
|
`get_worker_info()` (returns `WorkerInfoReply`: pid, version, MXAccess
|
||||||
|
ProgID/CLSID) and `drain_events(max_events)` (returns `Vec<MxEvent>`; the reply
|
||||||
|
is bounded server-side, so drain until an empty result). A reply missing its
|
||||||
|
expected payload surfaces as `Error::MalformedReply`.
|
||||||
|
|
||||||
|
TLS note: `ClientOptions::with_ca_file` is read asynchronously (`tokio::fs`)
|
||||||
|
during `connect`, so connecting no longer blocks the executor thread.
|
||||||
|
|
||||||
## Event Streaming And Reconnect-Replay Gaps
|
## Event Streaming And Reconnect-Replay Gaps
|
||||||
|
|
||||||
`session.events()` / `session.events_after(after_worker_sequence)` (and the
|
`session.events()` / `session.events_after(after_worker_sequence)` (and the
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ impl GatewayClient {
|
|||||||
})?;
|
})?;
|
||||||
endpoint = endpoint.connect_timeout(options.connect_timeout());
|
endpoint = endpoint.connect_timeout(options.connect_timeout());
|
||||||
|
|
||||||
if let Some(tls) = build_tls_config(&options)? {
|
if let Some(tls) = build_tls_config(&options).await? {
|
||||||
endpoint = endpoint.tls_config(tls)?;
|
endpoint = endpoint.tls_config(tls)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ impl GalaxyClient {
|
|||||||
})?;
|
})?;
|
||||||
endpoint = endpoint.connect_timeout(options.connect_timeout());
|
endpoint = endpoint.connect_timeout(options.connect_timeout());
|
||||||
|
|
||||||
if let Some(tls) = build_tls_config(&options)? {
|
if let Some(tls) = build_tls_config(&options).await? {
|
||||||
endpoint = endpoint.tls_config(tls)?;
|
endpoint = endpoint.tls_config(tls)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+28
-15
@@ -3,7 +3,6 @@
|
|||||||
//! chain of `with_*` setters; the `Debug` impl redacts the API key.
|
//! chain of `with_*` setters; the `Debug` impl redacts the API key.
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -235,7 +234,9 @@ pub(crate) fn tls_trust_decision(options: &ClientOptions) -> TlsTrustDecision {
|
|||||||
/// rejects the configuration early with an actionable error. The strict opt-in
|
/// rejects the configuration early with an actionable error. The strict opt-in
|
||||||
/// instead loads the system trust roots so a certificate chaining to an
|
/// instead loads the system trust roots so a certificate chaining to an
|
||||||
/// already-trusted root validates.
|
/// already-trusted root validates.
|
||||||
pub(crate) fn build_tls_config(options: &ClientOptions) -> Result<Option<ClientTlsConfig>, Error> {
|
pub(crate) async fn build_tls_config(
|
||||||
|
options: &ClientOptions,
|
||||||
|
) -> Result<Option<ClientTlsConfig>, Error> {
|
||||||
let decision = tls_trust_decision(options);
|
let decision = tls_trust_decision(options);
|
||||||
if decision == TlsTrustDecision::None {
|
if decision == TlsTrustDecision::None {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -248,10 +249,13 @@ pub(crate) fn build_tls_config(options: &ClientOptions) -> Result<Option<ClientT
|
|||||||
match decision {
|
match decision {
|
||||||
TlsTrustDecision::PinnedCa => {
|
TlsTrustDecision::PinnedCa => {
|
||||||
let ca_file = options.ca_file().expect("PinnedCa implies a CA file");
|
let ca_file = options.ca_file().expect("PinnedCa implies a CA file");
|
||||||
let certificate = fs::read(ca_file).map_err(|source| Error::InvalidEndpoint {
|
let certificate =
|
||||||
endpoint: options.endpoint().to_owned(),
|
tokio::fs::read(ca_file)
|
||||||
detail: format!("failed to read CA file {}: {source}", ca_file.display()),
|
.await
|
||||||
})?;
|
.map_err(|source| Error::InvalidEndpoint {
|
||||||
|
endpoint: options.endpoint().to_owned(),
|
||||||
|
detail: format!("failed to read CA file {}: {source}", ca_file.display()),
|
||||||
|
})?;
|
||||||
tls = tls.ca_certificate(Certificate::from_pem(certificate));
|
tls = tls.ca_certificate(Certificate::from_pem(certificate));
|
||||||
}
|
}
|
||||||
TlsTrustDecision::SystemRoots => {
|
TlsTrustDecision::SystemRoots => {
|
||||||
@@ -334,11 +338,11 @@ mod tests {
|
|||||||
assert!(!debug.contains("mxgw_secret"));
|
assert!(!debug.contains("mxgw_secret"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn plaintext_needs_no_tls() {
|
async fn plaintext_needs_no_tls() {
|
||||||
let options = ClientOptions::new("http://127.0.0.1:5000").with_plaintext(true);
|
let options = ClientOptions::new("http://127.0.0.1:5000").with_plaintext(true);
|
||||||
assert_eq!(tls_trust_decision(&options), TlsTrustDecision::None);
|
assert_eq!(tls_trust_decision(&options), TlsTrustDecision::None);
|
||||||
assert!(build_tls_config(&options).unwrap().is_none());
|
assert!(build_tls_config(&options).await.unwrap().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -349,8 +353,8 @@ mod tests {
|
|||||||
assert_eq!(tls_trust_decision(&options), TlsTrustDecision::PinnedCa);
|
assert_eq!(tls_trust_decision(&options), TlsTrustDecision::PinnedCa);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn strict_without_ca_uses_system_roots() {
|
async fn strict_without_ca_uses_system_roots() {
|
||||||
// Regression for Client.Rust-031: strict verification with no pinned CA
|
// Regression for Client.Rust-031: strict verification with no pinned CA
|
||||||
// must verify against the system trust roots, not produce a config with
|
// must verify against the system trust roots, not produce a config with
|
||||||
// zero trust anchors. The trust decision proves roots are consulted; the
|
// zero trust anchors. The trust decision proves roots are consulted; the
|
||||||
@@ -365,15 +369,24 @@ mod tests {
|
|||||||
"strict-no-CA must request the system trust roots"
|
"strict-no-CA must request the system trust roots"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
build_tls_config(&options).unwrap().is_some(),
|
build_tls_config(&options).await.unwrap().is_some(),
|
||||||
"strict-no-CA must build a usable TLS config"
|
"strict-no-CA must build a usable TLS config"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn lenient_without_ca_is_rejected() {
|
async fn lenient_without_ca_is_rejected() {
|
||||||
let options = ClientOptions::new("https://127.0.0.1:5000").with_plaintext(false);
|
let options = ClientOptions::new("https://127.0.0.1:5000").with_plaintext(false);
|
||||||
assert_eq!(tls_trust_decision(&options), TlsTrustDecision::RejectNoCa);
|
assert_eq!(tls_trust_decision(&options), TlsTrustDecision::RejectNoCa);
|
||||||
assert!(build_tls_config(&options).is_err());
|
assert!(build_tls_config(&options).await.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn missing_pinned_ca_file_errors_without_blocking_read() {
|
||||||
|
let options = ClientOptions::new("https://127.0.0.1:5000")
|
||||||
|
.with_plaintext(false)
|
||||||
|
.with_ca_file("/nonexistent/definitely-missing-ca.pem");
|
||||||
|
let error = build_tls_config(&options).await.unwrap_err();
|
||||||
|
assert!(error.to_string().contains("failed to read CA file"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,15 @@ use crate::generated::mxaccess_gateway::v1::mx_command_reply;
|
|||||||
use crate::generated::mxaccess_gateway::v1::{
|
use crate::generated::mxaccess_gateway::v1::{
|
||||||
ActivateCommand, AddBufferedItemCommand, AddItem2Command, AddItemBulkCommand, AddItemCommand,
|
ActivateCommand, AddBufferedItemCommand, AddItem2Command, AddItemBulkCommand, AddItemCommand,
|
||||||
AdviseCommand, AdviseItemBulkCommand, AdviseSupervisoryCommand, ArchestrAUserToIdCommand,
|
AdviseCommand, AdviseItemBulkCommand, AdviseSupervisoryCommand, ArchestrAUserToIdCommand,
|
||||||
AuthenticateUserCommand, BulkReadResult, BulkWriteResult, CloseSessionRequest, MxCommand,
|
AuthenticateUserCommand, BulkReadResult, BulkWriteResult, CloseSessionRequest,
|
||||||
MxCommandKind, MxCommandReply, MxCommandRequest, MxDataType, MxSparseArray, MxSparseElement,
|
DrainEventsCommand, GetSessionStateCommand, GetWorkerInfoCommand, MxCommand, MxCommandKind,
|
||||||
MxValue as ProtoMxValue, OpenSessionRequest, ReadBulkCommand, RegisterCommand,
|
MxCommandReply, MxCommandRequest, MxDataType, MxEvent, MxSparseArray, MxSparseElement,
|
||||||
RemoveItemBulkCommand, RemoveItemCommand, SetBufferedUpdateIntervalCommand,
|
MxValue as ProtoMxValue, OpenSessionRequest, PingCommand, ReadBulkCommand, RegisterCommand,
|
||||||
|
RemoveItemBulkCommand, RemoveItemCommand, SessionState, SetBufferedUpdateIntervalCommand,
|
||||||
StreamEventsRequest, SubscribeBulkCommand, SubscribeResult, SuspendCommand, UnAdviseCommand,
|
StreamEventsRequest, SubscribeBulkCommand, SubscribeResult, SuspendCommand, UnAdviseCommand,
|
||||||
UnAdviseItemBulkCommand, UnregisterCommand, UnsubscribeBulkCommand, Write2BulkCommand,
|
UnAdviseItemBulkCommand, UnregisterCommand, UnsubscribeBulkCommand, WorkerInfoReply,
|
||||||
Write2BulkEntry, Write2Command, WriteBulkCommand, WriteBulkEntry, WriteCommand,
|
Write2BulkCommand, Write2BulkEntry, Write2Command, WriteBulkCommand, WriteBulkEntry,
|
||||||
WriteSecured2BulkCommand, WriteSecured2BulkEntry, WriteSecured2Command,
|
WriteCommand, WriteSecured2BulkCommand, WriteSecured2BulkEntry, WriteSecured2Command,
|
||||||
WriteSecuredBulkCommand, WriteSecuredBulkEntry, WriteSecuredCommand,
|
WriteSecuredBulkCommand, WriteSecuredBulkEntry, WriteSecuredCommand,
|
||||||
};
|
};
|
||||||
use crate::value::{MxStatus, MxValue, MxValueProjection};
|
use crate::value::{MxStatus, MxValue, MxValueProjection};
|
||||||
@@ -960,6 +961,90 @@ impl Session {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a `Ping` diagnostic command to the worker. A successful return
|
||||||
|
/// means the worker answered; `message` is forwarded for log correlation.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::Command`] for non-OK statuses plus transport errors.
|
||||||
|
pub async fn ping(&self, message: &str) -> Result<(), Error> {
|
||||||
|
self.invoke(
|
||||||
|
MxCommandKind::Ping,
|
||||||
|
Payload::Ping(PingCommand {
|
||||||
|
message: message.to_owned(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the gateway-side [`SessionState`] of this session.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::MalformedReply`] when the reply lacks a session-state
|
||||||
|
/// payload, plus the errors of [`invoke`](Self::invoke).
|
||||||
|
pub async fn get_session_state(&self) -> Result<SessionState, Error> {
|
||||||
|
let reply = self
|
||||||
|
.invoke(
|
||||||
|
MxCommandKind::GetSessionState,
|
||||||
|
Payload::GetSessionState(GetSessionStateCommand {}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
match reply.payload {
|
||||||
|
Some(mx_command_reply::Payload::SessionState(state)) => {
|
||||||
|
Ok(SessionState::try_from(state.state).unwrap_or(SessionState::Unspecified))
|
||||||
|
}
|
||||||
|
_ => Err(Error::MalformedReply {
|
||||||
|
detail: "get_session_state reply did not carry a SessionState payload".to_owned(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch worker process details (pid, version, MXAccess ProgID/CLSID).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::MalformedReply`] when the reply lacks a worker-info
|
||||||
|
/// payload, plus the errors of [`invoke`](Self::invoke).
|
||||||
|
pub async fn get_worker_info(&self) -> Result<WorkerInfoReply, Error> {
|
||||||
|
let reply = self
|
||||||
|
.invoke(
|
||||||
|
MxCommandKind::GetWorkerInfo,
|
||||||
|
Payload::GetWorkerInfo(GetWorkerInfoCommand {}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
match reply.payload {
|
||||||
|
Some(mx_command_reply::Payload::WorkerInfo(info)) => Ok(info),
|
||||||
|
_ => Err(Error::MalformedReply {
|
||||||
|
detail: "get_worker_info reply did not carry a WorkerInfo payload".to_owned(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drain up to `max_events` queued events through the diagnostic command
|
||||||
|
/// (distinct from the streaming [`events`](Self::events) surface). The
|
||||||
|
/// reply is bounded server-side, so call repeatedly until it is empty.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`Error::MalformedReply`] when the reply lacks a drain-events
|
||||||
|
/// payload, plus the errors of [`invoke`](Self::invoke).
|
||||||
|
pub async fn drain_events(&self, max_events: u32) -> Result<Vec<MxEvent>, Error> {
|
||||||
|
let reply = self
|
||||||
|
.invoke(
|
||||||
|
MxCommandKind::DrainEvents,
|
||||||
|
Payload::DrainEvents(DrainEventsCommand { max_events }),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
match reply.payload {
|
||||||
|
Some(mx_command_reply::Payload::DrainEvents(drain)) => Ok(drain.events),
|
||||||
|
_ => Err(Error::MalformedReply {
|
||||||
|
detail: "drain_events reply did not carry a DrainEvents payload".to_owned(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Issue a raw `Invoke` for an arbitrary command, without filtering on
|
/// Issue a raw `Invoke` for an arbitrary command, without filtering on
|
||||||
/// the protocol status. Useful when callers need the full reply for
|
/// the protocol status. Useful when callers need the full reply for
|
||||||
/// commands not yet wrapped by `Session`.
|
/// commands not yet wrapped by `Session`.
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ use zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::{
|
|||||||
AddItem2Reply, AddItemReply, AlarmConditionState, AlarmFeedMessage, AlarmTransitionKind,
|
AddItem2Reply, AddItemReply, AlarmConditionState, AlarmFeedMessage, AlarmTransitionKind,
|
||||||
AuthenticateUserCommand, AuthenticateUserReply, BulkReadReply, BulkReadResult,
|
AuthenticateUserCommand, AuthenticateUserReply, BulkReadReply, BulkReadResult,
|
||||||
BulkSubscribeReply, BulkWriteReply, BulkWriteResult, CloseSessionReply, CloseSessionRequest,
|
BulkSubscribeReply, BulkWriteReply, BulkWriteResult, CloseSessionReply, CloseSessionRequest,
|
||||||
MxCommandKind, MxCommandReply, MxDataType, MxEvent, MxEventFamily, MxSparseArray,
|
DrainEventsReply, MxCommandKind, MxCommandReply, MxDataType, MxEvent, MxEventFamily,
|
||||||
MxSparseElement, MxStatusCategory, MxStatusProxy, MxStatusSource, MxValue,
|
MxSparseArray, MxSparseElement, MxStatusCategory, MxStatusProxy, MxStatusSource, MxValue,
|
||||||
OnAlarmTransitionEvent, OpenSessionReply, OpenSessionRequest, ProtocolStatus,
|
OnAlarmTransitionEvent, OpenSessionReply, OpenSessionRequest, ProtocolStatus,
|
||||||
ProtocolStatusCode, QueryActiveAlarmsRequest, RegisterReply, ReplayGap, SessionState,
|
ProtocolStatusCode, QueryActiveAlarmsRequest, RegisterReply, ReplayGap, SessionState,
|
||||||
StreamAlarmsRequest, StreamEventsRequest, SubscribeResult, Write2BulkEntry, WriteBulkEntry,
|
SessionStateReply, StreamAlarmsRequest, StreamEventsRequest, SubscribeResult, WorkerInfoReply,
|
||||||
WriteCommand, WriteSecured2BulkEntry, WriteSecuredBulkEntry,
|
Write2BulkEntry, WriteBulkEntry, WriteCommand, WriteSecured2BulkEntry, WriteSecuredBulkEntry,
|
||||||
};
|
};
|
||||||
use zb_mom_ww_mxgateway_client::{
|
use zb_mom_ww_mxgateway_client::{
|
||||||
next_correlation_id, ApiKey, ClientOptions, CommandError, Error, EventItem, GatewayClient,
|
next_correlation_id, ApiKey, ClientOptions, CommandError, Error, EventItem, GatewayClient,
|
||||||
@@ -550,6 +550,81 @@ async fn unary_invoke_maps_status_unavailable_to_error_unavailable() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ping_round_trips_through_the_fake_gateway() {
|
||||||
|
let state = Arc::new(FakeState::default());
|
||||||
|
let endpoint = spawn_fake_gateway(state.clone()).await;
|
||||||
|
let client = GatewayClient::connect(ClientOptions::new(endpoint))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let session = client.session("session-fixture");
|
||||||
|
|
||||||
|
session.ping("hello").await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*state.last_command_kind.lock().await,
|
||||||
|
Some(MxCommandKind::Ping as i32)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_session_state_returns_typed_state() {
|
||||||
|
let state = Arc::new(FakeState::default());
|
||||||
|
let endpoint = spawn_fake_gateway(state.clone()).await;
|
||||||
|
let client = GatewayClient::connect(ClientOptions::new(endpoint))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let session = client.session("session-fixture");
|
||||||
|
|
||||||
|
let session_state = session.get_session_state().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(session_state, SessionState::Ready);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_worker_info_returns_typed_reply() {
|
||||||
|
let state = Arc::new(FakeState::default());
|
||||||
|
let endpoint = spawn_fake_gateway(state.clone()).await;
|
||||||
|
let client = GatewayClient::connect(ClientOptions::new(endpoint))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let session = client.session("session-fixture");
|
||||||
|
|
||||||
|
let info = session.get_worker_info().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(info.worker_process_id, 4242);
|
||||||
|
assert_eq!(info.worker_version, "1.2.3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn drain_events_returns_events() {
|
||||||
|
let state = Arc::new(FakeState::default());
|
||||||
|
let endpoint = spawn_fake_gateway(state.clone()).await;
|
||||||
|
let client = GatewayClient::connect(ClientOptions::new(endpoint))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let session = client.session("session-fixture");
|
||||||
|
|
||||||
|
let events = session.drain_events(10).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(events.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_session_state_malformed_without_payload() {
|
||||||
|
let state = Arc::new(FakeState::default());
|
||||||
|
let endpoint = spawn_fake_gateway(state.clone()).await;
|
||||||
|
let client = GatewayClient::connect(ClientOptions::new(endpoint))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let session = client.session("session-fixture");
|
||||||
|
*state.invoke_override.lock().await = Some(InvokeOverride::OkWithoutPayload);
|
||||||
|
|
||||||
|
let error = session.get_session_state().await.unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(error, Error::MalformedReply { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn read_bulk_round_trips_through_the_fake_gateway() {
|
async fn read_bulk_round_trips_through_the_fake_gateway() {
|
||||||
let state = Arc::new(FakeState::default());
|
let state = Arc::new(FakeState::default());
|
||||||
@@ -1337,6 +1412,57 @@ impl MxAccessGateway for FakeGateway {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if kind == MxCommandKind::Ping as i32 {
|
||||||
|
return Ok(Response::new(MxCommandReply {
|
||||||
|
session_id: request.session_id,
|
||||||
|
correlation_id: "fake-correlation".to_owned(),
|
||||||
|
kind,
|
||||||
|
protocol_status: Some(ok_status("command ok")),
|
||||||
|
..MxCommandReply::default()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == MxCommandKind::GetSessionState as i32 {
|
||||||
|
return Ok(Response::new(MxCommandReply {
|
||||||
|
session_id: request.session_id,
|
||||||
|
correlation_id: "fake-correlation".to_owned(),
|
||||||
|
kind,
|
||||||
|
protocol_status: Some(ok_status("command ok")),
|
||||||
|
payload: Some(mx_command_reply::Payload::SessionState(SessionStateReply {
|
||||||
|
state: SessionState::Ready as i32,
|
||||||
|
})),
|
||||||
|
..MxCommandReply::default()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == MxCommandKind::GetWorkerInfo as i32 {
|
||||||
|
return Ok(Response::new(MxCommandReply {
|
||||||
|
session_id: request.session_id,
|
||||||
|
correlation_id: "fake-correlation".to_owned(),
|
||||||
|
kind,
|
||||||
|
protocol_status: Some(ok_status("command ok")),
|
||||||
|
payload: Some(mx_command_reply::Payload::WorkerInfo(WorkerInfoReply {
|
||||||
|
worker_process_id: 4242,
|
||||||
|
worker_version: "1.2.3".to_owned(),
|
||||||
|
..WorkerInfoReply::default()
|
||||||
|
})),
|
||||||
|
..MxCommandReply::default()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == MxCommandKind::DrainEvents as i32 {
|
||||||
|
return Ok(Response::new(MxCommandReply {
|
||||||
|
session_id: request.session_id,
|
||||||
|
correlation_id: "fake-correlation".to_owned(),
|
||||||
|
kind,
|
||||||
|
protocol_status: Some(ok_status("command ok")),
|
||||||
|
payload: Some(mx_command_reply::Payload::DrainEvents(DrainEventsReply {
|
||||||
|
events: vec![MxEvent::default()],
|
||||||
|
})),
|
||||||
|
..MxCommandReply::default()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if kind == MxCommandKind::ReadBulk as i32 {
|
if kind == MxCommandKind::ReadBulk as i32 {
|
||||||
return Ok(Response::new(MxCommandReply {
|
return Ok(Response::new(MxCommandReply {
|
||||||
session_id: request.session_id,
|
session_id: request.session_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user