use serde_json::{Value, json};
use std::time::Duration;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, Request, ResponseTemplate};
#[allow(dead_code)]
pub struct MockOpenAITestHelper {
mock_server: MockServer,
}
#[allow(dead_code)]
impl MockOpenAITestHelper {
pub async fn new() -> Self {
let mock_server = MockServer::start().await;
Self { mock_server }
}
pub fn server(&self) -> &MockServer {
&self.mock_server
}
pub fn base_url(&self) -> String {
self.mock_server.uri()
}
pub async fn mock_chat_completion_success(&self, response_content: &str) {
let response_body = json!({
"choices": [
{
"message": { "content": response_content },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 100, "completion_tokens": 50, "total_tokens": 150 },
"model": "gpt-4.1-mini"
});
Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(header("authorization", "Bearer mock-api-key"))
.and(header("content-type", "application/json"))
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
.mount(&self.mock_server)
.await;
}
pub async fn mock_chat_completion_with_dynamic_ids(&self, video_id: &str, subtitle_id: &str) {
use crate::test_support::responses::MatchResponseGenerator;
let response_content =
MatchResponseGenerator::successful_match_with_ids(video_id, subtitle_id);
let response_body = json!({
"choices": [
{ "message": { "content": response_content }, "finish_reason": "stop" }
],
"usage": { "prompt_tokens": 100, "completion_tokens": 50, "total_tokens": 150 },
"model": "gpt-4.1-mini"
});
Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(header("authorization", "Bearer mock-api-key"))
.and(header("content-type", "application/json"))
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
.mount(&self.mock_server)
.await;
}
pub async fn mock_chat_completion_with_expectation(
&self,
response_content: &str,
expected_calls: usize,
) {
let response_body = json!({
"choices": [
{ "message": { "content": response_content }, "finish_reason": "stop" }
],
"usage": { "prompt_tokens": 100, "completion_tokens": 50, "total_tokens": 150 },
"model": "gpt-4.1-mini"
});
Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(header("authorization", "Bearer mock-api-key"))
.and(header("content-type", "application/json"))
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
.expect(expected_calls as u64)
.mount(&self.mock_server)
.await;
}
pub async fn verify_expectations(&self) {
let _ = self.mock_server.received_requests().await;
}
pub async fn setup_error_response(&self, status: u16, error_message: &str) {
let response_body = json!({
"error": { "message": error_message }
});
Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(header("authorization", "Bearer mock-api-key"))
.respond_with(ResponseTemplate::new(status).set_body_json(response_body))
.mount(&self.mock_server)
.await;
}
pub async fn setup_delayed_response(&self, delay_ms: u64, response_content: &str) {
let response_body = json!({
"choices": [
{ "message": { "content": response_content }, "finish_reason": "stop" }
],
"usage": { "prompt_tokens": 100, "completion_tokens": 50, "total_tokens": 150 },
"model": "gpt-4.1-mini"
});
Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(header("authorization", "Bearer mock-api-key"))
.respond_with(
ResponseTemplate::new(200)
.set_delay(Duration::from_millis(delay_ms))
.set_body_json(response_body),
)
.mount(&self.mock_server)
.await;
}
pub async fn mock_chat_completion_echoing_request_ids(
&self,
video_count: usize,
subtitle_count: usize,
confidence: f64,
) {
self.mock_chat_completion_echoing_request_ids_inner(
video_count,
subtitle_count,
confidence,
false,
None,
)
.await;
}
pub async fn mock_chat_completion_echoing_request_ids_with_expectation(
&self,
video_count: usize,
subtitle_count: usize,
confidence: f64,
expected_calls: u64,
) {
self.mock_chat_completion_echoing_request_ids_inner(
video_count,
subtitle_count,
confidence,
false,
Some(expected_calls),
)
.await;
}
async fn mock_chat_completion_echoing_request_ids_inner(
&self,
video_count: usize,
subtitle_count: usize,
confidence: f64,
fan_out: bool,
expected_calls: Option<u64>,
) {
let builder = Mock::given(method("POST"))
.and(path("/chat/completions"))
.and(header("authorization", "Bearer mock-api-key"))
.and(header("content-type", "application/json"))
.respond_with(EchoIdsResponder {
video_count,
subtitle_count,
confidence,
fan_out,
});
let mounted = if let Some(n) = expected_calls {
builder.expect(n)
} else {
builder
};
mounted.mount(&self.mock_server).await;
}
pub async fn mock_chat_completion_echoing_one_to_many(
&self,
video_count: usize,
subtitle_count: usize,
confidence: f64,
) {
self.mock_chat_completion_echoing_request_ids_inner(
video_count,
subtitle_count,
confidence,
true,
None,
)
.await;
}
}
struct EchoIdsResponder {
video_count: usize,
subtitle_count: usize,
confidence: f64,
fan_out: bool,
}
impl wiremock::Respond for EchoIdsResponder {
fn respond(&self, request: &Request) -> ResponseTemplate {
let body: Value =
serde_json::from_slice(&request.body).expect("OpenAI request body must be valid JSON");
let prompt = body
.get("messages")
.and_then(|m| m.as_array())
.and_then(|arr| arr.last())
.and_then(|m| m.get("content"))
.and_then(|c| c.as_str())
.unwrap_or("");
let id_pattern = regex::Regex::new(
r"file_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}",
)
.expect("static regex compiles");
let ids: Vec<String> = id_pattern
.find_iter(prompt)
.map(|m| m.as_str().to_string())
.collect();
let videos: Vec<&str> = ids
.iter()
.take(self.video_count)
.map(String::as_str)
.collect();
let subtitles: Vec<&str> = ids
.iter()
.skip(self.video_count)
.take(self.subtitle_count)
.map(String::as_str)
.collect();
let pairs = videos.len().min(subtitles.len());
let matches: Vec<Value> = if self.fan_out {
let video = videos.first().copied().unwrap_or("");
subtitles
.iter()
.map(|sub| {
json!({
"video_file_id": video,
"subtitle_file_id": sub,
"confidence": self.confidence,
"match_factors": ["echoed_from_request"],
})
})
.collect()
} else {
(0..pairs)
.map(|i| {
json!({
"video_file_id": videos[i],
"subtitle_file_id": subtitles[i],
"confidence": self.confidence,
"match_factors": ["echoed_from_request"],
})
})
.collect()
};
let content = json!({
"matches": matches,
"confidence": self.confidence,
"reasoning": "Echoing IDs captured from the live request",
})
.to_string();
let response_body = json!({
"choices": [
{ "message": { "content": content }, "finish_reason": "stop" }
],
"usage": { "prompt_tokens": 100, "completion_tokens": 50, "total_tokens": 150 },
"model": "gpt-4.1-mini"
});
ResponseTemplate::new(200).set_body_json(response_body)
}
}