use futures_util::StreamExt;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, Request, ResponseTemplate};
use xai_grok::{ChatRequest, GrokClient, Message};
fn client(server: &MockServer) -> GrokClient {
GrokClient::builder()
.api_key("xai-test-key")
.base_url(server.uri())
.build()
.expect("client builds with key + base_url")
}
fn req() -> ChatRequest {
ChatRequest::new("grok-4", vec![Message::user("hi")])
}
#[tokio::test]
async fn chat_sends_bearer_auth_to_correct_path() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(header("authorization", "Bearer xai-test-key"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "cmpl-1",
"model": "grok-4",
"choices": [{
"index": 0,
"message": { "role": "assistant", "content": "hello there" },
"finish_reason": "stop"
}]
})))
.expect(1)
.mount(&server)
.await;
let resp = client(&server)
.chat(req())
.await
.expect("2xx chat response");
assert_eq!(resp.content(), Some("hello there"));
}
#[tokio::test]
async fn chat_forces_stream_false_in_body() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.respond_with(|request: &Request| {
let body: serde_json::Value = serde_json::from_slice(&request.body).unwrap();
assert_eq!(body["stream"], serde_json::json!(false));
ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "cmpl-2",
"model": "grok-4",
"choices": [{
"index": 0,
"message": { "role": "assistant", "content": "ok" }
}]
}))
})
.mount(&server)
.await;
let resp = client(&server).chat(req()).await.expect("ok");
assert_eq!(resp.content(), Some("ok"));
}
#[tokio::test]
async fn non_2xx_maps_to_api_error_with_status_and_body() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.respond_with(ResponseTemplate::new(429).set_body_string(r#"{"error":"rate limited"}"#))
.mount(&server)
.await;
let err = client(&server)
.chat(req())
.await
.expect_err("429 should surface as an error");
match err {
xai_grok::Error::Api { status, body } => {
assert_eq!(status, 429);
assert!(body.contains("rate limited"), "body preserved: {body}");
}
other => panic!("expected Error::Api, got {other:?}"),
}
}
#[tokio::test]
async fn chat_stream_parses_sse_over_http() {
let server = MockServer::start().await;
let sse = concat!(
"data: {\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"Hel\"}}]}\n\n",
"data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"lo\"}}]}\n\n",
"data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n",
"data: [DONE]\n\n",
);
Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(header("authorization", "Bearer xai-test-key"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "text/event-stream")
.set_body_string(sse),
)
.mount(&server)
.await;
let acc = client(&server)
.chat_stream_collect(req(), |_tok| {})
.await
.expect("stream collects cleanly");
let choice = acc.choice(0).expect("a choice streamed");
assert_eq!(choice.content, "Hello");
assert_eq!(choice.role.as_deref(), Some("assistant"));
assert_eq!(choice.finish_reason.as_deref(), Some("stop"));
}
#[tokio::test]
async fn chat_stream_up_front_status_error() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/chat/completions"))
.respond_with(ResponseTemplate::new(401).set_body_string("unauthorized"))
.mount(&server)
.await;
match client(&server).chat_stream(req()).await {
Err(xai_grok::Error::Api { status, .. }) => assert_eq!(status, 401),
Err(other) => panic!("expected Error::Api, got {other:?}"),
Ok(_) => panic!("a streaming request to a 401 must fail up front"),
}
}
#[tokio::test]
async fn done_sentinel_terminates_stream() {
let server = MockServer::start().await;
let sse = concat!(
"data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"hi\"}}]}\n\n",
"data: [DONE]\n\n",
);
Mock::given(method("POST"))
.and(path("/chat/completions"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "text/event-stream")
.set_body_string(sse),
)
.mount(&server)
.await;
let mut stream = client(&server).chat_stream(req()).await.expect("ok");
let mut chunks = 0;
while let Some(item) = stream.next().await {
item.expect("chunk decodes");
chunks += 1;
}
assert_eq!(chunks, 1);
}