use assert_cmd::Command;
use predicates::prelude::*;
use serde_json::{json, Value};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn gateway_base(server: &MockServer) -> String {
format!("{}/v1/api", server.uri())
}
#[tokio::test]
async fn help_exits_zero_and_mentions_key_subcommands() {
let mut cmd = Command::cargo_bin("bezant").expect("binary");
cmd.arg("--help").assert().success().stdout(
predicate::str::contains("health")
.and(predicate::str::contains("accounts"))
.and(predicate::str::contains("positions"))
.and(predicate::str::contains("conid")),
);
}
#[tokio::test]
async fn health_outputs_auth_status_json() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/api/iserver/auth/status"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"authenticated": true,
"connected": true,
"competing": false,
"message": ""
})))
.mount(&server)
.await;
let mut cmd = Command::cargo_bin("bezant").expect("binary");
let assertion = cmd
.args(["--gateway-url", &gateway_base(&server), "health"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string();
let body: Value = serde_json::from_str(stdout.trim()).expect("json");
assert_eq!(body["authenticated"], json!(true));
assert_eq!(body["connected"], json!(true));
}
#[tokio::test]
async fn accounts_prints_array_from_gateway() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/api/portfolio/accounts"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([
{"accountId": "DU1", "currency": "USD"}
])))
.mount(&server)
.await;
let mut cmd = Command::cargo_bin("bezant").expect("binary");
let assertion = cmd
.args(["--gateway-url", &gateway_base(&server), "accounts"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string();
let body: Value = serde_json::from_str(stdout.trim()).expect("json");
assert_eq!(body[0]["accountId"], json!("DU1"));
}
#[tokio::test]
async fn pretty_flag_adds_newlines() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/api/iserver/auth/status"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"authenticated": true, "connected": true
})))
.mount(&server)
.await;
let mut cmd = Command::cargo_bin("bezant").expect("binary");
let out = cmd
.args([
"--gateway-url",
&gateway_base(&server),
"--pretty",
"health",
])
.output()
.expect("output");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains('\n') && stdout.contains(" "),
"pretty output should have newlines + indentation: {stdout}"
);
}
#[tokio::test]
async fn summary_prints_gateway_response() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/api/portfolio/DU123/summary"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"netliquidation": {"amount": 1234.5, "currency": "USD"}
})))
.mount(&server)
.await;
let mut cmd = Command::cargo_bin("bezant").expect("binary");
let assertion = cmd
.args(["--gateway-url", &gateway_base(&server), "summary", "DU123"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string();
let body: Value = serde_json::from_str(stdout.trim()).expect("json");
assert_eq!(body["netliquidation"]["amount"], json!(1234.5));
}
#[tokio::test]
async fn positions_paginates_and_flattens() {
let server = MockServer::start().await;
let page0: Vec<Value> = (0..30).map(|i| json!({"conid": 1000 + i})).collect();
Mock::given(method("GET"))
.and(path("/v1/api/portfolio/DU123/positions/0"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!(page0)))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/v1/api/portfolio/DU123/positions/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([{"conid": 9999}])))
.mount(&server)
.await;
let mut cmd = Command::cargo_bin("bezant").expect("binary");
let assertion = cmd
.args([
"--gateway-url",
&gateway_base(&server),
"positions",
"DU123",
])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string();
let body: Value = serde_json::from_str(stdout.trim()).expect("json");
let arr = body.as_array().expect("array");
assert_eq!(arr.len(), 31);
assert_eq!(arr[30]["conid"], json!(9999));
}
#[tokio::test]
async fn conid_resolves_symbol() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/api/iserver/secdef/search"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([
{"conid": "265598", "companyName": "Apple Inc"}
])))
.mount(&server)
.await;
let mut cmd = Command::cargo_bin("bezant").expect("binary");
let assertion = cmd
.args(["--gateway-url", &gateway_base(&server), "conid", "AAPL"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string();
let body: Value = serde_json::from_str(stdout.trim()).expect("json");
assert_eq!(body["symbol"], json!("AAPL"));
assert_eq!(body["conid"], json!(265_598));
}
#[tokio::test]
async fn tickle_reports_session_id() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/api/tickle"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"session": "sess-xyz"
})))
.mount(&server)
.await;
let mut cmd = Command::cargo_bin("bezant").expect("binary");
let assertion = cmd
.args(["--gateway-url", &gateway_base(&server), "tickle"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string();
let body: Value = serde_json::from_str(stdout.trim()).expect("json");
assert_eq!(body["session"], json!("sess-xyz"));
}
#[tokio::test]
async fn health_reports_error_on_401() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/api/iserver/auth/status"))
.respond_with(ResponseTemplate::new(401))
.mount(&server)
.await;
let mut cmd = Command::cargo_bin("bezant").expect("binary");
cmd.args(["--gateway-url", &gateway_base(&server), "health"])
.assert()
.failure()
.stderr(predicate::str::contains("not authenticated"));
}
#[tokio::test]
async fn quote_resolves_symbol_then_fetches_snapshot() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/api/iserver/secdef/search"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([
{"symbol": "AAPL", "conid": "265598"}
])))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/v1/api/iserver/marketdata/snapshot"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([
{"31": "193.42", "84": "193.40", "86": "193.45", "87": "12345678"}
])))
.mount(&server)
.await;
let mut cmd = Command::cargo_bin("bezant").expect("binary");
let assertion = cmd
.args(["--gateway-url", &gateway_base(&server), "quote", "AAPL"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string();
let body: Value = serde_json::from_str(stdout.trim()).expect("json");
assert_eq!(body["symbol"], json!("AAPL"));
assert_eq!(body["conid"], json!(265_598));
assert_eq!(body["last"], json!("193.42"));
assert_eq!(body["bid"], json!("193.40"));
}
#[tokio::test]
async fn orders_returns_array_from_iserver_endpoint() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/api/iserver/account/orders"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"orders": [
{"orderId": "abc", "ticker": "AAPL", "side": "BUY", "totalSize": 10}
]
})))
.mount(&server)
.await;
let mut cmd = Command::cargo_bin("bezant").expect("binary");
let assertion = cmd
.args(["--gateway-url", &gateway_base(&server), "orders", "DU123"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string();
let body: Value = serde_json::from_str(stdout.trim()).expect("json");
let arr = body.as_array().expect("array");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["orderId"], json!("abc"));
}
#[tokio::test]
async fn output_table_renders_human_readable_rows_for_accounts() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/api/portfolio/accounts"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([
{
"accountId": "DU1",
"accountTitle": "Test Account",
"currency": "USD",
"type": "INDIVIDUAL"
}
])))
.mount(&server)
.await;
let mut cmd = Command::cargo_bin("bezant").expect("binary");
let assertion = cmd
.args([
"--gateway-url",
&gateway_base(&server),
"--output",
"table",
"accounts",
])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string();
assert!(stdout.contains("accountId"), "stdout: {stdout}");
assert!(stdout.contains("accountTitle"), "stdout: {stdout}");
assert!(stdout.contains("DU1"), "stdout: {stdout}");
assert!(stdout.contains("Test Account"), "stdout: {stdout}");
}
#[tokio::test]
async fn output_table_falls_back_to_json_for_non_tabular_endpoints() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/api/tickle"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"session": "abc"})))
.mount(&server)
.await;
let mut cmd = Command::cargo_bin("bezant").expect("binary");
let assertion = cmd
.args([
"--gateway-url",
&gateway_base(&server),
"--output",
"table",
"tickle",
])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string();
let body: Value = serde_json::from_str(stdout.trim()).expect("json");
assert_eq!(body["session"], json!("abc"));
assert!(stdout.contains('\n'), "expected pretty JSON: {stdout}");
}