use prost::Message;
use super::*;
use crate::common::test_utils::helpers::{error_envelope, proto_response};
use crate::messages::{is_informational_code, ResponseMessage, DATA_ADVISORY_CODES, WARNING_CODE_RANGE};
#[test]
fn test_decoded_error_default() {
let d = DecodedError::default();
assert_eq!(d.request_id, UNSPECIFIED_REQUEST_ID);
assert_eq!(d.error_code, 0);
assert_eq!(d.error_message, "");
assert_eq!(d.error_time, None);
assert_eq!(d.advanced_order_reject_json, "");
}
#[test]
fn test_notice_from_decoded_preserves_rich_payload() {
use crate::messages::Notice;
use time::OffsetDateTime;
let payload = DecodedError {
request_id: 42,
error_code: 2104,
error_message: "Market data farm OK".into(),
error_time: Some(1_700_000_000_000),
advanced_order_reject_json: "{\"reject\":1}".into(),
};
let notice = Notice::from(payload);
assert_eq!(notice.code, 2104);
assert_eq!(notice.message, "Market data farm OK");
assert_eq!(notice.advanced_order_reject_json, "{\"reject\":1}");
let expected = OffsetDateTime::from_unix_timestamp_nanos(1_700_000_000_000_i128 * 1_000_000).unwrap();
assert_eq!(notice.error_time, Some(expected));
}
#[test]
fn test_notice_from_decoded_missing_optionals() {
use crate::messages::Notice;
let payload = DecodedError {
request_id: -1,
error_code: 200,
error_message: "no security".into(),
error_time: None,
advanced_order_reject_json: String::new(),
};
let notice = Notice::from(payload);
assert_eq!(notice.code, 200);
assert_eq!(notice.error_time, None);
assert_eq!(notice.advanced_order_reject_json, "");
}
#[test]
fn test_error_from_decoded_projects_to_notice() {
let payload = DecodedError {
request_id: 42,
error_code: 200,
error_message: "no security".into(),
error_time: None,
advanced_order_reject_json: String::new(),
};
let err = crate::Error::from(payload);
match err {
crate::Error::Notice(notice) => {
assert_eq!(notice.code, 200);
assert_eq!(notice.message, "no security");
}
other => panic!("expected Error::Notice, got {other:?}"),
}
}
#[test]
fn test_determine_routing_error_protobuf_malformed() {
let raw_bytes = vec![0xFFu8; 16];
let message = proto_response(IncomingMessages::Error, raw_bytes);
match determine_routing(&message) {
RoutingDecision::Error(payload) => {
assert_eq!(payload.request_id, UNSPECIFIED_REQUEST_ID);
assert_eq!(payload.error_code, 0);
assert_eq!(payload.error_message, "");
}
routing => panic!("Expected Error routing, got {routing:?}"),
}
}
#[test]
fn test_determine_routing_by_request_id() {
let message_str = "63\01\0123\0DU123456\0AccountType\0ADVISOR\0USD\0";
let message = ResponseMessage::from(message_str);
match determine_routing(&message) {
RoutingDecision::ByRequestId(id) => assert_eq!(id, 123),
routing => panic!("Expected ByRequestId routing, got {routing:?}"),
}
}
#[test]
fn test_determine_routing_error_protobuf() {
let mut envelope = error_envelope(Some(42), Some(2100), "Market data farm connection is OK");
envelope.error_time = Some(1700000000000);
envelope.advanced_order_reject_json = Some("{\"hint\":\"check filters\"}".to_string());
let message = proto_response(IncomingMessages::Error, envelope.encode_to_vec());
match determine_routing(&message) {
RoutingDecision::Error(payload) => {
assert_eq!(payload.request_id, 42);
assert_eq!(payload.error_code, 2100);
assert_eq!(payload.error_message, "Market data farm connection is OK");
assert_eq!(payload.error_time, Some(1700000000000));
assert_eq!(payload.advanced_order_reject_json, "{\"hint\":\"check filters\"}");
}
routing => panic!("Expected Error routing, got {routing:?}"),
}
}
#[test]
fn test_determine_routing_error_protobuf_unspecified_id() {
let envelope = error_envelope(None, Some(2104), "Market data farm connection is OK");
let message = proto_response(IncomingMessages::Error, envelope.encode_to_vec());
match determine_routing(&message) {
RoutingDecision::Error(payload) => {
assert_eq!(payload.request_id, UNSPECIFIED_REQUEST_ID);
assert_eq!(payload.error_code, 2104);
assert_eq!(payload.error_message, "Market data farm connection is OK");
assert_eq!(payload.error_time, None);
assert_eq!(payload.advanced_order_reject_json, "");
}
routing => panic!("Expected Error routing, got {routing:?}"),
}
}
#[test]
fn test_determine_routing_shared_message() {
let message_str = "15\01\0DU123456,DU234567\0";
let message = ResponseMessage::from(message_str);
match determine_routing(&message) {
RoutingDecision::SharedMessage(msg_type) => {
assert_eq!(msg_type, IncomingMessages::ManagedAccounts);
}
routing => panic!("Expected SharedMessage routing, got {routing:?}"),
}
}
#[test]
fn test_is_informational_code() {
assert!(is_informational_code(*WARNING_CODE_RANGE.start(), ""));
assert!(is_informational_code(*WARNING_CODE_RANGE.end(), ""));
assert!(is_informational_code(2119, ""));
assert!(is_informational_code(2150, ""));
assert!(!is_informational_code(*WARNING_CODE_RANGE.start() - 1, ""));
assert!(!is_informational_code(*WARNING_CODE_RANGE.end() + 1, ""));
assert!(!is_informational_code(200, ""));
assert!(!is_informational_code(2300, ""));
assert!(is_informational_code(0, "Warning: Approaching max rate of 50 messages per second (42)"));
assert!(is_informational_code(0, ""));
}
#[test]
fn test_is_informational_code_data_advisory_codes() {
for &code in DATA_ADVISORY_CODES {
assert!(is_informational_code(code, ""), "advisory code {code} should route as a notice");
}
assert!(!is_informational_code(316, ""), "depth HALTED must stay terminal");
}
#[test]
fn test_is_informational_code_classifies_order_message_from_text() {
assert!(is_informational_code(
399,
"Order Message:\nSELL 1 ES DEC'26\nWarning: Your order will not be placed at the exchange until 2026-08-17 08:30:00 US/Central.",
));
assert!(!is_informational_code(399, "Order Message:\nOrder cannot be transmitted"));
}
#[test]
fn test_is_informational_code_order_cancelled_code() {
assert!(is_informational_code(crate::messages::ORDER_CANCELLED_CODE, ""));
assert!(!is_informational_code(201, ""));
assert!(!is_informational_code(203, ""));
assert!(!crate::messages::is_warning_message(crate::messages::ORDER_CANCELLED_CODE, ""));
}
#[test]
fn test_classify_error_order_cancelled_routed_is_notice() {
let payload = DecodedError {
request_id: 42,
error_code: crate::messages::ORDER_CANCELLED_CODE,
error_message: "Order Canceled - reason:".into(),
..Default::default()
};
match classify_error(payload) {
ErrorDisposition::Route(42, RoutedItem::Notice(notice)) => {
assert_eq!(notice.code, crate::messages::ORDER_CANCELLED_CODE);
assert!(notice.is_cancellation());
assert!(notice.is_informational());
assert_eq!(notice.category(), crate::messages::NoticeCategory::Cancellation);
}
other => panic!("expected routed Notice, got {other:?}"),
}
}
#[test]
fn test_is_informational_code_system_message_codes() {
for code in crate::messages::SYSTEM_MESSAGE_CODES {
assert!(is_informational_code(code, ""), "system code {code} should route as a notice");
assert!(!crate::messages::is_warning_message(code, ""));
}
for code in [1099, 1103, 1299, 1301] {
assert!(!is_informational_code(code, ""), "code {code} should not route as a notice");
}
}
#[test]
fn test_classify_error_unrouted_system_message_is_notice_only() {
let payload = DecodedError {
error_code: crate::messages::CONNECTIVITY_RESTORED_DATA_MAINTAINED_CODE,
error_message: "Connectivity between IB and TWS has been restored - data maintained.".into(),
..Default::default()
};
match classify_error(payload) {
ErrorDisposition::NoticeOnly(notice) => {
assert!(notice.is_system_message());
assert!(!notice.is_warning());
assert_eq!(notice.category(), crate::messages::NoticeCategory::SystemMessage);
}
other => panic!("expected NoticeOnly, got {other:?}"),
}
}
#[test]
fn test_classify_error_routed_system_message_is_notice() {
let payload = DecodedError {
request_id: 42,
error_code: crate::messages::CONNECTIVITY_LOST_CODE,
error_message: "Connectivity between IB and TWS has been lost.".into(),
..Default::default()
};
match classify_error(payload) {
ErrorDisposition::Route(42, RoutedItem::Notice(notice)) => {
assert_eq!(notice.code, crate::messages::CONNECTIVITY_LOST_CODE);
assert!(notice.is_system_message());
}
other => panic!("expected routed Notice, got {other:?}"),
}
}
#[test]
fn test_order_update_notice_gating() {
let payload = DecodedError {
request_id: 42,
error_code: 201,
error_message: "Order rejected".into(),
error_time: None,
advanced_order_reject_json: String::new(),
};
let notice = order_update_notice(&payload, false).expect("order-bound error should produce a notice");
assert_eq!(notice.request_id, Some(42));
assert_eq!(notice.code, 201);
assert!(order_update_notice(&payload, true).is_none());
let request_less = DecodedError {
request_id: UNSPECIFIED_REQUEST_ID,
..payload
};
assert!(order_update_notice(&request_less, false).is_none());
}
#[test]
fn test_classify_error_unrouted_warning_is_notice_only() {
let payload = DecodedError {
error_code: 2104,
error_message: "Market data farm OK".into(),
..Default::default()
};
match classify_error(payload) {
ErrorDisposition::NoticeOnly(notice) => {
assert_eq!(notice.code, 2104);
assert_eq!(notice.message, "Market data farm OK");
}
other => panic!("expected NoticeOnly, got {other:?}"),
}
}
#[test]
fn test_classify_error_unrouted_hard_error_fails_one_shots() {
let payload = DecodedError {
error_code: 321,
error_message: "Server error".into(),
..Default::default()
};
match classify_error(payload) {
ErrorDisposition::NoticeAndFailOneShots(notice, error) => {
assert_eq!(notice.code, 321);
assert_eq!(notice.message, "Server error");
match error {
crate::Error::Notice(error_notice) => assert_eq!(error_notice, notice),
other => panic!("expected Error::Notice, got {other:?}"),
}
}
other => panic!("expected NoticeAndFailOneShots, got {other:?}"),
}
}
#[test]
fn test_classify_error_codeless_notice_is_notice_only() {
let envelope = error_envelope(None, None, "Warning: Approaching max rate of 50 messages per second (42)");
let message = proto_response(IncomingMessages::Error, envelope.encode_to_vec());
let payload = match determine_routing(&message) {
RoutingDecision::Error(payload) => payload,
routing => panic!("Expected Error routing, got {routing:?}"),
};
assert_eq!(payload.request_id, UNSPECIFIED_REQUEST_ID);
assert_eq!(payload.error_code, 0);
match classify_error(payload) {
ErrorDisposition::NoticeOnly(notice) => {
assert_eq!(notice.code, 0);
assert_eq!(notice.message, "Warning: Approaching max rate of 50 messages per second (42)");
assert!(notice.is_warning());
assert_eq!(notice.category(), crate::messages::NoticeCategory::Warning);
}
other => panic!("expected NoticeOnly, got {other:?}"),
}
}
#[test]
fn test_classify_error_codeless_routed_is_notice() {
let payload = DecodedError {
request_id: 42,
error_code: 0,
error_message: "Warning: Approaching max rate of 50 messages per second (42)".into(),
..Default::default()
};
match classify_error(payload) {
ErrorDisposition::Route(42, RoutedItem::Notice(notice)) => {
assert_eq!(notice.code, 0);
}
other => panic!("expected routed Notice, got {other:?}"),
}
}
#[test]
fn test_classify_error_routed_warning_is_notice() {
let payload = DecodedError {
request_id: 42,
error_code: 2104,
error_message: "Farm OK".into(),
..Default::default()
};
match classify_error(payload) {
ErrorDisposition::Route(42, RoutedItem::Notice(notice)) => {
assert_eq!(notice.code, 2104);
assert_eq!(notice.message, "Farm OK");
}
other => panic!("expected routed Notice, got {other:?}"),
}
}
#[test]
fn test_classify_error_routed_hard_error_is_error() {
let payload = DecodedError {
request_id: 7,
error_code: 200,
error_message: "No security".into(),
..Default::default()
};
match classify_error(payload) {
ErrorDisposition::Route(7, RoutedItem::Error(crate::Error::Notice(notice))) => {
assert_eq!(notice.code, 200);
assert_eq!(notice.message, "No security");
}
other => panic!("expected routed Error, got {other:?}"),
}
}
#[test]
fn test_order_message_routing_without_order_id_returns_sentinel() {
let completed_orders_end = proto_response(IncomingMessages::CompletedOrdersEnd, Vec::new());
match determine_routing(&completed_orders_end) {
RoutingDecision::ByOrderId(id) => assert_eq!(id, -1),
routing => panic!("Expected ByOrderId(-1) routing, got {routing:?}"),
}
let commission_report = proto_response(
IncomingMessages::CommissionsReport,
crate::proto::CommissionAndFeesReport {
exec_id: Some("exec123".into()),
..Default::default()
}
.encode_to_vec(),
);
match determine_routing(&commission_report) {
RoutingDecision::ByOrderId(id) => assert_eq!(id, -1),
routing => panic!("Expected ByOrderId(-1) routing, got {routing:?}"),
}
}
#[test]
fn test_determine_routing_protobuf_open_order() {
let bytes = crate::proto::OpenOrder {
order_id: Some(58),
..Default::default()
}
.encode_to_vec();
let message = proto_response(IncomingMessages::OpenOrder, bytes);
match determine_routing(&message) {
RoutingDecision::ByOrderId(id) => assert_eq!(id, 58),
routing => panic!("Expected ByOrderId(58), got {routing:?}"),
}
}
#[test]
fn test_determine_routing_protobuf_order_status() {
let bytes = crate::proto::OrderStatus {
order_id: Some(58),
status: Some("Filled".into()),
..Default::default()
}
.encode_to_vec();
let message = proto_response(IncomingMessages::OrderStatus, bytes);
match determine_routing(&message) {
RoutingDecision::ByOrderId(id) => assert_eq!(id, 58),
routing => panic!("Expected ByOrderId(58), got {routing:?}"),
}
}
#[test]
fn test_determine_routing_protobuf_execution_data_uses_nested_order_id() {
let bytes = crate::proto::ExecutionDetails {
req_id: Some(-1),
contract: None,
execution: Some(crate::proto::Execution {
order_id: Some(58),
..Default::default()
}),
}
.encode_to_vec();
let message = proto_response(IncomingMessages::ExecutionData, bytes);
match determine_routing(&message) {
RoutingDecision::ByOrderId(id) => assert_eq!(id, 58),
routing => panic!("Expected ByOrderId(58), got {routing:?}"),
}
}
#[test]
fn test_determine_routing_protobuf_execution_data_end() {
let bytes = crate::proto::ExecutionDetailsEnd { req_id: Some(7) }.encode_to_vec();
let message = proto_response(IncomingMessages::ExecutionDataEnd, bytes);
match determine_routing(&message) {
RoutingDecision::ByOrderId(id) => assert_eq!(id, 7),
routing => panic!("Expected ByOrderId(7), got {routing:?}"),
}
}
#[test]
fn test_determine_routing_protobuf_commissions_report_no_order_id() {
let bytes = crate::proto::CommissionAndFeesReport {
exec_id: Some("0000e0d5.69fb6496.01.01".into()),
..Default::default()
}
.encode_to_vec();
let message = proto_response(IncomingMessages::CommissionsReport, bytes);
match determine_routing(&message) {
RoutingDecision::ByOrderId(id) => assert_eq!(id, -1),
routing => panic!("Expected ByOrderId(-1), got {routing:?}"),
}
}
#[test]
fn test_determine_routing_protobuf_request_id_message() {
let bytes = crate::proto::AccountSummary {
req_id: Some(314),
..Default::default()
}
.encode_to_vec();
let message = proto_response(IncomingMessages::AccountSummary, bytes);
match determine_routing(&message) {
RoutingDecision::ByRequestId(id) => assert_eq!(id, 314),
routing => panic!("Expected ByRequestId(314), got {routing:?}"),
}
}
#[test]
fn test_determine_routing_protobuf_market_data_type() {
let bytes = crate::proto::MarketDataType {
req_id: Some(9001),
market_data_type: Some(3),
}
.encode_to_vec();
let message = proto_response(IncomingMessages::MarketDataType, bytes);
match determine_routing(&message) {
RoutingDecision::ByRequestId(id) => assert_eq!(id, 9001),
routing => panic!("Expected ByRequestId(9001), got {routing:?}"),
}
}
#[test]
fn test_first_unroutable_by_request_id_accepts_registered_types() {
let declared = &[
IncomingMessages::TickPrice, IncomingMessages::CommissionsReport, ];
assert_eq!(first_unroutable_by_request_id(declared), None);
assert_eq!(first_unroutable_by_request_id(&[]), None);
}
#[test]
fn test_first_unroutable_by_request_id_rejects_error() {
assert_eq!(
first_unroutable_by_request_id(&[IncomingMessages::TickPrice, IncomingMessages::Error]),
Some(IncomingMessages::Error)
);
}
#[test]
fn test_first_unroutable_by_request_id_reports_first_offender() {
let declared = &[IncomingMessages::TickPrice, IncomingMessages::FamilyCodes, IncomingMessages::MarketRule];
assert_eq!(first_unroutable_by_request_id(declared), Some(IncomingMessages::FamilyCodes));
}
#[test]
fn test_unknown_message_type_routes_by_message_type() {
let message = ResponseMessage::from("9999\01\0");
assert_eq!(message.message_type(), IncomingMessages::NotValid);
assert_eq!(determine_routing(&message), RoutingDecision::ByMessageType(IncomingMessages::NotValid));
}