use prost::Message;
use super::*;
use crate::client::ids::{OrderId, RequestId};
use crate::common::test_utils::helpers::{error_envelope, proto_response};
use crate::messages::{is_informational_code, ResponseMessage, DATA_ADVISORY_CODES, WARNING_CODE_RANGE};
use crate::testdata::builders::orders::next_valid_id;
use crate::testdata::builders::ResponseProtoEncoder;
#[test]
fn 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 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 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 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 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 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 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 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 determine_routing_shared_message() {
let cases = [
(
IncomingMessages::ManagedAccounts,
crate::proto::ManagedAccounts {
accounts_list: Some("DU123456,DU234567".into()),
}
.encode_to_vec(),
),
(IncomingMessages::NextValidId, next_valid_id().order_id(90).encode_proto()),
(
IncomingMessages::CurrentTime,
crate::proto::CurrentTime {
current_time: Some(1_700_000_000),
}
.encode_to_vec(),
),
];
for (kind, body) in cases {
match determine_routing(&proto_response(kind, body)) {
RoutingDecision::ByMessageType(routed) => assert_eq!(routed, kind),
routing => panic!("{kind:?}: expected ByMessageType routing, got {routing:?}"),
}
}
}
#[test]
fn is_informational_code_works() {
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 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 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 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, ""));
}
#[test]
fn 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(id, RoutedItem::Notice(notice)) if id == WireId::Order(OrderId::from(42)) => {
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 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");
}
for code in [1099, 1103, 1299, 1301] {
assert!(!is_informational_code(code, ""), "code {code} should not route as a notice");
}
}
#[test]
fn 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_eq!(notice.category(), crate::messages::NoticeCategory::SystemMessage);
}
other => panic!("expected NoticeOnly, got {other:?}"),
}
}
#[test]
fn 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(id, RoutedItem::Notice(notice)) if id == WireId::Order(OrderId::from(42)) => {
assert_eq!(notice.code, crate::messages::CONNECTIVITY_LOST_CODE);
assert!(notice.is_system_message());
}
other => panic!("expected routed Notice, got {other:?}"),
}
}
#[test]
fn 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).expect("order-bound error should produce a notice");
assert_eq!(notice.request_id, Some(42));
assert_eq!(notice.code, 201);
let request_bound = DecodedError {
request_id: RequestId::nth(0).raw(),
..payload.clone()
};
assert!(order_update_notice(&request_bound).is_none());
let request_less = DecodedError {
request_id: UNSPECIFIED_REQUEST_ID,
..payload
};
assert!(order_update_notice(&request_less).is_none());
}
#[test]
fn classify_error_routes_by_id_range() {
let error = |request_id| DecodedError {
request_id,
error_code: 200,
error_message: "No security definition".into(),
..Default::default()
};
let request_id = RequestId::nth(3);
assert!(matches!(
classify_error(error(request_id.raw())),
ErrorDisposition::Route(WireId::Request(id), RoutedItem::Error(_)) if id == request_id
));
assert!(matches!(
classify_error(error(42)),
ErrorDisposition::Route(WireId::Order(id), RoutedItem::Error(_)) if id == OrderId::from(42)
));
}
#[test]
fn 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 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 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 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(id, RoutedItem::Notice(notice)) if id == WireId::Order(OrderId::from(42)) => {
assert_eq!(notice.code, 0);
}
other => panic!("expected routed Notice, got {other:?}"),
}
}
#[test]
fn 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(id, RoutedItem::Notice(notice)) if id == WireId::Order(OrderId::from(42)) => {
assert_eq!(notice.code, 2104);
assert_eq!(notice.message, "Farm OK");
}
other => panic!("expected routed Notice, got {other:?}"),
}
}
#[test]
fn 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(id, RoutedItem::Error(crate::Error::Notice(notice))) if id == WireId::Order(OrderId::from(7)) => {
assert_eq!(notice.code, 200);
assert_eq!(notice.message, "No security");
}
other => panic!("expected routed Error, got {other:?}"),
}
}
#[test]
fn 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 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 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 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 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 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 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 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 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 first_unroutable_by_request_id_rejects_error() {
assert_eq!(
first_unroutable_by_request_id(&[IncomingMessages::TickPrice, IncomingMessages::Error]),
Some(IncomingMessages::Error)
);
}
#[test]
fn 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 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));
}
#[test]
fn order_message_types_route_by_order_id() {
let id_at_tag_1 = crate::proto::ExecutionDetailsEnd { req_id: Some(7) }.encode_to_vec();
let order_types = [
IncomingMessages::OrderStatus,
IncomingMessages::OrderBound,
IncomingMessages::OpenOrder,
IncomingMessages::OpenOrderEnd,
IncomingMessages::CompletedOrder,
IncomingMessages::CompletedOrdersEnd,
IncomingMessages::ExecutionData,
IncomingMessages::ExecutionDataEnd,
IncomingMessages::CommissionsReport,
];
for message_type in order_types {
let message = proto_response(message_type, id_at_tag_1.clone());
let routing = determine_routing(&message);
assert!(
matches!(routing, RoutingDecision::ByOrderId(_)),
"{message_type:?}: expected ByOrderId, got {routing:?}"
);
}
}
#[test]
fn order_routing_strategy_for_own_and_foreign_frames() {
use IncomingMessages as M;
use OrderRoutingStrategy as S;
let cases = [
(M::OpenOrder, S::OrderAndShared, S::SharedOnly),
(M::OrderStatus, S::OrderAndShared, S::SharedOnly),
(M::ExecutionData, S::ExecutionData, S::ByRequestId),
(M::OrderBound, S::OrderUpdateOnly, S::Ignore),
(M::CommissionsReport, S::ByExecutionId, S::ByExecutionId),
(M::ExecutionDataEnd, S::ExecutionDataEnd, S::ExecutionDataEnd),
(M::CompletedOrder, S::SharedOnly, S::SharedOnly),
(M::OpenOrderEnd, S::SharedOnly, S::SharedOnly),
];
for (message_type, own, foreign) in cases {
assert_eq!(order_routing_strategy(message_type, false), own, "{message_type:?} own");
assert_eq!(order_routing_strategy(message_type, true), foreign, "{message_type:?} foreign");
}
}
#[test]
fn is_foreign_compares_client_ids() {
let status = |client_id: Option<i32>| {
let body = crate::proto::OrderStatus {
order_id: Some(7),
client_id,
..Default::default()
};
proto_response(IncomingMessages::OrderStatus, body.encode_to_vec())
};
let cases = [
("same client", Some(28), false),
("other client", Some(29), true),
("no client id", None, false),
];
for (name, client_id, expected) in cases {
assert_eq!(is_foreign(&status(client_id), 28), expected, "{name}");
}
}