use cow_sdk::{
CowError, ErrorClass,
core::{CoreError, TransportErrorClass, ValidationError},
orderbook::{OrderbookApiError, OrderbookError, ResponseBody},
signing::SigningError,
trading::TradingError,
};
use serde_json::json;
use std::time::Duration;
fn rejected(status: u16, error_type: &str) -> OrderbookError {
OrderbookApiError::new(
status,
"diagnostic",
ResponseBody::Json(json!({ "errorType": error_type, "description": "x" })),
)
.into()
}
fn api(status: u16) -> OrderbookError {
OrderbookError::Api(Box::new(OrderbookApiError::new(
status,
"diagnostic",
ResponseBody::Text("opaque body".to_owned()),
)))
}
#[test]
fn exhausted_retry_429_classifies_as_rate_limited() {
assert_eq!(
CowError::Orderbook(api(429)).class(),
ErrorClass::RateLimited,
);
assert_eq!(
CowError::Orderbook(rejected(429, "Forbidden")).class(),
ErrorClass::RateLimited,
);
}
#[test]
fn non_429_remote_responses_stay_remote() {
assert_eq!(CowError::Orderbook(api(500)).class(), ErrorClass::Remote);
assert_eq!(
CowError::Orderbook(rejected(400, "ZeroAmount")).class(),
ErrorClass::Remote,
);
}
#[test]
fn error_class_partitions_every_bucket() {
assert_eq!(
CoreError::Validation(ValidationError::EmptyField { field: "appCode" }).class(),
ErrorClass::Validation
);
assert_eq!(TradingError::MissingOwner.class(), ErrorClass::Validation);
assert_eq!(
OrderbookError::Transport {
class: TransportErrorClass::Connect,
detail: "connect failed".to_owned().into(),
}
.class(),
ErrorClass::Transport
);
assert_eq!(api(500).class(), ErrorClass::Remote);
assert_eq!(api(429).class(), ErrorClass::RateLimited);
assert_eq!(
SigningError::Signer {
operation: "typed-data signature",
message: "signer failed".to_owned().into(),
}
.class(),
ErrorClass::Signing
);
assert_eq!(CoreError::Cancelled.class(), ErrorClass::Cancelled);
assert_eq!(OrderbookError::Cancelled.class(), ErrorClass::Cancelled);
assert_eq!(TradingError::Cancelled.class(), ErrorClass::Cancelled);
assert_eq!(
CoreError::Serialization("decode failed".to_owned().into()).class(),
ErrorClass::Internal
);
}
#[test]
fn error_class_delegates_through_trading_and_facade() {
assert_eq!(
TradingError::Orderbook(api(429)).class(),
ErrorClass::RateLimited
);
let transport = OrderbookError::Transport {
class: TransportErrorClass::Connect,
detail: "connect failed".to_owned().into(),
};
let leaf_class = transport.class();
assert_eq!(CowError::from(transport).class(), leaf_class);
assert_eq!(
CowError::from(TradingError::Cancelled).class(),
ErrorClass::Cancelled
);
}
fn api_retry_after(status: u16, retry_after: Option<Duration>) -> OrderbookError {
OrderbookError::Api(Box::new(
OrderbookApiError::new(
status,
"diagnostic",
ResponseBody::Text("opaque".to_owned()),
)
.with_retry_after(retry_after),
))
}
#[test]
fn is_retryable_delegates_through_trading_and_facade() {
assert!(api(503).is_retryable());
assert!(!api(400).is_retryable());
assert!(
OrderbookError::Transport {
class: TransportErrorClass::Timeout,
detail: "timed out".to_owned().into(),
}
.is_retryable()
);
assert!(CowError::Orderbook(api(503)).is_retryable());
assert!(CowError::Trading(TradingError::Orderbook(api(503))).is_retryable());
assert!(TradingError::Orderbook(api(429)).is_retryable());
assert!(!CowError::from(TradingError::MissingOwner).is_retryable());
assert!(!TradingError::Cancelled.is_retryable());
assert!(!CowError::Orderbook(OrderbookError::Cancelled).is_retryable());
}
#[test]
fn backoff_hint_delegates_through_trading_and_facade() {
assert_eq!(
api_retry_after(429, Some(Duration::from_secs(30))).backoff_hint(),
Some(Duration::from_secs(30))
);
assert_eq!(
CowError::Orderbook(api_retry_after(429, Some(Duration::from_secs(30)))).backoff_hint(),
Some(Duration::from_secs(30))
);
assert_eq!(
TradingError::Orderbook(api_retry_after(503, Some(Duration::from_secs(5)))).backoff_hint(),
Some(Duration::from_secs(5))
);
assert_eq!(
CowError::Orderbook(api_retry_after(503, None)).backoff_hint(),
None
);
assert_eq!(
CowError::from(TradingError::MissingOwner).backoff_hint(),
None
);
}
#[cfg(feature = "subgraph")]
mod subgraph {
use super::{CowError, ErrorClass};
use cow_sdk::core::TransportErrorClass;
use cow_sdk::subgraph::{SubgraphError, SubgraphRequestErrorContext};
fn ctx() -> Box<SubgraphRequestErrorContext> {
Box::new(SubgraphRequestErrorContext::new(
1,
"https://gateway.thegraph.com/api/<redacted>/subgraphs/id/x",
"query Totals { totals { orders } }",
Some("Totals".to_owned()),
None,
))
}
#[test]
fn subgraph_error_class_partitions_every_bucket() {
assert_eq!(
SubgraphError::UnsupportedNetwork { chain_id: 999_999 }.class(),
ErrorClass::Validation
);
assert_eq!(
SubgraphError::HttpStatus {
context: ctx(),
status: 429,
body: "throttled".to_owned().into()
}
.class(),
ErrorClass::RateLimited
);
assert_eq!(
SubgraphError::HttpStatus {
context: ctx(),
status: 500,
body: "boom".to_owned().into()
}
.class(),
ErrorClass::Remote
);
assert_eq!(
SubgraphError::GraphQl {
context: ctx(),
errors: Vec::new()
}
.class(),
ErrorClass::Remote
);
assert_eq!(
SubgraphError::Transport {
context: ctx(),
class: TransportErrorClass::Connect,
details: "connect failed".to_owned().into(),
}
.class(),
ErrorClass::Transport
);
assert_eq!(SubgraphError::Cancelled.class(), ErrorClass::Cancelled);
assert_eq!(SubgraphError::NoTotalsFound.class(), ErrorClass::Internal);
assert_eq!(
SubgraphError::MissingData { context: ctx() }.class(),
ErrorClass::Internal
);
}
#[test]
fn subgraph_error_class_delegates_through_facade() {
let leaf = SubgraphError::UnsupportedNetwork { chain_id: 999_999 };
let leaf_class = leaf.class();
assert_eq!(CowError::Subgraph(leaf).class(), leaf_class);
assert_eq!(
CowError::from(SubgraphError::Cancelled).class(),
ErrorClass::Cancelled
);
}
}