tests: unit tests for client.rs and auth.rs

This commit is contained in:
floor-licker
2025-11-04 23:26:07 -05:00
parent f7921de2c3
commit 0d13e93705
4 changed files with 1554 additions and 1 deletions
+147
View File
@@ -188,6 +188,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::client::ApiCreds;
#[test]
fn test_unix_timestamp() {
@@ -206,4 +207,150 @@ mod tests {
);
assert!(result.is_ok());
}
#[test]
fn test_hmac_signature_with_body() {
let body = r#"{"test": "data"}"#;
let result = build_hmac_signature(
"test_secret",
1234567890,
"POST",
"/orders",
Some(body),
);
assert!(result.is_ok());
let signature = result.unwrap();
assert!(!signature.is_empty());
}
#[test]
fn test_hmac_signature_consistency() {
let secret = "test_secret";
let timestamp = 1234567890;
let method = "GET";
let path = "/test";
let sig1 = build_hmac_signature::<String>(secret, timestamp, method, path, None).unwrap();
let sig2 = build_hmac_signature::<String>(secret, timestamp, method, path, None).unwrap();
// Same inputs should produce same signature
assert_eq!(sig1, sig2);
}
#[test]
fn test_hmac_signature_different_inputs() {
let secret = "test_secret";
let timestamp = 1234567890;
let sig1 = build_hmac_signature::<String>(secret, timestamp, "GET", "/test", None).unwrap();
let sig2 = build_hmac_signature::<String>(secret, timestamp, "POST", "/test", None).unwrap();
let sig3 = build_hmac_signature::<String>(secret, timestamp, "GET", "/other", None).unwrap();
// Different inputs should produce different signatures
assert_ne!(sig1, sig2);
assert_ne!(sig1, sig3);
assert_ne!(sig2, sig3);
}
#[test]
fn test_create_l1_headers() {
use alloy_signer_local::PrivateKeySigner;
use alloy_primitives::U256;
let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
let result = create_l1_headers(&signer, Some(U256::from(12345)));
assert!(result.is_ok());
let headers = result.unwrap();
assert!(headers.contains_key("POLY_ADDRESS"));
assert!(headers.contains_key("POLY_SIGNATURE"));
assert!(headers.contains_key("POLY_TIMESTAMP"));
assert!(headers.contains_key("POLY_NONCE"));
}
#[test]
fn test_create_l1_headers_different_nonces() {
use alloy_signer_local::PrivateKeySigner;
use alloy_primitives::U256;
let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
let headers_1 = create_l1_headers(&signer, Some(U256::from(12345))).unwrap();
let headers_2 = create_l1_headers(&signer, Some(U256::from(54321))).unwrap();
// Different nonces should produce different signatures
assert_ne!(
headers_1.get("POLY_SIGNATURE"),
headers_2.get("POLY_SIGNATURE")
);
// But same address
assert_eq!(
headers_1.get("POLY_ADDRESS"),
headers_2.get("POLY_ADDRESS")
);
}
#[test]
fn test_create_l2_headers() {
use alloy_signer_local::PrivateKeySigner;
let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
let api_creds = ApiCredentials {
api_key: "test_key".to_string(),
secret: "test_secret".to_string(),
passphrase: "test_passphrase".to_string(),
};
let result = create_l2_headers(&signer, &api_creds, "/test", "GET", None);
assert!(result.is_ok());
let headers = result.unwrap();
assert!(headers.contains_key("POLY_API_KEY"));
assert!(headers.contains_key("POLY_SIGNATURE"));
assert!(headers.contains_key("POLY_TIMESTAMP"));
assert!(headers.contains_key("POLY_PASSPHRASE"));
assert_eq!(headers.get("POLY_API_KEY").unwrap(), "test_key");
assert_eq!(headers.get("POLY_PASSPHRASE").unwrap(), "test_passphrase");
}
#[test]
fn test_eip712_signature_format() {
use alloy_signer_local::PrivateKeySigner;
use alloy_primitives::U256;
let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
// Test that we can create and sign EIP-712 messages
let result = create_l1_headers(&signer, Some(U256::from(12345)));
assert!(result.is_ok());
let headers = result.unwrap();
let signature = headers.get("POLY_SIGNATURE").unwrap();
// EIP-712 signatures should be hex strings of specific length
assert!(signature.starts_with("0x"));
assert_eq!(signature.len(), 132); // 0x + 130 hex chars = 132 total
}
#[test]
fn test_timestamp_generation() {
let ts1 = get_current_unix_time_secs();
std::thread::sleep(std::time::Duration::from_millis(1));
let ts2 = get_current_unix_time_secs();
// Timestamps should be increasing
assert!(ts2 >= ts1);
// Should be reasonable current time (after 2020, before 2030)
assert!(ts1 > 1_600_000_000);
assert!(ts1 < 1_900_000_000);
}
}
+456 -1
View File
@@ -1178,4 +1178,459 @@ pub struct CreateOrderOptions {
}
// Re-export for compatibility
pub type PolyfillClient = ClobClient;
pub type PolyfillClient = ClobClient;
#[cfg(test)]
mod tests {
use super::*;
use crate::types::*;
use mockito::{Matcher, Server};
use rust_decimal::Decimal;
use std::str::FromStr;
use tokio;
fn create_test_client(base_url: &str) -> ClobClient {
ClobClient::new(base_url)
}
fn create_test_client_with_auth(base_url: &str) -> ClobClient {
ClobClient::with_l1_headers(
base_url,
"0x1234567890123456789012345678901234567890123456789012345678901234",
137,
)
}
#[tokio::test]
async fn test_client_creation() {
let client = create_test_client("https://test.example.com");
assert_eq!(client.base_url, "https://test.example.com");
assert!(client.signer.is_none());
assert!(client.api_creds.is_none());
}
#[tokio::test]
async fn test_client_with_l1_headers() {
let client = create_test_client_with_auth("https://test.example.com");
assert_eq!(client.base_url, "https://test.example.com");
assert!(client.signer.is_some());
assert_eq!(client.chain_id, 137);
}
#[tokio::test]
async fn test_client_with_l2_headers() {
let api_creds = ApiCreds {
key: "test_key".to_string(),
secret: "test_secret".to_string(),
passphrase: "test_passphrase".to_string(),
};
let client = ClobClient::with_l2_headers(
"https://test.example.com",
"0x1234567890123456789012345678901234567890123456789012345678901234",
137,
api_creds.clone(),
);
assert_eq!(client.base_url, "https://test.example.com");
assert!(client.signer.is_some());
assert!(client.api_creds.is_some());
assert_eq!(client.chain_id, 137);
}
#[tokio::test]
async fn test_set_api_creds() {
let mut client = create_test_client("https://test.example.com");
assert!(client.api_creds.is_none());
let api_creds = ApiCreds {
key: "test_key".to_string(),
secret: "test_secret".to_string(),
passphrase: "test_passphrase".to_string(),
};
client.set_api_creds(api_creds.clone());
assert!(client.api_creds.is_some());
assert_eq!(client.api_creds.unwrap().key, "test_key");
}
#[tokio::test]
async fn test_get_sampling_markets_success() {
let mut server = Server::new_async().await;
let mock_response = r#"{
"limit": "10",
"count": "2",
"next_cursor": null,
"data": [
{
"condition_id": "0x123",
"tokens": [
{"token_id": "0x456", "outcome": "Yes"},
{"token_id": "0x789", "outcome": "No"}
],
"rewards": {
"rates": null,
"min_size": "1.0",
"max_spread": "0.1",
"event_start_date": null,
"event_end_date": null,
"in_game_multiplier": null,
"reward_epoch": null
},
"min_incentive_size": null,
"max_incentive_spread": null,
"active": true,
"closed": false,
"question_id": "0x123",
"minimum_order_size": "1.0",
"minimum_tick_size": "0.01",
"description": "Test market",
"category": "test",
"end_date_iso": null,
"game_start_time": null,
"question": "Will this test pass?",
"market_slug": "test-market",
"seconds_delay": "0",
"icon": "",
"fpmm": ""
}
]
}"#;
let mock = server
.mock("GET", "/sampling-markets")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(mock_response)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.get_sampling_markets(None).await;
mock.assert_async().await;
assert!(result.is_ok());
let markets = result.unwrap();
assert_eq!(markets.data.len(), 1);
assert_eq!(markets.data[0].question, "Will this test pass?");
}
#[tokio::test]
async fn test_get_sampling_markets_with_cursor() {
let mut server = Server::new_async().await;
let mock_response = r#"{
"limit": "5",
"count": "0",
"next_cursor": null,
"data": []
}"#;
let mock = server
.mock("GET", "/sampling-markets")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("cursor".into(), "test_cursor".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(mock_response)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.get_sampling_markets(Some("test_cursor")).await;
mock.assert_async().await;
assert!(result.is_ok());
let markets = result.unwrap();
assert_eq!(markets.data.len(), 0);
}
#[tokio::test]
async fn test_get_order_book_success() {
let mut server = Server::new_async().await;
let mock_response = r#"{
"market": "0x123",
"bids": [
{"price": "0.75", "size": "100.0"}
],
"asks": [
{"price": "0.76", "size": "50.0"}
]
}"#;
let mock = server
.mock("GET", "/book")
.match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(mock_response)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.get_order_book("0x123").await;
mock.assert_async().await;
assert!(result.is_ok());
let book = result.unwrap();
assert_eq!(book.market, "0x123");
assert_eq!(book.bids.len(), 1);
assert_eq!(book.asks.len(), 1);
}
#[tokio::test]
async fn test_get_midpoint_success() {
let mut server = Server::new_async().await;
let mock_response = r#"{
"mid": "0.755"
}"#;
let mock = server
.mock("GET", "/midpoint")
.match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(mock_response)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.get_midpoint("0x123").await;
mock.assert_async().await;
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.mid, Decimal::from_str("0.755").unwrap());
}
#[tokio::test]
async fn test_get_spread_success() {
let mut server = Server::new_async().await;
let mock_response = r#"{
"spread": "0.01"
}"#;
let mock = server
.mock("GET", "/spread")
.match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(mock_response)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.get_spread("0x123").await;
mock.assert_async().await;
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.spread, Decimal::from_str("0.01").unwrap());
}
#[tokio::test]
async fn test_get_price_success() {
let mut server = Server::new_async().await;
let mock_response = r#"{
"price": "0.76"
}"#;
let mock = server
.mock("GET", "/price")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("token_id".into(), "0x123".into()),
Matcher::UrlEncoded("side".into(), "buy".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(mock_response)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.get_price("0x123", Side::BUY).await;
mock.assert_async().await;
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.price, Decimal::from_str("0.76").unwrap());
}
#[tokio::test]
async fn test_get_tick_size_success() {
let mut server = Server::new_async().await;
let mock_response = r#"{
"minimum_tick_size": "0.01"
}"#;
let mock = server
.mock("GET", "/tick-size")
.match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(mock_response)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.get_tick_size("0x123").await;
mock.assert_async().await;
assert!(result.is_ok());
let tick_size = result.unwrap();
assert_eq!(tick_size, Decimal::from_str("0.01").unwrap());
}
#[tokio::test]
async fn test_get_neg_risk_success() {
let mut server = Server::new_async().await;
let mock_response = r#"{
"neg_risk": false
}"#;
let mock = server
.mock("GET", "/neg-risk")
.match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(mock_response)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.get_neg_risk("0x123").await;
mock.assert_async().await;
assert!(result.is_ok());
let neg_risk = result.unwrap();
assert!(!neg_risk);
}
#[tokio::test]
async fn test_api_error_handling() {
let mut server = Server::new_async().await;
let mock = server
.mock("GET", "/book")
.with_status(404)
.with_header("content-type", "application/json")
.with_body(r#"{"error": "Market not found"}"#)
.create_async()
.await;
let client = create_test_client(&server.url());
let result = client.get_order_book("invalid_token").await;
mock.assert_async().await;
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, PolyfillError::Network { .. }));
}
#[tokio::test]
async fn test_network_error_handling() {
// Test with invalid URL to simulate network error
let client = create_test_client("http://invalid-host-that-does-not-exist.com");
let result = client.get_order_book("0x123").await;
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, PolyfillError::Network { .. }));
}
#[test]
fn test_resolve_tick_size() {
let client = create_test_client("https://test.example.com");
// Test with provided tick size
let result = client.resolve_tick_size(Some(Decimal::from_str("0.001").unwrap()));
assert_eq!(result, Decimal::from_str("0.001").unwrap());
// Test with default tick size
let result = client.resolve_tick_size(None);
assert_eq!(result, Decimal::from_str("0.01").unwrap());
}
#[test]
fn test_is_price_in_range() {
let client = create_test_client("https://test.example.com");
let price = Decimal::from_str("0.5").unwrap();
let tick_size = Decimal::from_str("0.01").unwrap();
// Test valid price
assert!(client.is_price_in_range(price, tick_size));
// Test price too low
let low_price = Decimal::from_str("0.005").unwrap();
assert!(!client.is_price_in_range(low_price, tick_size));
// Test price too high
let high_price = Decimal::from_str("0.995").unwrap();
assert!(!client.is_price_in_range(high_price, tick_size));
}
#[tokio::test]
async fn test_get_midpoints_batch() {
let mut server = Server::new_async().await;
let mock_response = r#"{
"0x123": "0.755",
"0x456": "0.623"
}"#;
let mock = server
.mock("GET", "/midpoints")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("token_ids".into(), "0x123,0x456".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(mock_response)
.create_async()
.await;
let client = create_test_client(&server.url());
let token_ids = vec!["0x123".to_string(), "0x456".to_string()];
let result = client.get_midpoints(&token_ids).await;
mock.assert_async().await;
assert!(result.is_ok());
let midpoints = result.unwrap();
assert_eq!(midpoints.len(), 2);
assert_eq!(midpoints.get("0x123").unwrap(), &Decimal::from_str("0.755").unwrap());
assert_eq!(midpoints.get("0x456").unwrap(), &Decimal::from_str("0.623").unwrap());
}
#[test]
fn test_calculate_market_price() {
let client = create_test_client("https://test.example.com");
// Test buy order
let buy_price = client.calculate_market_price(
Decimal::from_str("0.75").unwrap(),
Decimal::from_str("0.76").unwrap(),
Side::BUY,
Some(Decimal::from_str("0.02").unwrap()),
);
assert_eq!(buy_price, Decimal::from_str("0.7752").unwrap()); // 0.76 * 1.02
// Test sell order
let sell_price = client.calculate_market_price(
Decimal::from_str("0.75").unwrap(),
Decimal::from_str("0.76").unwrap(),
Side::SELL,
Some(Decimal::from_str("0.02").unwrap()),
);
assert_eq!(sell_price, Decimal::from_str("0.735").unwrap()); // 0.75 * 0.98
// Test without slippage
let no_slippage_buy = client.calculate_market_price(
Decimal::from_str("0.75").unwrap(),
Decimal::from_str("0.76").unwrap(),
Side::BUY,
None,
);
assert_eq!(no_slippage_buy, Decimal::from_str("0.76").unwrap());
}
}
+214
View File
@@ -381,4 +381,218 @@ mod tests {
let seed2 = generate_seed();
assert_ne!(seed1, seed2);
}
#[test]
fn test_decimal_to_token_u32_edge_cases() {
// Test zero
let result = decimal_to_token_u32(Decimal::ZERO);
assert_eq!(result, 0);
// Test small decimal
let result = decimal_to_token_u32(Decimal::from_str("0.000001").unwrap());
assert_eq!(result, 1);
// Test large number
let result = decimal_to_token_u32(Decimal::from_str("1000.0").unwrap());
assert_eq!(result, 1_000_000_000);
}
#[tokio::test]
async fn test_order_builder_creation() {
use alloy_signer_local::PrivateKeySigner;
let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
let builder = OrderBuilder::new(signer, 137);
assert_eq!(builder.chain_id, 137);
}
#[tokio::test]
async fn test_build_order_success() {
use alloy_signer_local::PrivateKeySigner;
let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
let mut builder = OrderBuilder::new(signer, 137);
let order_args = crate::client::OrderArgs {
token_id: "0x123".to_string(),
price: Decimal::from_str("0.75").unwrap(),
size: Decimal::from_str("100.0").unwrap(),
side: Side::BUY,
};
let result = builder.build_order(&order_args).await;
assert!(result.is_ok());
let signed_order = result.unwrap();
assert_eq!(signed_order.token_id, "0x123");
assert!(!signed_order.signature.is_empty());
}
#[tokio::test]
async fn test_build_order_different_sides() {
use alloy_signer_local::PrivateKeySigner;
let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
let mut builder = OrderBuilder::new(signer, 137);
let buy_order = crate::client::OrderArgs {
token_id: "0x123".to_string(),
price: Decimal::from_str("0.75").unwrap(),
size: Decimal::from_str("100.0").unwrap(),
side: Side::BUY,
};
let sell_order = crate::client::OrderArgs {
token_id: "0x123".to_string(),
price: Decimal::from_str("0.75").unwrap(),
size: Decimal::from_str("100.0").unwrap(),
side: Side::SELL,
};
let buy_result = builder.build_order(&buy_order).await.unwrap();
let sell_result = builder.build_order(&sell_order).await.unwrap();
// Different sides should produce different signatures
assert_ne!(buy_result.signature, sell_result.signature);
// But same other fields
assert_eq!(buy_result.token_id, sell_result.token_id);
assert_eq!(buy_result.maker_amount, sell_result.taker_amount);
assert_eq!(buy_result.taker_amount, sell_result.maker_amount);
}
#[tokio::test]
async fn test_build_order_price_rounding() {
use alloy_signer_local::PrivateKeySigner;
let private_key = "0x1234567890123456789012345678901234567890123456789012345678901234";
let signer: PrivateKeySigner = private_key.parse().expect("Valid private key");
let mut builder = OrderBuilder::new(signer, 137);
// Test price that needs rounding
let order_args = crate::client::OrderArgs {
token_id: "0x123".to_string(),
price: Decimal::from_str("0.753456").unwrap(), // Should round to nearest tick
size: Decimal::from_str("100.0").unwrap(),
side: Side::BUY,
};
let result = builder.build_order(&order_args).await;
assert!(result.is_ok());
let signed_order = result.unwrap();
// The price should be rounded (exact value depends on tick size)
assert!(!signed_order.maker_amount.is_empty());
assert!(!signed_order.taker_amount.is_empty());
}
#[test]
fn test_round_to_tick_size() {
let tick_size = Decimal::from_str("0.01").unwrap();
// Test exact tick
let price = Decimal::from_str("0.75").unwrap();
let rounded = round_to_tick_size(price, tick_size);
assert_eq!(rounded, price);
// Test rounding down
let price = Decimal::from_str("0.754").unwrap();
let rounded = round_to_tick_size(price, tick_size);
assert_eq!(rounded, Decimal::from_str("0.75").unwrap());
// Test rounding up
let price = Decimal::from_str("0.756").unwrap();
let rounded = round_to_tick_size(price, tick_size);
assert_eq!(rounded, Decimal::from_str("0.76").unwrap());
}
#[test]
fn test_round_to_tick_size_different_tick_sizes() {
// Test with 0.001 tick size
let tick_size = Decimal::from_str("0.001").unwrap();
let price = Decimal::from_str("0.7534").unwrap();
let rounded = round_to_tick_size(price, tick_size);
assert_eq!(rounded, Decimal::from_str("0.753").unwrap());
// Test with 0.1 tick size
let tick_size = Decimal::from_str("0.1").unwrap();
let price = Decimal::from_str("0.75").unwrap();
let rounded = round_to_tick_size(price, tick_size);
assert_eq!(rounded, Decimal::from_str("0.8").unwrap());
}
#[test]
fn test_validate_order_args() {
// Test valid order
let valid_order = crate::client::OrderArgs {
token_id: "0x1234567890123456789012345678901234567890".to_string(),
price: Decimal::from_str("0.75").unwrap(),
size: Decimal::from_str("100.0").unwrap(),
side: Side::BUY,
};
let result = validate_order_args(&valid_order);
assert!(result.is_ok());
// Test invalid token ID
let invalid_token = crate::client::OrderArgs {
token_id: "invalid".to_string(),
price: Decimal::from_str("0.75").unwrap(),
size: Decimal::from_str("100.0").unwrap(),
side: Side::BUY,
};
let result = validate_order_args(&invalid_token);
assert!(result.is_err());
// Test zero price
let zero_price = crate::client::OrderArgs {
token_id: "0x1234567890123456789012345678901234567890".to_string(),
price: Decimal::ZERO,
size: Decimal::from_str("100.0").unwrap(),
side: Side::BUY,
};
let result = validate_order_args(&zero_price);
assert!(result.is_err());
// Test zero size
let zero_size = crate::client::OrderArgs {
token_id: "0x1234567890123456789012345678901234567890".to_string(),
price: Decimal::from_str("0.75").unwrap(),
size: Decimal::ZERO,
side: Side::BUY,
};
let result = validate_order_args(&zero_size);
assert!(result.is_err());
}
#[test]
fn test_seed_generation_uniqueness() {
let mut seeds = std::collections::HashSet::new();
// Generate 1000 seeds and ensure they're all unique
for _ in 0..1000 {
let seed = generate_seed();
assert!(seeds.insert(seed), "Duplicate seed generated");
}
}
#[test]
fn test_seed_generation_range() {
for _ in 0..100 {
let seed = generate_seed();
// Seeds should be positive and within reasonable range
assert!(seed > 0);
assert!(seed < u64::MAX);
}
}
}