mod common;
use futures_util::StreamExt;
use std::time::Duration;
#[tokio::test]
#[ignore = "Requires model download"]
async fn test_health_returns_ok() {
let (port, shutdown) = common::start_server(&common::model_dir()).await;
let resp = tokio::time::timeout(Duration::from_secs(10), async {
reqwest::Client::new()
.get(format!("http://127.0.0.1:{port}/health"))
.send()
.await
.expect("GET /health failed")
})
.await
.expect("GET /health timed out");
assert_eq!(resp.status(), 200);
let text = resp.text().await.expect("Expected text body");
let body: serde_json::Value = serde_json::from_str(&text).expect("Expected JSON body");
assert_eq!(body["status"], "ok", "status field should be \"ok\"");
assert!(
body["model"]
.as_str()
.unwrap_or_default()
.contains("zipformer"),
"model field should contain \"zipformer\", got: {:?}",
body["model"]
);
assert!(
!body["version"].as_str().unwrap_or_default().is_empty(),
"version field should be a non-empty string"
);
let _ = shutdown.send(());
}
#[tokio::test]
#[ignore = "Requires model download (includes test_wavs/)"]
async fn test_transcribe_wav_returns_text() {
let (port, shutdown) = common::start_server(&common::model_dir()).await;
let wav_path = common::test_wav_path(0);
let wav = tokio::fs::read(&wav_path)
.await
.expect("Failed to read test WAV");
let resp = tokio::time::timeout(Duration::from_secs(30), async {
reqwest::Client::new()
.post(format!("http://127.0.0.1:{port}/v1/transcribe"))
.body(wav)
.send()
.await
.expect("POST /v1/transcribe failed")
})
.await
.expect("POST /v1/transcribe timed out");
assert_eq!(resp.status(), 200);
let text = resp.text().await.expect("Expected text body");
let body: serde_json::Value = serde_json::from_str(&text).expect("Expected JSON body");
assert!(
body["text"].is_string(),
"\"text\" field should be a string, got: {:?}",
body["text"]
);
assert!(
body["words"].is_array(),
"\"words\" field should be an array, got: {:?}",
body["words"]
);
let duration = body["duration"]
.as_f64()
.expect("\"duration\" should be a number");
assert!(duration > 0.0, "duration should be > 0, got {duration}");
let _ = shutdown.send(());
}
#[tokio::test]
#[ignore = "Requires model download"]
async fn test_transcribe_empty_body_returns_400() {
let (port, shutdown) = common::start_server(&common::model_dir()).await;
let resp = tokio::time::timeout(Duration::from_secs(10), async {
reqwest::Client::new()
.post(format!("http://127.0.0.1:{port}/v1/transcribe"))
.body(Vec::<u8>::new())
.send()
.await
.expect("POST /v1/transcribe failed")
})
.await
.expect("POST /v1/transcribe timed out");
assert_eq!(resp.status(), 400);
let text = resp.text().await.expect("Expected text body");
let body: serde_json::Value = serde_json::from_str(&text).expect("Expected JSON body");
assert_eq!(
body["code"], "empty_body",
"code field should be \"empty_body\", got: {:?}",
body["code"]
);
let _ = shutdown.send(());
}
#[tokio::test]
#[ignore = "Requires model download"]
async fn test_transcribe_invalid_audio_returns_422() {
let (port, shutdown) = common::start_server(&common::model_dir()).await;
let garbage: Vec<u8> = (0u8..=255).cycle().take(1000).collect();
let resp = tokio::time::timeout(Duration::from_secs(30), async {
reqwest::Client::new()
.post(format!("http://127.0.0.1:{port}/v1/transcribe"))
.body(garbage)
.send()
.await
.expect("POST /v1/transcribe failed")
})
.await
.expect("POST /v1/transcribe timed out");
assert_eq!(resp.status(), 422);
let text = resp.text().await.expect("Expected text body");
let body: serde_json::Value = serde_json::from_str(&text).expect("Expected JSON body");
let code = body["code"].as_str().unwrap_or_default();
assert!(
code == "invalid_audio" || code == "transcription_error",
"code should be \"invalid_audio\" or \"transcription_error\", got: {code:?}"
);
let _ = shutdown.send(());
}
#[tokio::test]
#[ignore = "Requires model download"]
async fn test_transcribe_stream_sse_incremental() {
let (port, shutdown) = common::start_server(&common::model_dir()).await;
let wav = common::generate_wav(10, 16000);
let resp = tokio::time::timeout(Duration::from_secs(60), async {
reqwest::Client::new()
.post(format!("http://127.0.0.1:{port}/v1/transcribe/stream"))
.body(wav)
.send()
.await
.expect("POST /v1/transcribe/stream failed")
})
.await
.expect("POST /v1/transcribe/stream timed out");
assert_eq!(resp.status(), 200);
let mut stream = resp.bytes_stream();
let mut all_bytes = Vec::new();
tokio::time::timeout(Duration::from_secs(60), async {
while let Some(chunk) = stream.next().await {
match chunk {
Ok(bytes) => all_bytes.extend_from_slice(&bytes),
Err(e) => {
eprintln!("SSE stream error: {e}");
break;
}
}
}
})
.await
.expect("SSE stream did not complete within 60s");
let raw = String::from_utf8_lossy(&all_bytes);
for line in raw.lines() {
if let Some(json_str) = line.strip_prefix("data:") {
let json_str = json_str.trim();
if json_str.is_empty() {
continue;
}
let v: serde_json::Value =
serde_json::from_str(json_str).expect("SSE data should be valid JSON");
assert!(
v["type"].is_string(),
"SSE event should have a \"type\" field, got: {:?}",
v
);
}
}
let _ = shutdown.send(());
}
#[tokio::test]
#[ignore = "Requires model download"]
async fn test_transcribe_stream_empty_body_returns_400() {
let (port, shutdown) = common::start_server(&common::model_dir()).await;
let resp = tokio::time::timeout(Duration::from_secs(10), async {
reqwest::Client::new()
.post(format!("http://127.0.0.1:{port}/v1/transcribe/stream"))
.body(Vec::<u8>::new())
.send()
.await
.expect("POST /v1/transcribe/stream failed")
})
.await
.expect("POST /v1/transcribe/stream timed out");
assert_eq!(resp.status(), 400);
let _ = shutdown.send(());
}
#[tokio::test]
#[ignore = "Requires model download"]
async fn test_sse_events_well_formed() {
let (port, shutdown) = common::start_server(&common::model_dir()).await;
let wav = common::generate_wav(5, 16000);
let resp = tokio::time::timeout(Duration::from_secs(60), async {
reqwest::Client::new()
.post(format!("http://127.0.0.1:{port}/v1/transcribe/stream"))
.body(wav)
.send()
.await
.expect("POST /v1/transcribe/stream failed")
})
.await
.expect("POST /v1/transcribe/stream timed out");
assert_eq!(resp.status(), 200);
let mut stream = resp.bytes_stream();
let mut all_bytes = Vec::new();
let collect_timeout = Duration::from_secs(30);
tokio::time::timeout(collect_timeout, async {
while let Some(chunk) = stream.next().await {
match chunk {
Ok(bytes) => all_bytes.extend_from_slice(&bytes),
Err(e) => {
eprintln!("SSE stream error: {e}");
break;
}
}
}
})
.await
.ok();
let raw = String::from_utf8_lossy(&all_bytes);
for line in raw.lines() {
if let Some(json_str) = line.strip_prefix("data:") {
let json_str = json_str.trim();
if json_str.is_empty() {
continue;
}
let v: serde_json::Value = serde_json::from_str(json_str)
.unwrap_or_else(|_| panic!("SSE data line is not valid JSON: {json_str:?}"));
let event_type = v["type"]
.as_str()
.unwrap_or_else(|| panic!("SSE event missing \"type\" field: {v:?}"));
assert!(
event_type == "partial" || event_type == "final",
"SSE event type should be \"partial\" or \"final\", got: {event_type:?}"
);
}
}
let _ = shutdown.send(());
}
#[tokio::test]
#[ignore = "Requires model download"]
async fn test_sse_midstream_disconnect() {
let (port, shutdown) = common::start_server(&common::model_dir()).await;
let wav = common::generate_wav(10, 16000);
let resp = tokio::time::timeout(Duration::from_secs(60), async {
reqwest::Client::new()
.post(format!("http://127.0.0.1:{port}/v1/transcribe/stream"))
.body(wav)
.send()
.await
.expect("POST /v1/transcribe/stream failed")
})
.await
.expect("POST /v1/transcribe/stream timed out");
assert_eq!(resp.status(), 200);
let mut stream = resp.bytes_stream();
let _first = tokio::time::timeout(Duration::from_secs(10), stream.next())
.await
.expect("Timed out waiting for first SSE event");
drop(stream);
tokio::time::sleep(Duration::from_millis(500)).await;
let health_resp = tokio::time::timeout(Duration::from_secs(10), async {
reqwest::Client::new()
.get(format!("http://127.0.0.1:{port}/health"))
.send()
.await
.expect("GET /health after disconnect failed")
})
.await
.expect("GET /health after disconnect timed out");
assert_eq!(
health_resp.status(),
200,
"Server should still be healthy after midstream disconnect"
);
let _ = shutdown.send(());
}