use llmposter::{Fixture, ServerBuilder};
async fn post_openai(
url: &str,
body: serde_json::Value,
extra_header: Option<(&str, &str)>,
) -> u16 {
let client = reqwest::Client::new();
let mut req = client
.post(format!("{}/v1/chat/completions", url))
.json(&body);
if let Some((name, value)) = extra_header {
req = req.header(name, value);
}
req.send().await.unwrap().status().as_u16()
}
#[tokio::test]
async fn should_match_on_header_value() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_header("x-tenant", "acme")
.respond_with_content("acme-tenant"),
)
.build()
.await
.unwrap();
let body = serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}]
});
assert_eq!(post_openai(&server.url(), body.clone(), None).await, 404);
assert_eq!(
post_openai(&server.url(), body.clone(), Some(("x-tenant", "globex"))).await,
404
);
assert_eq!(
post_openai(&server.url(), body, Some(("x-tenant", "acme"))).await,
200
);
}
#[tokio::test]
async fn should_match_header_with_multiple_values() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_header("accept", "application/json")
.respond_with_content("multi-accept"),
)
.build()
.await
.unwrap();
let resp = reqwest::Client::new()
.post(format!("{}/v1/chat/completions", server.url()))
.header("accept", "text/html")
.header("accept", "application/json")
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn should_match_on_system_prompt_openai() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_system_prompt("You are a pirate")
.respond_with_content("yarr"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "hello"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
let resp: serde_json::Value = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [
{"role": "system", "content": "You are a pirate. Speak like one."},
{"role": "user", "content": "hello"}
]
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(resp["choices"][0]["message"]["content"], "yarr");
}
#[tokio::test]
async fn should_match_on_anthropic_top_level_system_string() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_system_prompt("helpful assistant")
.respond_with_content("OK"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/messages", server.url()))
.json(&serde_json::json!({
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"system": "You are a helpful assistant that answers questions.",
"messages": [{"role": "user", "content": "hello"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn should_match_on_exact_temperature() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_temperature(0.7)
.respond_with_content("warm"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"temperature": 0.2,
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"temperature": 0.7,
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn should_match_on_temperature_range() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_temperature_range(Some(0.5), Some(1.0))
.respond_with_content("in range"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"temperature": 0.2,
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"temperature": 0.5,
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"temperature": 1.5,
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
}
#[tokio::test]
async fn should_match_on_metadata_entry() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_metadata("customer_id", "cust-42")
.respond_with_content("known customer"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"metadata": {"customer_id": "cust-99"},
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"metadata": {"customer_id": "cust-42"},
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn should_match_on_tool_schema_openai_and_anthropic() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_tool_schema("get_weather")
.respond_with_content("weather tool detected"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}],
"tools": [
{"type": "function", "function": {"name": "get_weather", "parameters": {}}}
]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let resp = client
.post(format!("{}/v1/messages", server.url()))
.json(&serde_json::json!({
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "hi"}],
"tools": [{"name": "get_weather", "input_schema": {"type": "object"}}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn should_match_on_gemini_tool_schema() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_tool_schema("get_weather")
.respond_with_content("weather"),
)
.build()
.await
.unwrap();
let resp = reqwest::Client::new()
.post(format!(
"{}/v1beta/models/gemini-pro:generateContent",
server.url()
))
.json(&serde_json::json!({
"contents": [{"role": "user", "parts": [{"text": "hi"}]}],
"tools": [{
"functionDeclarations": [
{"name": "get_weather", "description": "..."}
]
}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn should_reject_fixture_with_blank_header_name() {
let result = ServerBuilder::new()
.fixture(
Fixture::new()
.match_header(" ", "value")
.respond_with_content("ok"),
)
.build()
.await;
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(
err.contains("header name must not be blank"),
"unexpected: {err}"
);
}
#[tokio::test]
async fn should_prefer_high_priority_fixture_regardless_of_file_order() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_user_message("hello")
.respond_with_content("low-priority match"),
)
.fixture(
Fixture::new()
.match_user_message("hello world")
.with_priority(100)
.respond_with_content("high-priority specific"),
)
.build()
.await
.unwrap();
let body: serde_json::Value = reqwest::Client::new()
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "hello world"}]
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(
body["choices"][0]["message"]["content"],
"high-priority specific"
);
}
#[tokio::test]
async fn should_use_catch_all_only_when_no_other_fixture_matches() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.as_catch_all()
.respond_with_content("catch-all fallback"),
)
.fixture(
Fixture::new()
.match_user_message("specific")
.respond_with_content("specific match"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let body: serde_json::Value = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "specific"}]
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(body["choices"][0]["message"]["content"], "specific match");
let body: serde_json::Value = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "anything else"}]
}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(
body["choices"][0]["message"]["content"],
"catch-all fallback"
);
}
#[tokio::test]
async fn should_reject_fixture_with_inverted_temperature_range() {
let result = ServerBuilder::new()
.fixture(
Fixture::new()
.match_temperature_range(Some(1.0), Some(0.5))
.respond_with_content("ok"),
)
.build()
.await;
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(err.contains("range inverted"), "unexpected: {err}");
}
#[cfg(feature = "jsonpath")]
#[tokio::test]
async fn should_match_on_body_jsonpath_present() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_body_jsonpath("$.messages[?(@.role == 'system')]")
.respond_with_content("system-present"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [
{"role": "system", "content": "be brief"},
{"role": "user", "content": "hi"}
]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["choices"][0]["message"]["content"], "system-present");
}
#[cfg(feature = "jsonpath")]
#[tokio::test]
async fn should_match_on_body_jsonpath_deep_field() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_body_jsonpath("$.tools[?(@.function.name == 'get_weather')]")
.respond_with_content("weather-tool-seen"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}],
"tools": [{
"type": "function",
"function": {"name": "get_stock_price", "parameters": {}}
}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}],
"tools": [{
"type": "function",
"function": {"name": "get_weather", "parameters": {}}
}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[cfg(feature = "jsonpath")]
#[tokio::test]
async fn should_reject_fixture_with_invalid_jsonpath() {
let result = ServerBuilder::new()
.fixture(
Fixture::new()
.match_body_jsonpath("$[not-valid")
.respond_with_content("ok"),
)
.build()
.await;
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(
err.contains("body_jsonpath is invalid"),
"unexpected: {err}"
);
}
#[cfg(feature = "jsonpath")]
#[tokio::test]
async fn should_reject_fixture_with_blank_jsonpath() {
let result = ServerBuilder::new()
.fixture(
Fixture::new()
.match_body_jsonpath(" ")
.respond_with_content("ok"),
)
.build()
.await;
assert!(result.is_err());
let err = format!("{}", result.unwrap_err());
assert!(
err.contains("body_jsonpath must not be empty"),
"unexpected: {err}"
);
}
#[tokio::test]
async fn should_reject_fixture_with_refusal_and_streaming() {
let result = ServerBuilder::new()
.fixture(Fixture {
refusal: Some(llmposter::Refusal {
reason: "unsafe".to_string(),
}),
streaming: Some(llmposter::StreamingConfig {
latency: Some(100),
chunk_size: None,
}),
..Fixture::new()
})
.build()
.await;
let err = format!("{}", result.unwrap_err());
assert!(err.contains("mutually exclusive"), "unexpected: {err}");
}
#[tokio::test]
async fn should_reject_fixture_with_invalid_header_name_characters() {
let tmp = std::env::temp_dir().join(format!(
"llmposter-bad-hdr-chars-{}.yaml",
std::process::id()
));
std::fs::write(
&tmp,
r#"fixtures:
- match:
headers:
"x bad header": acme
response:
content: ok
"#,
)
.unwrap();
let result = ServerBuilder::new().load_yaml(&tmp);
let _ = std::fs::remove_file(&tmp);
let err = format!("{:?}", result.err().expect("expected load error"));
assert!(
err.contains("not a valid HTTP header name"),
"unexpected: {err}"
);
}
#[test]
fn match_fixture_warns_on_unsupported_fields() {
use llmposter::fixture::match_fixture;
let fixtures = vec![Fixture::new()
.match_header("x-tenant", "acme")
.respond_with_content("header-fixture")];
let result = match_fixture(&fixtures, "", None, None, None);
assert!(result.is_none());
}
#[tokio::test]
async fn should_match_responses_system_prompt_via_instructions_field() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_system_prompt("Be concise")
.respond_with_content("concise-matched"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/responses", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"input": [{"role": "user", "content": "hi"}],
"instructions": "Be concise and helpful"
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn should_match_gemini_temperature_via_generation_config() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_temperature(0.7)
.respond_with_content("gemini-temp-matched"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!(
"{}/v1beta/models/gemini-pro:generateContent",
server.url()
))
.json(&serde_json::json!({
"contents": [{"role": "user", "parts": [{"text": "hi"}]}],
"generationConfig": {
"temperature": 0.7
}
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn should_reject_fixture_with_nan_exact_temperature() {
let result = ServerBuilder::new()
.fixture(
Fixture::new()
.match_temperature(f64::NAN)
.respond_with_content("ok"),
)
.build()
.await;
let err = format!("{}", result.unwrap_err());
assert!(err.contains("must be a finite number"), "unexpected: {err}");
}
#[tokio::test]
async fn should_reject_fixture_with_nonfinite_temperature_range_bounds() {
let result = ServerBuilder::new()
.fixture(
Fixture::new()
.match_temperature_range(Some(f64::NEG_INFINITY), Some(1.0))
.respond_with_content("ok"),
)
.build()
.await;
let err = format!("{}", result.unwrap_err());
assert!(err.contains("match.temperature.min must be finite"));
let result = ServerBuilder::new()
.fixture(
Fixture::new()
.match_temperature_range(Some(0.0), Some(f64::INFINITY))
.respond_with_content("ok"),
)
.build()
.await;
let err = format!("{}", result.unwrap_err());
assert!(err.contains("match.temperature.max must be finite"));
}
#[tokio::test]
async fn should_reject_fixture_with_empty_temperature_range() {
let result = ServerBuilder::new()
.fixture(
Fixture::new()
.match_temperature_range(None, None)
.respond_with_content("ok"),
)
.build()
.await;
let err = format!("{}", result.unwrap_err());
assert!(err.contains("at least one of min/max"), "unexpected: {err}");
}
#[tokio::test]
async fn should_reject_fixture_with_blank_metadata_key() {
let result = ServerBuilder::new()
.fixture(
Fixture::new()
.match_metadata(" ", "value")
.respond_with_content("ok"),
)
.build()
.await;
let err = format!("{}", result.unwrap_err());
assert!(err.contains("match.metadata: key must not be blank"));
}
#[tokio::test]
async fn should_reject_fixture_with_case_folded_duplicate_headers() {
let tmp =
std::env::temp_dir().join(format!("llmposter-dup-headers-{}.yaml", std::process::id()));
std::fs::write(
&tmp,
r#"fixtures:
- match:
headers:
X-Tenant: acme
x-tenant: globex
response:
content: ok
"#,
)
.unwrap();
let result = ServerBuilder::new().load_yaml(&tmp);
let _ = std::fs::remove_file(&tmp);
let err = format!("{:?}", result.err().expect("expected load error"));
assert!(
err.contains("duplicate header name after case-folding"),
"unexpected: {err}"
);
assert!(err.contains("X-Tenant"), "missing first key in: {err}");
assert!(err.contains("x-tenant"), "missing second key in: {err}");
}
#[tokio::test]
async fn should_match_anthropic_system_content_block_array() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_system_prompt("pirate")
.respond_with_content("yarr"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/messages", server.url()))
.json(&serde_json::json!({
"model": "claude-sonnet-4-6",
"max_tokens": 10,
"system": [
{"type": "text", "text": "You are a pirate"},
{"type": "text", "text": "Answer only in shanties"}
],
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn should_match_gemini_system_instruction_parts() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_system_prompt("pirate")
.respond_with_content("arr-matched"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!(
"{}/v1beta/models/gemini-pro:generateContent",
server.url()
))
.json(&serde_json::json!({
"contents": [{"role": "user", "parts": [{"text": "hi"}]}],
"systemInstruction": {
"parts": [{"text": "You are a pirate"}, {"text": "talk like one"}]
}
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn should_match_responses_system_via_input_array_fallback() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_system_prompt("Be concise")
.respond_with_content("concise-input-matched"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/responses", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"input": [
{"role": "system", "content": "Be concise please"},
{"role": "user", "content": "hi"}
]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn should_match_openai_system_with_content_parts_array() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_system_prompt("pirate")
.respond_with_content("yarr-parts"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [
{
"role": "system",
"content": [
{"type": "text", "text": "You are a pirate"},
{"type": "text", "text": "tell tall tales"}
]
},
{"role": "user", "content": "hi"}
]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn should_reject_request_missing_temperature_field() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_temperature(0.7)
.respond_with_content("ok"),
)
.build()
.await
.unwrap();
let resp = reqwest::Client::new()
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
}
#[tokio::test]
async fn should_match_metadata_coerced_from_number_or_bool() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_metadata("priority", "2")
.respond_with_content("num-coerced"),
)
.fixture(
Fixture::new()
.match_metadata("active", "true")
.respond_with_content("bool-coerced"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"metadata": {"priority": 2},
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["choices"][0]["message"]["content"], "num-coerced");
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"metadata": {"active": true},
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["choices"][0]["message"]["content"], "bool-coerced");
}
#[tokio::test]
async fn should_reject_metadata_with_object_value() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_metadata("nested", "value")
.respond_with_content("ok"),
)
.build()
.await
.unwrap();
let resp = reqwest::Client::new()
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"metadata": {"nested": {"inner": "value"}},
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
}
#[tokio::test]
async fn should_reject_request_missing_metadata_object() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_metadata("tenant", "acme")
.respond_with_content("ok"),
)
.build()
.await
.unwrap();
let resp = reqwest::Client::new()
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
}
#[tokio::test]
async fn should_reject_request_when_metadata_key_missing() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_metadata("tenant", "acme")
.respond_with_content("ok"),
)
.build()
.await
.unwrap();
let resp = reqwest::Client::new()
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"metadata": {"other": "field"},
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
}
#[tokio::test]
async fn should_reject_tool_schema_when_no_matching_tool_declared() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_tool_schema("nonexistent_tool")
.respond_with_content("ok"),
)
.build()
.await
.unwrap();
let resp = reqwest::Client::new()
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"tools": [{"type": "function", "function": {"name": "get_weather"}}],
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
}
#[tokio::test]
async fn should_override_gemini_streaming_stop_reason() {
use llmposter::fixture::FixtureResponse;
let server = ServerBuilder::new()
.fixture(Fixture {
response: Some(FixtureResponse {
content: Some("truncated gemini".to_string()),
stop_reason: Some("MAX_TOKENS".to_string()),
..Default::default()
}),
..Fixture::new()
})
.build()
.await
.unwrap();
let body: String = reqwest::Client::new()
.post(format!(
"{}/v1beta/models/gemini-pro:streamGenerateContent?alt=sse",
server.url()
))
.json(&serde_json::json!({
"contents": [{"role": "user", "parts": [{"text": "hi"}]}]
}))
.send()
.await
.unwrap()
.text()
.await
.unwrap();
assert!(
body.contains("MAX_TOKENS"),
"expected MAX_TOKENS stop reason, got:\n{body}"
);
}
#[tokio::test]
async fn should_override_openai_streaming_finish_reason() {
use llmposter::fixture::FixtureResponse;
let server = ServerBuilder::new()
.fixture(Fixture {
response: Some(FixtureResponse {
content: Some("truncated stream".to_string()),
finish_reason: Some("length".to_string()),
..Default::default()
}),
..Fixture::new()
})
.build()
.await
.unwrap();
let body: String = reqwest::Client::new()
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}],
"stream": true
}))
.send()
.await
.unwrap()
.text()
.await
.unwrap();
assert!(
body.contains("\"finish_reason\":\"length\""),
"expected length finish_reason, got:\n{body}"
);
}
#[tokio::test]
async fn should_serve_code_429_without_provider_specific_headers() {
let server = ServerBuilder::new()
.fixture(Fixture::new().respond_with_content("unused"))
.build()
.await
.unwrap();
let resp = reqwest::get(format!("{}/code/429", server.url()))
.await
.unwrap();
assert_eq!(resp.status(), 429);
assert_eq!(resp.headers().get("retry-after").unwrap(), "60");
assert!(resp.headers().get("x-ratelimit-limit-requests").is_none());
assert!(resp
.headers()
.get("anthropic-ratelimit-requests-limit")
.is_none());
}
#[tokio::test]
async fn should_reject_system_prompt_when_pattern_does_not_match_actual_text() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_system_prompt("pirate")
.respond_with_content("ok"),
)
.build()
.await
.unwrap();
let resp = reqwest::Client::new()
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [
{"role": "system", "content": "be a friendly assistant"},
{"role": "user", "content": "hi"}
]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
}
#[tokio::test]
async fn should_reject_system_prompt_when_no_system_message_present() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.match_system_prompt("pirate")
.respond_with_content("ok"),
)
.build()
.await
.unwrap();
let resp = reqwest::Client::new()
.post(format!(
"{}/v1beta/models/gemini-pro:generateContent",
server.url()
))
.json(&serde_json::json!({
"contents": [{"role": "user", "parts": [{"text": "hi"}]}]
}))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 404);
}
#[tokio::test]
async fn should_sort_catch_all_fixtures_by_priority() {
let server = ServerBuilder::new()
.fixture(
Fixture::new()
.as_catch_all()
.with_priority(1)
.respond_with_content("low-pri-catch-all"),
)
.fixture(
Fixture::new()
.as_catch_all()
.with_priority(10)
.respond_with_content("high-pri-catch-all"),
)
.build()
.await
.unwrap();
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/v1/chat/completions", server.url()))
.json(&serde_json::json!({
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}]
}))
.send()
.await
.unwrap();
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(
body["choices"][0]["message"]["content"],
"high-pri-catch-all"
);
}