use super::*;
use std::time::Duration;
use proptest::prelude::*;
use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::method;
use rsa::RsaPrivateKey;
use rsa::pkcs8::EncodePrivateKey;
use rsa::pkcs8::LineEnding;
use std::sync::OnceLock;
fn cached_test_private_key() -> &'static str {
static KEY: OnceLock<String> = OnceLock::new();
KEY.get_or_init(|| {
let mut rng = rand::thread_rng();
let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("生成密钥失败");
private_key
.to_pkcs8_pem(LineEnding::LF)
.expect("编码 PKCS#8 PEM 失败")
.to_string()
})
}
fn test_config(server_url: &str) -> ClientConfig {
ClientConfig {
tiger_id: "test_tiger_id".to_string(),
private_key: cached_test_private_key().to_string(),
account: "DU123456".to_string(),
license: None,
language: crate::model::enums::Language::ZhCn,
timezone: None,
timeout: Duration::from_secs(5),
token: None,
token_refresh_duration: None,
server_url: server_url.to_string(),
quote_server_url: server_url.to_string(),
tiger_public_key: "".to_string(),
device_id: "".to_string(),
}
}
#[test]
fn test_user_agent() {
assert_eq!(HttpClient::user_agent(), "openapi-rust-sdk-0.3.0");
}
#[test]
fn test_execute_rejects_empty_api_method() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let config = test_config("http://localhost:1234");
let client = HttpClient::new(config);
let result = client.execute("", r#"{"market":"US"}"#).await;
assert!(result.is_err());
match result.unwrap_err() {
TigerError::Config(msg) => assert!(msg.contains("api_method")),
other => panic!("应返回 Config 错误,实际: {:?}", other),
}
});
}
#[test]
fn test_execute_rejects_invalid_json() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let config = test_config("http://localhost:1234");
let client = HttpClient::new(config);
let result = client.execute("market_state", "not json").await;
assert!(result.is_err());
match result.unwrap_err() {
TigerError::Config(msg) => assert!(msg.contains("JSON")),
other => panic!("应返回 Config 错误,实际: {:?}", other),
}
});
}
#[tokio::test]
async fn test_execute_sends_request_and_returns_raw_response() {
let mock_server = MockServer::start().await;
let response_body = r#"{"code":0,"message":"success","data":{"market":"US"}}"#;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(response_body))
.mount(&mock_server)
.await;
let config = test_config(&mock_server.uri());
let client = HttpClient::new(config);
let result = client.execute("market_state", r#"{"market":"US"}"#).await;
assert!(result.is_ok());
let body = result.unwrap();
assert_eq!(body, response_body);
}
#[tokio::test]
async fn test_execute_request_parses_success_response() {
let mock_server = MockServer::start().await;
let response_body = r#"{"code":0,"message":"success","data":{"market":"US"},"timestamp":1234567890}"#;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(response_body))
.mount(&mock_server)
.await;
let config = test_config(&mock_server.uri());
let client = HttpClient::new(config);
let req = ApiRequest::new("market_state", r#"{"market":"US"}"#);
let result = client.execute_request(&req).await;
assert!(result.is_ok());
let resp = result.unwrap();
assert_eq!(resp.code, 0);
assert_eq!(resp.message, "success");
}
#[tokio::test]
async fn test_execute_request_returns_api_error() {
let mock_server = MockServer::start().await;
let response_body = r#"{"code":1010,"message":"参数错误","data":null,"timestamp":1234567890}"#;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(response_body))
.mount(&mock_server)
.await;
let config = test_config(&mock_server.uri());
let client = HttpClient::new(config);
let req = ApiRequest::new("market_state", r#"{"market":"US"}"#);
let result = client.execute_request(&req).await;
assert!(result.is_err());
match result.unwrap_err() {
TigerError::Api { code, message } => {
assert_eq!(code, 1010);
assert_eq!(message, "参数错误");
}
other => panic!("应返回 Api 错误,实际: {:?}", other),
}
}
fn valid_api_method() -> impl Strategy<Value = String> {
prop_oneof![
Just("market_state".to_string()),
Just("quote_real_time".to_string()),
Just("kline".to_string()),
Just("place_order".to_string()),
Just("get_position".to_string()),
Just("get_orders".to_string()),
]
}
fn valid_biz_content_json() -> impl Strategy<Value = String> {
prop_oneof![
Just(r#"{"market":"US"}"#.to_string()),
Just(r#"{"symbol":"AAPL"}"#.to_string()),
Just(r#"{"market":"HK","symbol":"00700"}"#.to_string()),
Just(r#"{}"#.to_string()),
Just(r#"{"limit":100}"#.to_string()),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(30))]
#[test]
fn generic_execute_request_construction(
api_method in valid_api_method(),
biz_content in valid_biz_content_json(),
) {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let mock_server = MockServer::start().await;
let response_body = r#"{"code":0,"message":"success","data":null}"#;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(response_body))
.expect(1)
.mount(&mock_server)
.await;
let config = test_config(&mock_server.uri());
let client = HttpClient::new(config);
let result = client.execute(&api_method, &biz_content).await;
prop_assert!(result.is_ok(), "请求应成功");
let received = mock_server.received_requests().await.unwrap();
prop_assert_eq!(received.len(), 1, "应发送恰好一个请求");
let req_body: serde_json::Value = serde_json::from_slice(&received[0].body).unwrap();
prop_assert_eq!(req_body["method"].as_str().unwrap(), api_method.as_str(),
"method 字段应等于传入的 api_method");
prop_assert_eq!(req_body["biz_content"].as_str().unwrap(), biz_content.as_str(),
"biz_content 字段应等于传入的 request_json");
prop_assert_eq!(req_body["tiger_id"].as_str().unwrap(), "test_tiger_id",
"tiger_id 应正确");
prop_assert_eq!(req_body["charset"].as_str().unwrap(), "UTF-8",
"charset 应为 UTF-8");
prop_assert_eq!(req_body["sign_type"].as_str().unwrap(), "RSA",
"sign_type 应为 RSA");
prop_assert_eq!(req_body["version"].as_str().unwrap(), "2.0",
"version 应为 2.0");
prop_assert!(req_body["sign"].as_str().is_some(),
"应包含 sign 字段");
prop_assert!(req_body["timestamp"].as_str().is_some(),
"应包含 timestamp 字段");
Ok(())
})?;
}
}
fn valid_response_json() -> impl Strategy<Value = String> {
prop_oneof![
Just(r#"{"code":0,"message":"success","data":{"market":"US"}}"#.to_string()),
Just(r#"{"code":0,"message":"ok","data":[1,2,3]}"#.to_string()),
Just(r#"{"code":0,"message":"","data":null}"#.to_string()),
Just(r#"{"code":1010,"message":"error","data":null}"#.to_string()),
Just(r#"{"code":0,"message":"success","data":{"items":[{"id":1},{"id":2}]}}"#.to_string()),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(30))]
#[test]
fn generic_execute_response_passthrough(
response_json in valid_response_json(),
) {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(&response_json))
.mount(&mock_server)
.await;
let config = test_config(&mock_server.uri());
let client = HttpClient::new(config);
let result = client.execute("market_state", r#"{"market":"US"}"#).await;
prop_assert!(result.is_ok(), "请求应成功");
let body = result.unwrap();
prop_assert_eq!(&body, &response_json,
"返回的响应应与服务器原始响应完全一致");
Ok(())
})?;
}
}
fn invalid_json_string() -> impl Strategy<Value = String> {
prop_oneof![
Just("not json".to_string()),
Just("{invalid}".to_string()),
Just("".to_string()),
Just("{key: value}".to_string()),
Just("[1,2,".to_string()),
Just(r#"{"unclosed": "string"#.to_string()),
Just("undefined".to_string()),
Just("NaN".to_string()),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn generic_execute_rejects_invalid_json(
invalid_json in invalid_json_string(),
) {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let config = test_config("http://localhost:1234");
let client = HttpClient::new(config);
let result = client.execute("market_state", &invalid_json).await;
prop_assert!(result.is_err(), "无效 JSON 应返回错误");
match result.unwrap_err() {
TigerError::Config(msg) => {
prop_assert!(msg.contains("JSON"), "错误消息应提及 JSON: {}", msg);
}
other => {
prop_assert!(false, "应返回 Config 错误,实际: {:?}", other);
}
}
Ok(())
})?;
}
}
#[tokio::test]
async fn test_authorization_header_with_token() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r#"{"code":0,"message":"success","data":null}"#,
))
.mount(&mock_server)
.await;
let mut config = test_config(&mock_server.uri());
config.token = Some("my_test_token_value".to_string());
let client = HttpClient::new(config);
let result = client.execute("market_state", r#"{"market":"US"}"#).await;
assert!(result.is_ok());
let received = mock_server.received_requests().await.unwrap();
assert_eq!(received.len(), 1);
let auth_header: wiremock::http::HeaderName = "Authorization".into();
let auth = received[0]
.headers
.get(&auth_header)
.expect("应包含 Authorization 头");
assert_eq!(auth.as_str(), "my_test_token_value");
}
#[tokio::test]
async fn test_no_authorization_header_without_token() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r#"{"code":0,"message":"success","data":null}"#,
))
.mount(&mock_server)
.await;
let config = test_config(&mock_server.uri());
let client = HttpClient::new(config);
let result = client.execute("market_state", r#"{"market":"US"}"#).await;
assert!(result.is_ok());
let received = mock_server.received_requests().await.unwrap();
assert_eq!(received.len(), 1);
let auth_header: wiremock::http::HeaderName = "Authorization".into();
assert!(
received[0].headers.get(&auth_header).is_none(),
"未设置 Token 时不应携带 Authorization 头"
);
}