//! HTTP-contract tests for the transcription service, using a fake transcriber so //! they run fast with no model. The real whisper path is covered in `whisper.rs`. use std::sync::Arc; use async_trait::async_trait; use serde_json::Value; use transcription_svc::{build_router, AppState, Transcriber}; struct FakeTranscriber { reply: String, } #[async_trait] impl Transcriber for FakeTranscriber { async fn transcribe(&self, audio: &[u8], _content_type: &str) -> anyhow::Result { assert!(!audio.is_empty(), "handler must not call transcriber on empty body"); Ok(self.reply.clone()) } } async fn spawn(reply: &str) -> (String, reqwest::Client) { let state = AppState { transcriber: Arc::new(FakeTranscriber { reply: reply.to_string(), }), }; let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { axum::serve(listener, build_router(state)).await.unwrap(); }); (format!("http://{addr}"), reqwest::Client::new()) } #[tokio::test] async fn transcribe_returns_text_json() { let (base, client) = spawn("nfc read resolved quickly").await; let res = client .post(format!("{base}/transcribe")) .header("content-type", "audio/webm") .body(b"-some-audio-".to_vec()) .send() .await .unwrap(); assert_eq!(res.status(), 200); let body: Value = res.json().await.unwrap(); assert_eq!(body["text"], "nfc read resolved quickly"); } #[tokio::test] async fn empty_body_is_400() { let (base, client) = spawn("unused").await; let res = client .post(format!("{base}/transcribe")) .header("content-type", "audio/webm") .body(Vec::::new()) .send() .await .unwrap(); assert_eq!(res.status(), 400); } #[tokio::test] async fn health_ok() { let (base, client) = spawn("unused").await; let res = client.get(format!("{base}/health")).send().await.unwrap(); assert_eq!(res.status(), 200); }