1use std::sync::{
19 Arc,
20 atomic::{AtomicBool, Ordering},
21};
22
23use nautilus_network::{
24 retry::{RetryManager, create_websocket_retry_manager},
25 websocket::{AuthTracker, SubscriptionState, WebSocketClient},
26};
27use tokio_tungstenite::tungstenite::Message;
28
29use super::{
30 enums::BybitWsOperation,
31 error::{BybitWsError, create_bybit_timeout_error, should_retry_bybit_error},
32 messages::{
33 BybitWebSocketError, BybitWsFrame, BybitWsMessage, BybitWsResponse, BybitWsSubscriptionMsg,
34 },
35 parse::parse_bybit_ws_frame,
36};
37
38#[derive(Debug)]
40pub enum HandlerCommand {
41 SetClient(WebSocketClient),
42 Disconnect,
43 Authenticate { payload: String },
44 Subscribe { topics: Vec<String> },
45 Unsubscribe { topics: Vec<String> },
46 SendText { payload: String },
47}
48
49pub(super) struct BybitWsFeedHandler {
50 signal: Arc<AtomicBool>,
51 inner: Option<WebSocketClient>,
52 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
53 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
54 auth_tracker: AuthTracker,
55 subscriptions: SubscriptionState,
56 retry_manager: RetryManager<BybitWsError>,
57}
58
59impl BybitWsFeedHandler {
60 pub(super) fn new(
62 signal: Arc<AtomicBool>,
63 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
64 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
65 auth_tracker: AuthTracker,
66 subscriptions: SubscriptionState,
67 ) -> Self {
68 Self {
69 signal,
70 inner: None,
71 cmd_rx,
72 raw_rx,
73 auth_tracker,
74 subscriptions,
75 retry_manager: create_websocket_retry_manager(),
76 }
77 }
78
79 pub(super) fn is_stopped(&self) -> bool {
80 self.signal.load(Ordering::Relaxed)
81 }
82
83 async fn send_with_retry(&self, payload: String) -> Result<(), BybitWsError> {
85 if let Some(client) = &self.inner {
86 self.retry_manager
87 .execute_with_retry(
88 "websocket_send",
89 || {
90 let payload = payload.clone();
91 async move {
92 client
93 .send_text(payload, None)
94 .await
95 .map_err(|e| BybitWsError::Transport(format!("Send failed: {e}")))
96 }
97 },
98 should_retry_bybit_error,
99 create_bybit_timeout_error,
100 )
101 .await
102 } else {
103 Err(BybitWsError::ClientError(
104 "No active WebSocket client".to_string(),
105 ))
106 }
107 }
108
109 pub(super) async fn next(&mut self) -> Option<BybitWsMessage> {
110 loop {
111 tokio::select! {
112 Some(cmd) = self.cmd_rx.recv() => {
113 match cmd {
114 HandlerCommand::SetClient(client) => {
115 log::debug!("WebSocketClient received by handler");
116 self.inner = Some(client);
117 }
118 HandlerCommand::Disconnect => {
119 log::debug!("Disconnect command received");
120
121 if let Some(client) = self.inner.take() {
122 client.disconnect().await;
123 }
124 }
125 HandlerCommand::Authenticate { payload } => {
126 log::debug!("Authenticate command received");
127
128 if let Err(e) = self.send_with_retry(payload).await {
129 log::error!("Failed to send authentication after retries: {e}");
130 }
131 }
132 HandlerCommand::Subscribe { topics } => {
133 for topic in topics {
134 log::debug!("Subscribing to topic: topic={topic}");
135 if let Err(e) = self.send_with_retry(topic.clone()).await {
136 log::error!("Failed to send subscription after retries: topic={topic}, error={e}");
137 }
138 }
139 }
140 HandlerCommand::Unsubscribe { topics } => {
141 for topic in topics {
142 log::debug!("Unsubscribing from topic: topic={topic}");
143 if let Err(e) = self.send_with_retry(topic.clone()).await {
144 log::error!("Failed to send unsubscription after retries: topic={topic}, error={e}");
145 }
146 }
147 }
148 HandlerCommand::SendText { payload } => {
149 if let Err(e) = self.send_with_retry(payload).await {
150 log::error!("Error sending text with retry: {e}");
151 }
152 }
153 }
154 }
155
156 () = tokio::time::sleep(std::time::Duration::from_millis(100)) => {
157 if self.signal.load(Ordering::Relaxed) {
158 log::debug!("Stop signal received during idle period");
159 return None;
160 }
161 }
162
163 msg = self.raw_rx.recv() => {
164 let msg = match msg {
165 Some(msg) => msg,
166 None => {
167 log::debug!("WebSocket stream closed");
168 return None;
169 }
170 };
171
172 if let Message::Ping(data) = &msg {
173 log::trace!("Received ping frame with {} bytes", data.len());
174
175 if let Some(client) = &self.inner
176 && let Err(e) = client.send_pong(data.to_vec()).await
177 {
178 log::warn!("Failed to send pong frame: error={e}");
179 }
180 continue;
181 }
182
183 let frame = match Self::parse_raw_frame(msg) {
184 Some(frame) => frame,
185 None => continue,
186 };
187
188 if self.signal.load(Ordering::Relaxed) {
189 log::debug!("Stop signal received");
190 return None;
191 }
192
193 match frame {
194 BybitWsFrame::Subscription(ref sub_msg) => {
195 self.handle_subscription_ack(sub_msg);
196 }
197 BybitWsFrame::Auth(auth_response) => {
198 let is_success = auth_response.success.unwrap_or(false)
199 || (auth_response.ret_code == Some(0));
200
201 if is_success {
202 self.auth_tracker.succeed();
203 log::debug!("WebSocket authenticated");
204 } else {
205 let error_msg = auth_response
206 .ret_msg
207 .as_deref()
208 .unwrap_or("Authentication rejected");
209 self.auth_tracker.fail(error_msg);
210 log::error!("WebSocket authentication failed: error={error_msg}");
211 }
212 return Some(BybitWsMessage::Auth(auth_response));
213 }
214 BybitWsFrame::ErrorResponse(ref resp) => {
215 if let Some(op) = &resp.op {
220 if *op == BybitWsOperation::Subscribe
221 || *op == BybitWsOperation::Unsubscribe
222 {
223 self.handle_subscription_error(resp);
224 } else {
225 let error = BybitWebSocketError::from_response(resp);
226 return Some(BybitWsMessage::Error(error));
227 }
228 } else {
229 let error = BybitWebSocketError::from_response(resp);
230 return Some(BybitWsMessage::Error(error));
231 }
232 }
233 BybitWsFrame::OrderResponse(resp) => {
234 return Some(BybitWsMessage::OrderResponse(resp));
235 }
236 BybitWsFrame::Orderbook(msg) => {
237 return Some(BybitWsMessage::Orderbook(msg));
238 }
239 BybitWsFrame::Trade(msg) => {
240 return Some(BybitWsMessage::Trade(msg));
241 }
242 BybitWsFrame::Kline(msg) => {
243 return Some(BybitWsMessage::Kline(msg));
244 }
245 BybitWsFrame::TickerLinear(msg) => {
246 return Some(BybitWsMessage::TickerLinear(msg));
247 }
248 BybitWsFrame::TickerOption(msg) => {
249 return Some(BybitWsMessage::TickerOption(msg));
250 }
251 BybitWsFrame::AccountOrder(msg) => {
252 return Some(BybitWsMessage::AccountOrder(msg));
253 }
254 BybitWsFrame::AccountExecution(msg) => {
255 return Some(BybitWsMessage::AccountExecution(msg));
256 }
257 BybitWsFrame::AccountExecutionFast(msg) => {
258 return Some(BybitWsMessage::AccountExecutionFast(msg));
259 }
260 BybitWsFrame::AccountWallet(msg) => {
261 return Some(BybitWsMessage::AccountWallet(msg));
262 }
263 BybitWsFrame::AccountPosition(msg) => {
264 return Some(BybitWsMessage::AccountPosition(msg));
265 }
266 BybitWsFrame::Reconnected => {
267 self.auth_tracker.invalidate();
268 return Some(BybitWsMessage::Reconnected);
269 }
270 BybitWsFrame::Unknown(value) => {
271 log::debug!("Unknown WebSocket frame: {value}");
272 }
273 }
274 }
275 }
276 }
277 }
278
279 fn handle_subscription_ack(&self, sub_msg: &BybitWsSubscriptionMsg) {
280 match sub_msg.op {
281 BybitWsOperation::Subscribe => {
282 if sub_msg.success {
283 if let Some(topic) = &sub_msg.req_id {
284 self.subscriptions.confirm_subscribe(topic);
285 log::debug!("Subscription confirmed: topic={topic}");
286 } else {
287 for topic in self.subscriptions.pending_subscribe_topics() {
289 self.subscriptions.confirm_subscribe(&topic);
290 log::debug!("Subscription confirmed (bulk): topic={topic}");
291 }
292 }
293 } else if let Some(topic) = &sub_msg.req_id {
294 self.subscriptions.mark_failure(topic);
295 log::warn!(
296 "Subscription failed: topic={topic}, error={:?}",
297 sub_msg.ret_msg
298 );
299 } else {
300 for topic in self.subscriptions.pending_subscribe_topics() {
301 self.subscriptions.mark_failure(&topic);
302 log::warn!(
303 "Subscription failed (bulk): topic={topic}, error={:?}",
304 sub_msg.ret_msg
305 );
306 }
307 }
308 }
309 BybitWsOperation::Unsubscribe => {
310 if sub_msg.success {
311 if let Some(topic) = &sub_msg.req_id {
312 self.subscriptions.confirm_unsubscribe(topic);
313 log::debug!("Unsubscription confirmed: topic={topic}");
314 } else {
315 for topic in self.subscriptions.pending_unsubscribe_topics() {
316 self.subscriptions.confirm_unsubscribe(&topic);
317 log::debug!("Unsubscription confirmed (bulk): topic={topic}");
318 }
319 }
320 } else {
321 let topic_desc = sub_msg.req_id.as_deref().unwrap_or("unknown");
322 log::warn!(
323 "Unsubscription failed: topic={topic_desc}, error={:?}",
324 sub_msg.ret_msg
325 );
326 }
327 }
328 _ => {}
329 }
330 }
331
332 fn handle_subscription_error(&self, resp: &BybitWsResponse) {
333 let topic = resp.req_id.as_deref().unwrap_or("unknown");
334 let error_msg = resp.ret_msg.as_deref().unwrap_or("unknown error");
335
336 match resp.op {
337 Some(BybitWsOperation::Subscribe) => {
338 if is_already_subscribed_error(error_msg)
341 && let Some(ref req_id) = resp.req_id
342 {
343 self.subscriptions.confirm_subscribe(req_id);
344 log::debug!("Subscription duplicate ignored: topic={topic}, error={error_msg}");
345 return;
346 }
347
348 if let Some(ref req_id) = resp.req_id {
349 self.subscriptions.mark_failure(req_id);
350 } else {
351 for t in self.subscriptions.pending_subscribe_topics() {
352 self.subscriptions.mark_failure(&t);
353 }
354 }
355 log::warn!("Subscription error: topic={topic}, error={error_msg}");
356 }
357 Some(BybitWsOperation::Unsubscribe) => {
358 log::warn!("Unsubscription error: topic={topic}, error={error_msg}");
359 }
360 _ => {}
361 }
362 }
363
364 fn parse_raw_frame(msg: Message) -> Option<BybitWsFrame> {
365 match msg {
366 Message::Text(text) => {
367 if text == nautilus_network::RECONNECTED {
368 log::debug!("Received WebSocket reconnected signal");
369 return Some(BybitWsFrame::Reconnected);
370 }
371
372 if text.trim().eq_ignore_ascii_case("pong") {
373 return None;
374 }
375
376 log::trace!("Raw websocket message: {text}");
377
378 let value: serde_json::Value = match serde_json::from_str(&text) {
379 Ok(v) => v,
380 Err(e) => {
381 log::error!("Failed to parse WebSocket message: {e}: {text}");
382 return None;
383 }
384 };
385
386 if value
387 .get("op")
388 .and_then(serde_json::Value::as_str)
389 .is_some_and(|op| op == BybitWsOperation::Pong.as_ref())
390 {
391 return None;
392 }
393
394 Some(parse_bybit_ws_frame(value))
395 }
396 Message::Binary(msg) => {
397 log::debug!("Raw binary frame ({} bytes)", msg.len());
398 log::trace!("Raw binary: {msg:?}");
399 None
400 }
401 Message::Close(_) => {
402 log::debug!("Received close message, waiting for reconnection");
403 None
404 }
405 _ => None,
406 }
407 }
408}
409
410fn is_already_subscribed_error(error_msg: &str) -> bool {
411 error_msg
412 .to_ascii_lowercase()
413 .contains("already subscribed")
414}
415
416#[cfg(test)]
417mod tests {
418 use rstest::rstest;
419 use ustr::Ustr;
420
421 use super::*;
422 use crate::common::{consts::BYBIT_WS_TOPIC_DELIMITER, testing::load_test_json};
423
424 fn create_test_handler() -> BybitWsFeedHandler {
425 let signal = Arc::new(AtomicBool::new(false));
426 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
427 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
428 let auth_tracker = AuthTracker::new();
429 let subscriptions = SubscriptionState::new(BYBIT_WS_TOPIC_DELIMITER);
430
431 BybitWsFeedHandler::new(signal, cmd_rx, raw_rx, auth_tracker, subscriptions)
432 }
433
434 fn load_value(fixture: &str) -> serde_json::Value {
435 let json = load_test_json(fixture);
436 serde_json::from_str(&json).unwrap()
437 }
438
439 #[rstest]
440 fn test_handler_initializes() {
441 let _handler = create_test_handler();
442 }
443
444 #[rstest]
445 fn test_parse_frame_auth_success() {
446 let value = load_value("ws_auth_success.json");
447 let frame = parse_bybit_ws_frame(value);
448 match frame {
449 BybitWsFrame::Auth(auth) => {
450 assert_eq!(auth.conn_id.as_deref(), Some("cejreaspqfm9se7usbrg-2xh"));
451 assert_eq!(auth.ret_code, Some(0));
452 assert_eq!(auth.success, Some(true));
453 }
454 other => panic!("Expected Auth, was {other:?}"),
455 }
456 }
457
458 #[rstest]
459 fn test_parse_frame_auth_failure() {
460 let value = load_value("ws_auth_failure.json");
461 let frame = parse_bybit_ws_frame(value);
462 match frame {
463 BybitWsFrame::ErrorResponse(resp) => {
464 assert_eq!(resp.ret_code, Some(10003));
465 assert_eq!(resp.ret_msg.as_deref(), Some("Invalid apikey"));
466 }
467 other => panic!("Expected ErrorResponse, was {other:?}"),
468 }
469 }
470
471 #[rstest]
472 fn test_parse_frame_subscription_ack() {
473 let value = load_value("ws_subscription_ack.json");
474 let frame = parse_bybit_ws_frame(value);
475 match frame {
476 BybitWsFrame::Subscription(sub) => {
477 assert!(sub.success);
478 assert_eq!(sub.op, BybitWsOperation::Subscribe);
479 assert_eq!(sub.req_id.as_deref(), Some("sub-orderbook-1"));
480 }
481 other => panic!("Expected Subscription, was {other:?}"),
482 }
483 }
484
485 #[rstest]
486 fn test_parse_frame_subscription_failure() {
487 let value = load_value("ws_subscription_failure.json");
488 let frame = parse_bybit_ws_frame(value);
489 match frame {
490 BybitWsFrame::ErrorResponse(resp) => {
491 assert_eq!(
492 resp.ret_msg.as_deref(),
493 Some("Invalid topic: invalid.topic.BTCUSDT")
494 );
495 }
496 other => panic!("Expected ErrorResponse, was {other:?}"),
497 }
498 }
499
500 #[rstest]
501 fn test_parse_frame_order_response() {
502 let value = load_value("ws_order_response.json");
503 let frame = parse_bybit_ws_frame(value);
504 match frame {
505 BybitWsFrame::OrderResponse(resp) => {
506 assert_eq!(resp.op.as_str(), "order.create");
507 assert_eq!(resp.ret_code, 0);
508 assert_eq!(resp.ret_msg, "OK");
509 }
510 other => panic!("Expected OrderResponse, was {other:?}"),
511 }
512 }
513
514 #[rstest]
515 fn test_parse_frame_orderbook() {
516 let value = load_value("ws_orderbook_snapshot.json");
517 let frame = parse_bybit_ws_frame(value);
518 assert!(
519 matches!(frame, BybitWsFrame::Orderbook(_)),
520 "Expected Orderbook, was {frame:?}"
521 );
522 }
523
524 #[rstest]
525 fn test_parse_frame_trade() {
526 let value = load_value("ws_public_trade.json");
527 let frame = parse_bybit_ws_frame(value);
528 assert!(
529 matches!(frame, BybitWsFrame::Trade(_)),
530 "Expected Trade, was {frame:?}"
531 );
532 }
533
534 #[rstest]
535 fn test_parse_frame_kline() {
536 let value = load_value("ws_kline.json");
537 let frame = parse_bybit_ws_frame(value);
538 assert!(
539 matches!(frame, BybitWsFrame::Kline(_)),
540 "Expected Kline, was {frame:?}"
541 );
542 }
543
544 #[rstest]
545 fn test_parse_frame_ticker_linear() {
546 let value = load_value("ws_ticker_linear.json");
547 let frame = parse_bybit_ws_frame(value);
548 assert!(
549 matches!(frame, BybitWsFrame::TickerLinear(_)),
550 "Expected TickerLinear, was {frame:?}"
551 );
552 }
553
554 #[rstest]
555 fn test_parse_frame_ticker_option() {
556 let value = load_value("ws_ticker_option.json");
557 let frame = parse_bybit_ws_frame(value);
558 assert!(
559 matches!(frame, BybitWsFrame::TickerOption(_)),
560 "Expected TickerOption, was {frame:?}"
561 );
562 }
563
564 #[rstest]
565 fn test_parse_frame_account_order() {
566 let value = load_value("ws_account_order.json");
567 let frame = parse_bybit_ws_frame(value);
568 assert!(
569 matches!(frame, BybitWsFrame::AccountOrder(_)),
570 "Expected AccountOrder, was {frame:?}"
571 );
572 }
573
574 #[rstest]
575 fn test_parse_frame_account_execution() {
576 let value = load_value("ws_account_execution.json");
577 let frame = parse_bybit_ws_frame(value);
578 assert!(
579 matches!(frame, BybitWsFrame::AccountExecution(_)),
580 "Expected AccountExecution, was {frame:?}"
581 );
582 }
583
584 #[rstest]
585 fn test_parse_frame_account_wallet() {
586 let value = load_value("ws_account_wallet.json");
587 let frame = parse_bybit_ws_frame(value);
588 assert!(
589 matches!(frame, BybitWsFrame::AccountWallet(_)),
590 "Expected AccountWallet, was {frame:?}"
591 );
592 }
593
594 #[rstest]
595 fn test_parse_frame_account_position() {
596 let value = load_value("ws_account_position.json");
597 let frame = parse_bybit_ws_frame(value);
598 assert!(
599 matches!(frame, BybitWsFrame::AccountPosition(_)),
600 "Expected AccountPosition, was {frame:?}"
601 );
602 }
603
604 #[rstest]
605 fn test_parse_frame_unknown_message() {
606 let value: serde_json::Value = serde_json::json!({"foo": "bar"});
607 let frame = parse_bybit_ws_frame(value);
608 assert!(
609 matches!(frame, BybitWsFrame::Unknown(_)),
610 "Expected Unknown, was {frame:?}"
611 );
612 }
613
614 #[rstest]
615 fn test_parse_raw_reconnected_signal() {
616 let msg = Message::Text(nautilus_network::RECONNECTED.to_string().into());
617 let result = BybitWsFeedHandler::parse_raw_frame(msg);
618 assert!(
619 matches!(result, Some(BybitWsFrame::Reconnected)),
620 "Expected Some(Reconnected), was {result:?}"
621 );
622 }
623
624 #[rstest]
625 fn test_parse_raw_pong_text() {
626 let msg = Message::Text("pong".into());
627 let result = BybitWsFeedHandler::parse_raw_frame(msg);
628 assert!(result.is_none(), "Expected None for pong, was {result:?}");
629 }
630
631 #[rstest]
632 fn test_parse_raw_json_pong_message() {
633 let msg = Message::Text(
634 r#"{"args":["1777226678908"],"conn_id":"yzr7jz02gws1vh60mk5m-hxqdp","op":"pong"}"#
635 .into(),
636 );
637 let result = BybitWsFeedHandler::parse_raw_frame(msg);
638 assert!(
639 result.is_none(),
640 "Expected None for JSON pong, was {result:?}"
641 );
642 }
643
644 #[rstest]
645 fn test_parse_raw_valid_json() {
646 let json = load_test_json("ws_public_trade.json");
647 let msg = Message::Text(json.into());
648 let result = BybitWsFeedHandler::parse_raw_frame(msg);
649 assert!(
650 matches!(result, Some(BybitWsFrame::Trade(_))),
651 "Expected Some(Trade), was {result:?}"
652 );
653 }
654
655 #[rstest]
656 fn test_parse_raw_invalid_json() {
657 let msg = Message::Text("not valid json".into());
658 let result = BybitWsFeedHandler::parse_raw_frame(msg);
659 assert!(
660 result.is_none(),
661 "Expected None for invalid JSON, was {result:?}"
662 );
663 }
664
665 #[rstest]
666 fn test_parse_raw_binary_message() {
667 let msg = Message::Binary(vec![0x01, 0x02].into());
668 let result = BybitWsFeedHandler::parse_raw_frame(msg);
669 assert!(result.is_none(), "Expected None for binary, was {result:?}");
670 }
671
672 #[rstest]
673 fn test_subscription_ack_with_req_id_confirms_only_that_topic() {
674 let handler = create_test_handler();
675 handler.subscriptions.mark_subscribe("orderbook.50.BTCUSDT");
676 handler.subscriptions.mark_subscribe("publicTrade.BTCUSDT");
677
678 let ack = BybitWsSubscriptionMsg {
679 success: true,
680 op: BybitWsOperation::Subscribe,
681 conn_id: None,
682 req_id: Some("orderbook.50.BTCUSDT".to_string()),
683 ret_msg: None,
684 };
685
686 handler.handle_subscription_ack(&ack);
687
688 assert!(
690 handler
691 .subscriptions
692 .pending_subscribe_topics()
693 .contains(&"publicTrade.BTCUSDT".to_string())
694 );
695 assert!(
696 !handler
697 .subscriptions
698 .pending_subscribe_topics()
699 .contains(&"orderbook.50.BTCUSDT".to_string())
700 );
701 }
702
703 #[rstest]
704 fn test_subscription_failure_with_req_id_marks_only_that_topic() {
705 let handler = create_test_handler();
706 handler.subscriptions.mark_subscribe("orderbook.50.BTCUSDT");
707 handler.subscriptions.mark_subscribe("publicTrade.BTCUSDT");
708
709 let ack = BybitWsSubscriptionMsg {
710 success: false,
711 op: BybitWsOperation::Subscribe,
712 conn_id: None,
713 req_id: Some("orderbook.50.BTCUSDT".to_string()),
714 ret_msg: Some("Invalid topic".to_string()),
715 };
716
717 handler.handle_subscription_ack(&ack);
718
719 let pending = handler.subscriptions.pending_subscribe_topics();
722 assert!(pending.contains(&"orderbook.50.BTCUSDT".to_string()));
723 assert!(pending.contains(&"publicTrade.BTCUSDT".to_string()));
724 }
725
726 #[rstest]
727 fn test_error_response_with_subscribe_op_triggers_mark_failure() {
728 let handler = create_test_handler();
729 handler
730 .subscriptions
731 .mark_subscribe("invalid.topic.BTCUSDT");
732
733 let resp = BybitWsResponse {
734 op: Some(BybitWsOperation::Subscribe),
735 topic: None,
736 success: Some(false),
737 conn_id: None,
738 req_id: Some("invalid.topic.BTCUSDT".to_string()),
739 ret_code: Some(10001),
740 ret_msg: Some("Invalid topic".to_string()),
741 };
742
743 handler.handle_subscription_error(&resp);
744
745 let pending = handler.subscriptions.pending_subscribe_topics();
747 assert!(pending.contains(&"invalid.topic.BTCUSDT".to_string()));
748 }
749
750 #[rstest]
751 fn test_already_subscribed_error_confirms_topic() {
752 let handler = create_test_handler();
753 handler.subscriptions.mark_subscribe("tickers.ETHUSDT");
754
755 let resp = BybitWsResponse {
756 op: Some(BybitWsOperation::Subscribe),
757 topic: None,
758 success: Some(false),
759 conn_id: None,
760 req_id: Some("tickers.ETHUSDT".to_string()),
761 ret_code: Some(10001),
762 ret_msg: Some("error:already subscribed,topic:tickers.ETHUSDT".to_string()),
763 };
764
765 handler.handle_subscription_error(&resp);
766
767 let pending = handler.subscriptions.pending_subscribe_topics();
768 assert!(!pending.contains(&"tickers.ETHUSDT".to_string()));
769 let symbols = handler.subscriptions.confirmed();
770 let entry = symbols
771 .get(&Ustr::from("tickers"))
772 .expect("channel present");
773 assert!(entry.contains(&Ustr::from("ETHUSDT")));
774 }
775
776 #[rstest]
777 fn test_subscription_ack_without_req_id_confirms_all_pending() {
778 let handler = create_test_handler();
779 handler.subscriptions.mark_subscribe("orderbook.50.BTCUSDT");
780 handler.subscriptions.mark_subscribe("publicTrade.BTCUSDT");
781
782 let ack = BybitWsSubscriptionMsg {
783 success: true,
784 op: BybitWsOperation::Subscribe,
785 conn_id: None,
786 req_id: None,
787 ret_msg: None,
788 };
789
790 handler.handle_subscription_ack(&ack);
791
792 assert!(handler.subscriptions.pending_subscribe_topics().is_empty());
794 }
795}