Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/tinyinference-voice/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ tinyinference-local = { version = "0.3.0", path = "../tinyinference-local" }
tokio = { workspace = true, features = ["fs", "io-util"] }
uuid = { workspace = true }

[dev-dependencies]
axum = { workspace = true }

[lints]
workspace = true
109 changes: 109 additions & 0 deletions crates/tinyinference-voice/src/cloud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,21 @@ fn nonempty(value: Option<&str>) -> Option<&str> {
#[cfg(test)]
mod tests {
use super::*;
use axum::{
Router,
http::{HeaderMap, StatusCode},
routing::post,
};
use std::sync::{Arc, Mutex};

async fn test_server(app: Router) -> reqwest::Url {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
format!("http://{address}/v1/audio/transcriptions")
.parse()
.unwrap()
}

#[test]
fn response_sanitization_removes_exact_and_prefixed_secrets() {
Expand All @@ -140,4 +155,98 @@ mod tests {
assert!(!detail.contains("sk-provider-secret"));
assert!(detail.contains("[REDACTED]"));
}

#[test]
fn rejects_invalid_or_empty_audio_before_making_a_request() {
let options = CloudTranscribeOptions::default();
let client = reqwest::Client::new();
let url: reqwest::Url = "http://127.0.0.1:1/transcribe".parse().unwrap();

let empty = tokio::runtime::Runtime::new().unwrap().block_on(transcribe(
&client,
url.clone(),
"token",
" ",
&options,
));
assert_eq!(empty.unwrap_err(), "audio_base64 is required");

let invalid = tokio::runtime::Runtime::new().unwrap().block_on(transcribe(
&client,
url,
"token",
"not base64!",
&options,
));
assert!(invalid.unwrap_err().starts_with("invalid base64 audio:"));
}

#[tokio::test]
async fn uploads_multipart_audio_with_defaults_and_returns_trimmed_text() {
let authorization = Arc::new(Mutex::new(None));
let captured = Arc::clone(&authorization);
let app = Router::new().route(
"/v1/audio/transcriptions",
post(move |headers: HeaderMap| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Assert the multipart body in the upload test

The handler extracts only the authorization header and never reads or validates the request body. If transcribe regresses to sending an empty body, JSON, incorrect audio bytes, or omits the default model, filename, or MIME type, this test will still return 200 and pass despite its stated multipart/defaults coverage. Capture the content type and multipart body and assert the expected fields and decoded bytes.

Useful? React with 👍 / 👎.

let captured = Arc::clone(&captured);
async move {
*captured.lock().unwrap() = headers
.get(AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.map(ToOwned::to_owned);
(StatusCode::OK, r#"{"text":" hello world "}"#)
}
}),
);
let url = test_server(app).await;

let result = transcribe(
&reqwest::Client::new(),
url,
"secret-token",
"AQID",
&CloudTranscribeOptions::default(),
)
.await
.unwrap();

assert_eq!(
result,
CloudTranscribeResult {
text: "hello world".to_string()
}
);
assert_eq!(
authorization.lock().unwrap().as_deref(),
Some("Bearer secret-token")
);
}

#[tokio::test]
async fn failure_responses_redact_bearer_tokens() {
let app = Router::new().route(
"/v1/audio/transcriptions",
post(|| async { (StatusCode::BAD_GATEWAY, "token=secret-token sk-hidden") }),
);
let error = transcribe(
&reqwest::Client::new(),
test_server(app).await,
"secret-token",
"AQID",
&CloudTranscribeOptions::default(),
)
.await
.unwrap_err();

assert!(error.contains("502 Bad Gateway"));
assert!(!error.contains("secret-token"));
assert!(!error.contains("sk-hidden"));
}

#[test]
fn nonempty_trims_and_discards_blank_values() {
assert_eq!(nonempty(Some(" model ")), Some("model"));
assert_eq!(nonempty(Some("\t\n")), None);
assert_eq!(nonempty(None), None);
}
}
Loading