use mockserver_client::*;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
fn get_client() -> MockServerClient {
let url = std::env::var("MOCKSERVER_URL")
.expect("MOCKSERVER_URL must be set (e.g., http://localhost:1080)");
let url = url.trim_end_matches('/');
let without_scheme = url
.strip_prefix("http://")
.or_else(|| url.strip_prefix("https://"))
.unwrap_or(url);
let (host, port_str) = without_scheme
.rsplit_once(':')
.expect("MOCKSERVER_URL must include port (e.g., http://localhost:1080)");
let port: u16 = port_str.parse().expect("Port must be a valid u16");
let secure = url.starts_with("https://");
ClientBuilder::new(host, port)
.secure(secure)
.build()
.expect("Failed to build client")
}
fn data_host_port() -> (String, u16) {
let url = std::env::var("MOCKSERVER_URL")
.expect("MOCKSERVER_URL must be set (e.g., http://localhost:1080)");
let url = url.trim_end_matches('/');
let without_scheme = url
.strip_prefix("http://")
.or_else(|| url.strip_prefix("https://"))
.unwrap_or(url);
let (host, port_str) = without_scheme
.rsplit_once(':')
.expect("MOCKSERVER_URL must include port (e.g., http://localhost:1080)");
(host.to_string(), port_str.parse().expect("Port must be a valid u16"))
}
fn http_get(path: &str, extra_headers: &[(&str, &str)]) -> (u16, String) {
let (host, port) = data_host_port();
let mut stream =
TcpStream::connect((host.as_str(), port)).expect("connect to MockServer data plane");
stream
.set_read_timeout(Some(Duration::from_secs(20)))
.expect("set read timeout");
stream
.set_write_timeout(Some(Duration::from_secs(20)))
.expect("set write timeout");
let mut req = format!("GET {path} HTTP/1.1\r\nHost: {host}:{port}\r\nConnection: close\r\n");
for (name, value) in extra_headers {
req.push_str(name);
req.push_str(": ");
req.push_str(value);
req.push_str("\r\n");
}
req.push_str("\r\n");
stream.write_all(req.as_bytes()).expect("write request");
stream.flush().ok();
let mut raw = Vec::new();
let mut chunk = [0u8; 4096];
loop {
match stream.read(&mut chunk) {
Ok(0) => break, Ok(n) => {
raw.extend_from_slice(&chunk[..n]);
if response_complete(&raw) {
break;
}
}
Err(e)
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut =>
{
break
}
Err(e) => panic!("read response failed: {e}"),
}
}
let text = String::from_utf8_lossy(&raw);
let status = text
.split_whitespace()
.nth(1)
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or_else(|| panic!("could not parse status line from response: {text:?}"));
let body = text
.split_once("\r\n\r\n")
.map_or(String::new(), |(_, b)| b.to_string());
(status, body)
}
fn response_complete(raw: &[u8]) -> bool {
let text = String::from_utf8_lossy(raw);
let Some((headers, _)) = text.split_once("\r\n\r\n") else {
return false;
};
let body_start = headers.len() + 4; for line in headers.split("\r\n") {
let lower = line.to_ascii_lowercase();
if let Some(rest) = lower.strip_prefix("content-length:") {
if let Ok(len) = rest.trim().parse::<usize>() {
return raw.len() >= body_start + len;
}
}
}
false
}
#[test]
#[ignore]
fn test_create_expectation_and_verify() {
let client = get_client();
client.reset().expect("reset failed");
client
.when(HttpRequest::new().method("GET").path("/integration-test"))
.respond(HttpResponse::new().status_code(200).body("integration OK"))
.expect("creating expectation failed");
let expectations = client
.retrieve_active_expectations(None)
.expect("retrieve failed");
assert!(
!expectations.is_empty(),
"Should have at least one active expectation"
);
client.reset().expect("reset failed");
}
#[test]
#[ignore]
fn test_status() {
let client = get_client();
let ports = client.status().expect("status failed");
assert!(!ports.ports.is_empty(), "Server should report at least one port");
}
#[test]
#[ignore]
fn test_clear_and_reset() {
let client = get_client();
client
.when(HttpRequest::new().method("POST").path("/to-clear"))
.respond(HttpResponse::new().status_code(201))
.expect("creating expectation failed");
client
.clear(
Some(&HttpRequest::new().path("/to-clear")),
Some(ClearType::Expectations),
)
.expect("clear failed");
client.reset().expect("reset failed");
}
#[test]
#[ignore]
fn test_verify_failure() {
let client = get_client();
client.reset().expect("reset failed");
let result = client.verify(
HttpRequest::new().method("DELETE").path("/never-called"),
VerificationTimes::at_least(1),
);
assert!(
result.is_err(),
"Verification should fail for a request that was never received"
);
match result.unwrap_err() {
Error::VerificationFailure(_) => {} other => panic!("Expected VerificationFailure, got: {other}"),
}
}
#[test]
#[ignore]
fn test_forward_expectation() {
let client = get_client();
client.reset().expect("reset failed");
client
.when(HttpRequest::new().method("GET").path("/forward-test"))
.forward(HttpForward::new("httpbin.org", 80).scheme("HTTP"))
.expect("creating forward expectation failed");
let expectations = client
.retrieve_active_expectations(None)
.expect("retrieve failed");
assert!(!expectations.is_empty());
client.reset().expect("reset failed");
}
#[test]
#[ignore]
fn test_respond_with_advanced_response_builders() {
let client = get_client();
client.reset().expect("reset failed");
client
.when(HttpRequest::new().method("GET").path("/sse"))
.respond_with_sse(HttpSseResponse::new().event(SseEvent::new().data("tick")))
.expect("respond_with_sse failed");
client
.when(HttpRequest::new().method("GET").path("/ws"))
.respond_with_web_socket(
HttpWebSocketResponse::new().message(WebSocketMessage::text("hi")),
)
.expect("respond_with_web_socket failed");
client
.when(HttpRequest::new().path("/dns"))
.respond_with_dns(DnsResponse::new().answer_record(DnsRecord::a("host", "10.0.0.1")))
.expect("respond_with_dns failed");
client
.when(HttpRequest::new().path("/raw"))
.respond_with_binary(BinaryResponse::from_bytes([0x00, 0xFF]))
.expect("respond_with_binary failed");
client
.when(HttpRequest::new().path("/grpc"))
.respond_with_grpc_stream(
GrpcStreamResponse::new().message(GrpcStreamMessage::json("{}")),
)
.expect("respond_with_grpc_stream failed");
let expectations = client
.retrieve_active_expectations(None)
.expect("retrieve failed");
assert_eq!(
expectations.len(),
5,
"all five advanced-response expectations should be registered"
);
client.reset().expect("reset failed");
}
#[test]
#[ignore]
fn test_verify_sequence() {
let client = get_client();
client.reset().expect("reset failed");
let result = client.verify_sequence(vec![
HttpRequest::new().path("/seq-1"),
HttpRequest::new().path("/seq-2"),
]);
assert!(result.is_err());
client.reset().expect("reset failed");
}
#[test]
#[ignore]
fn test_negation_matcher_enforced_over_wire() {
let client = get_client();
client.reset().expect("reset failed");
client
.when(
HttpRequest::new()
.method("GET")
.path("/neg-header")
.header("X-Neg", "!foo"),
)
.respond(HttpResponse::new().status_code(200).body("NEG_MATCH"))
.expect("creating bare-negation expectation failed");
let (status_bar, body_bar) = http_get("/neg-header", &[("X-Neg", "bar")]);
assert_eq!(
status_bar, 200,
"a value that is NOT 'foo' must satisfy the negation and match"
);
assert_eq!(body_bar, "NEG_MATCH");
let (status_foo, _) = http_get("/neg-header", &[("X-Neg", "foo")]);
assert_eq!(
status_foo, 404,
"the excluded value 'foo' must NOT match — negation enforced server-side"
);
client.reset().expect("reset failed");
client
.when(
HttpRequest::new()
.method("GET")
.path("/neg-typed")
.header_matcher("X-Tag", MatcherValue::not_literal("foo")),
)
.respond(HttpResponse::new().status_code(200).body("NOT_LITERAL"))
.expect("creating not_literal expectation failed");
assert_eq!(
http_get("/neg-typed", &[("X-Tag", "other")]).0,
200,
"not_literal('foo') must match any value other than 'foo'"
);
assert_eq!(
http_get("/neg-typed", &[("X-Tag", "foo")]).0,
404,
"not_literal('foo') must reject 'foo'"
);
client.reset().expect("reset failed");
client
.when(
HttpRequest::new()
.method("GET")
.path("/neg-escaped")
.header_matcher("X-Lit", MatcherValue::literal("!foo")),
)
.respond(HttpResponse::new().status_code(200).body("LITERAL_BANG"))
.expect("creating literal-'!foo' expectation failed");
assert_eq!(
http_get("/neg-escaped", &[("X-Lit", "!foo")]).0,
200,
"an escaped literal '!foo' matcher must match the value '!foo' itself"
);
assert_eq!(
http_get("/neg-escaped", &[("X-Lit", "foo")]).0,
404,
"'foo' must NOT match a literal '!foo' matcher — the '!' is not a negation"
);
client.reset().expect("reset failed");
}
fn decoded_header(expectations: &[Expectation], path: &str, key: &str) -> MatcherValue {
let request = expectations
.iter()
.filter_map(|e| e.http_request.as_ref())
.find(|r| r.path.as_deref() == Some(path))
.unwrap_or_else(|| panic!("no expectation retrieved for path {path}"));
if let Some(matcher) = request
.header_matchers
.as_ref()
.and_then(|m| m.get(key))
.and_then(|vs| vs.first())
{
return matcher.clone();
}
let raw = request
.headers
.as_ref()
.and_then(|m| m.get(key))
.and_then(|vs| vs.first())
.unwrap_or_else(|| panic!("header {key} not present on expectation for {path}"));
MatcherValue::from(raw.clone())
}
#[test]
#[ignore]
fn test_negation_matcher_decoded_from_server() {
let client = get_client();
client.reset().expect("reset failed");
client
.when(
HttpRequest::new()
.method("GET")
.path("/neg-decode")
.header_matcher("X-Tag", MatcherValue::not_literal("foo")),
)
.respond(HttpResponse::new().status_code(200))
.expect("creating not_literal expectation failed");
client
.when(
HttpRequest::new()
.method("GET")
.path("/esc-decode")
.header_matcher("X-Lit", MatcherValue::literal("!foo")),
)
.respond(HttpResponse::new().status_code(200))
.expect("creating escaped-literal expectation failed");
let expectations = client
.retrieve_active_expectations(None)
.expect("retrieve failed");
let neg = decoded_header(&expectations, "/neg-decode", "X-Tag");
assert!(
neg.not,
"the '!' negation flag must survive the server echo → client decode (got {neg:?})"
);
assert_eq!(
neg,
MatcherValue::not_literal("foo"),
"server-echoed negation must decode to not=true, value=foo (got {neg:?})"
);
let esc = decoded_header(&expectations, "/esc-decode", "X-Lit");
assert!(
!esc.not,
"an escaped '!foo' must NOT be decoded as a negation (got {esc:?})"
);
assert_eq!(
esc.value, "!foo",
"the escaped '!' must survive the round-trip as literal data (got {esc:?})"
);
assert_eq!(
esc,
MatcherValue::literal("!foo"),
"server-echoed escaped literal must decode to not=false, value=!foo (got {esc:?})"
);
client.reset().expect("reset failed");
}
#[test]
#[ignore]
fn test_forward_action_actually_forwards() {
let client = get_client();
client.reset().expect("reset failed");
let internal_port = *client
.status()
.expect("status failed")
.ports
.first()
.expect("server should report at least one bound port");
client
.when(HttpRequest::new().method("GET").path("/forward-loop"))
.priority(10)
.times(Times::once())
.forward(HttpForward::new("127.0.0.1", internal_port).scheme("HTTP"))
.expect("creating loopback forward expectation failed");
client
.when(HttpRequest::new().method("GET").path("/forward-loop"))
.priority(0)
.respond(HttpResponse::new().status_code(200).body("UPSTREAM_REACHED"))
.expect("creating fall-through respond expectation failed");
let (status, body) = http_get("/forward-loop", &[]);
assert_eq!(status, 200, "looped-back forward response status");
assert_eq!(
body, "UPSTREAM_REACHED",
"body must come from the fall-through reached VIA the loopback forward, \
proving the server actually forwarded the request"
);
client.reset().expect("reset failed");
}
#[test]
#[ignore]
fn test_error_action_actually_returns_raw_bytes() {
let client = get_client();
client.reset().expect("reset failed");
const TEAPOT_RESPONSE_B64: &str =
"SFRUUC8xLjEgNDE4IFRlYXBvdA0KQ29udGVudC1MZW5ndGg6IDgNCkNvbm5lY3Rpb246IGNsb3NlDQoNClRFQVBPVCEh";
client
.when(HttpRequest::new().method("GET").path("/error-drive"))
.error(HttpError::new().response_bytes(TEAPOT_RESPONSE_B64))
.expect("creating error expectation failed");
let (status, body) = http_get("/error-drive", &[]);
assert_eq!(
status, 418,
"server must actually perform the error action and write the raw bytes back"
);
assert_eq!(body, "TEAPOT!!");
client.reset().expect("reset failed");
}
#[test]
#[ignore]
fn test_response_template_rendered_over_wire() {
let client = get_client();
client.reset().expect("reset failed");
let template =
r#"{"statusCode": 200, "body": "TEMPLATED path=$!{request.path}"}"#;
let expectation = Expectation::new(HttpRequest::new().method("GET").path("/tmpl-wire"))
.respond_template(HttpTemplate::new("VELOCITY", template));
client
.upsert(&[expectation])
.expect("registering response-template expectation failed");
let (status, body) = http_get("/tmpl-wire", &[]);
assert_eq!(
status, 200,
"the response template must be executed and produce a 200"
);
assert_eq!(
body, "TEMPLATED path=/tmpl-wire",
"the served body must be the VELOCITY-rendered template with the request path \
substituted — proving the server ran the template engine over the live request"
);
client.reset().expect("reset failed");
}
#[test]
#[ignore]
fn test_mustache_response_template_rendered_over_wire() {
let client = get_client();
client.reset().expect("reset failed");
let template =
r#"{ "statusCode": 200, "body": "MUSTACHE method={{request.method}}" }"#;
let expectation = Expectation::new(HttpRequest::new().method("GET").path("/mustache-wire"))
.respond_template(HttpTemplate::new("MUSTACHE", template));
client
.upsert(&[expectation])
.expect("registering mustache response-template expectation failed");
let (status, body) = http_get("/mustache-wire", &[]);
assert_eq!(
status, 200,
"the MUSTACHE response template must be executed and produce a 200"
);
assert_eq!(
body, "MUSTACHE method=GET",
"the served body must be the MUSTACHE-rendered template with the request method \
substituted — proving the server ran the MUSTACHE engine over the live request"
);
client.reset().expect("reset failed");
}