1use crate::util::Result;
22use serde_json::{json, Value};
23use std::io::{Read, Write};
24use std::net::{SocketAddr, TcpListener, TcpStream};
25use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
26use std::sync::{Arc, Mutex};
27use std::time::Duration;
28
29#[derive(Debug, Clone)]
31pub struct Route {
32 pub method: Option<String>,
33 pub path: String,
34 pub status: u16,
35 pub body: Value,
36 pub content_type: Option<String>,
37}
38
39#[derive(Debug, Clone)]
41pub struct Seen {
42 pub method: String,
43 pub path: String,
45 pub headers: Vec<(String, String)>,
47 pub body: String,
49 pub body_bytes: Vec<u8>,
51}
52
53pub type Request = Seen;
55
56impl Seen {
57 pub fn route(&self) -> &str {
59 self.path.split('?').next().unwrap_or("")
60 }
61 pub fn query(&self) -> Option<&str> {
63 self.path.split_once('?').map(|(_, q)| q)
64 }
65 pub fn query_param(&self, name: &str) -> Option<&str> {
67 self.query()?
68 .split('&')
69 .filter_map(|kv| kv.split_once('=').or(Some((kv, ""))))
70 .find(|(k, _)| *k == name)
71 .map(|(_, v)| v)
72 }
73 pub fn header(&self, name: &str) -> Option<&str> {
75 self.headers
76 .iter()
77 .find(|(k, _)| k.eq_ignore_ascii_case(name))
78 .map(|(_, v)| v.as_str())
79 }
80 pub fn json(&self) -> Value {
82 serde_json::from_slice(&self.body_bytes).unwrap_or(Value::Null)
83 }
84 pub fn is(&self, method: &str, route: &str) -> bool {
85 self.method.eq_ignore_ascii_case(method) && self.route() == route
86 }
87 pub fn to_value(&self) -> Value {
88 let headers: serde_json::Map<String, Value> = self
89 .headers
90 .iter()
91 .map(|(k, v)| (k.to_ascii_lowercase(), json!(v)))
92 .collect();
93 json!({"method": self.method, "path": self.path, "headers": headers, "body": self.body,
94 "json": self.json()})
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct Reply {
101 pub status: u16,
102 pub content_type: String,
103 pub headers: Vec<(String, String)>,
104 pub body: Vec<u8>,
105}
106
107impl Reply {
108 pub fn json(v: Value) -> Self {
110 Self::status(200, v)
111 }
112 pub fn status(status: u16, v: Value) -> Self {
114 Self {
115 status,
116 content_type: "application/json".into(),
117 headers: vec![],
118 body: v.to_string().into_bytes(),
119 }
120 }
121 pub fn bytes(content_type: &str, body: impl Into<Vec<u8>>) -> Self {
123 Self {
124 status: 200,
125 content_type: content_type.into(),
126 headers: vec![],
127 body: body.into(),
128 }
129 }
130 pub fn file(content_type: &str, path: &std::path::Path) -> Self {
132 match std::fs::read(path) {
133 Ok(b) => Self::bytes(content_type, b),
134 Err(e) => Self::status(
135 500,
136 json!({"error": format!("mock could not read {}: {e}", path.display())}),
137 ),
138 }
139 }
140 pub fn text(body: &str) -> Self {
142 Self::bytes("text/plain; charset=utf-8", body.as_bytes().to_vec())
143 }
144 pub fn unmocked() -> Self {
146 Self::status(404, json!({"error": "unmocked"}))
147 }
148 pub fn with_status(mut self, status: u16) -> Self {
149 self.status = status;
150 self
151 }
152 pub fn with_header(mut self, name: &str, value: &str) -> Self {
153 self.headers.push((name.into(), value.into()));
154 self
155 }
156 pub fn ignore_range(self) -> Self {
160 self.with_header(IGNORE_RANGE_HEADER, "1")
161 }
162 pub fn drop_connection() -> Self {
167 Self::bytes("application/octet-stream", Vec::new()).with_header(DROP_HEADER, "1")
168 }
169 pub fn is_drop(&self) -> bool {
171 self.headers
172 .iter()
173 .any(|(k, _)| k.eq_ignore_ascii_case(DROP_HEADER))
174 }
175 pub fn cut_after(self, bytes: u64) -> Self {
180 self.with_header(CUT_AFTER_HEADER, &bytes.to_string())
181 }
182}
183
184pub const DROP_HEADER: &str = "X-RightKit-Mock-Drop";
186pub const IGNORE_RANGE_HEADER: &str = "X-RightKit-Mock-Ignore-Range";
188pub const CUT_AFTER_HEADER: &str = "X-RightKit-Mock-Cut-After";
190
191#[derive(Debug, Clone, PartialEq, Eq)]
193enum BodyPlan {
194 Full,
195 Partial(u64, u64),
197 Unsatisfiable,
198}
199
200fn plan_range(range: Option<&str>, len: u64) -> Option<BodyPlan> {
203 let spec = range?.trim().strip_prefix("bytes=")?.trim();
204 if spec.contains(',') {
205 return None;
206 }
207 let (a, b) = spec.split_once('-')?;
208 let (a, b) = (a.trim(), b.trim());
209 let digits = |s: &str| !s.is_empty() && s.bytes().all(|c| c.is_ascii_digit());
210 if a.is_empty() {
211 if !digits(b) {
212 return None;
213 }
214 let n: u64 = b.parse().ok()?;
215 if n == 0 || len == 0 {
216 return Some(BodyPlan::Unsatisfiable);
217 }
218 return Some(BodyPlan::Partial(len.saturating_sub(n), len - 1));
219 }
220 if !digits(a) || !(b.is_empty() || digits(b)) {
221 return None;
222 }
223 let start: u64 = a.parse().ok()?;
224 let end = if b.is_empty() {
225 None
226 } else {
227 Some(b.parse::<u64>().ok()?)
228 };
229 if end.is_some_and(|e| e < start) {
230 return None;
231 }
232 if start >= len {
233 return Some(BodyPlan::Unsatisfiable);
234 }
235 Some(BodyPlan::Partial(
236 start,
237 end.map_or(len - 1, |e| e.min(len - 1)),
238 ))
239}
240
241fn if_range_holds(if_range: &str, headers: &[(String, String)]) -> bool {
243 let v = if_range.trim();
244 let get = |n: &str| {
245 headers
246 .iter()
247 .find(|(k, _)| k.eq_ignore_ascii_case(n))
248 .map(|(_, v)| v.trim())
249 };
250 if v.starts_with('"') || v.starts_with("W/") {
251 !v.starts_with("W/") && get("etag").is_some_and(|e| e == v && !e.starts_with("W/"))
252 } else {
253 get("last-modified").is_some_and(|m| m == v)
254 }
255}
256
257impl From<Option<Reply>> for Reply {
259 fn from(r: Option<Reply>) -> Self {
260 r.unwrap_or_else(Reply::unmocked)
261 }
262}
263
264type Handler = dyn Fn(&Request, &str) -> Reply + Send + Sync;
265
266#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct RequestLog {
269 pub method: String,
271 pub path: String,
273 pub status: u16,
275 pub rejected: Option<&'static str>,
278}
279
280type LogHook = dyn Fn(&RequestLog) + Send + Sync;
281
282#[derive(Clone, Default)]
285pub struct MockOptions {
286 pub port: Option<u16>,
288 pub max_header_bytes: Option<usize>,
290 pub max_body_bytes: Option<usize>,
292 pub reject_transfer_encoding: bool,
294 pub max_connections: Option<usize>,
296 pub linger: Option<Duration>,
300 pub on_request: Option<Arc<LogHook>>,
302}
303
304impl MockOptions {
305 pub fn port(mut self, port: u16) -> Self {
306 self.port = Some(port);
307 self
308 }
309 pub fn max_header_bytes(mut self, n: usize) -> Self {
310 self.max_header_bytes = Some(n);
311 self
312 }
313 pub fn max_body_bytes(mut self, n: usize) -> Self {
314 self.max_body_bytes = Some(n);
315 self
316 }
317 pub fn reject_transfer_encoding(mut self) -> Self {
318 self.reject_transfer_encoding = true;
319 self
320 }
321 pub fn max_connections(mut self, n: usize) -> Self {
322 self.max_connections = Some(n);
323 self
324 }
325 pub fn linger(mut self, d: Duration) -> Self {
326 self.linger = Some(d);
327 self
328 }
329 pub fn on_request(mut self, f: impl Fn(&RequestLog) + Send + Sync + 'static) -> Self {
330 self.on_request = Some(Arc::new(f));
331 self
332 }
333}
334
335#[derive(Clone)]
338pub struct ShutdownHandle {
339 addr: SocketAddr,
340 stop: Arc<AtomicBool>,
341}
342
343impl ShutdownHandle {
344 pub fn shutdown(&self) {
346 self.stop.store(true, Ordering::SeqCst);
347 let _ = TcpStream::connect_timeout(&self.addr, Duration::from_millis(200));
348 }
349 pub fn is_shutdown(&self) -> bool {
350 self.stop.load(Ordering::SeqCst)
351 }
352 pub fn wait(&self) {
354 while !self.is_shutdown() {
355 std::thread::sleep(Duration::from_millis(5));
356 }
357 }
358}
359
360struct Limits {
361 opts: MockOptions,
362}
363
364pub struct MockServer {
365 pub base: String,
366 addr: SocketAddr,
367 seen: Arc<Mutex<Vec<Seen>>>,
368 stop: Arc<AtomicBool>,
369}
370
371impl MockServer {
372 pub fn start(routes: Vec<Route>) -> Result<Self> {
374 Self::start_with(move |req: &Request, _base: &str| {
375 routes
376 .iter()
377 .find(|r| {
378 r.path == req.route()
379 && r.method
380 .as_deref()
381 .map(|m| m.eq_ignore_ascii_case(&req.method))
382 .unwrap_or(true)
383 })
384 .map(|r| Reply {
385 status: r.status,
386 content_type: r
387 .content_type
388 .clone()
389 .unwrap_or_else(|| "application/json".into()),
390 headers: vec![],
391 body: r.body.to_string().into_bytes(),
392 })
393 })
394 }
395
396 pub fn start_static(routes: Vec<(Option<String>, String, Reply)>) -> Result<Self> {
400 Self::start_with(move |req: &Request, _base: &str| {
401 routes
402 .iter()
403 .find(|(m, p, _)| {
404 *p == req.route()
405 && m.as_deref()
406 .is_none_or(|m| m.eq_ignore_ascii_case(&req.method))
407 })
408 .map(|(_, _, r)| r.clone())
409 })
410 }
411
412 pub fn start_with<R: Into<Reply>>(
417 handler: impl Fn(&Request, &str) -> R + Send + Sync + 'static,
418 ) -> Result<Self> {
419 Self::start_opts(MockOptions::default(), handler)
420 }
421
422 pub fn start_opts<R: Into<Reply>>(
426 opts: MockOptions,
427 handler: impl Fn(&Request, &str) -> R + Send + Sync + 'static,
428 ) -> Result<Self> {
429 let listener = TcpListener::bind(("127.0.0.1", opts.port.unwrap_or(0)))?;
430 let addr = listener.local_addr()?;
431 let base = format!("http://{addr}");
432 let seen = Arc::new(Mutex::new(Vec::new()));
433 let stop = Arc::new(AtomicBool::new(false));
434 let handler: Arc<Handler> = Arc::new(move |r: &Request, b: &str| handler(r, b).into());
435 let (s2, st2, b2) = (seen.clone(), stop.clone(), base.clone());
436 let limits = Arc::new(Limits { opts });
437 let active = Arc::new(AtomicUsize::new(0));
438 std::thread::spawn(move || {
439 for mut conn in listener.incoming().flatten() {
440 if st2.load(Ordering::SeqCst) {
441 break;
442 }
443 if limits
444 .opts
445 .max_connections
446 .is_some_and(|cap| active.load(Ordering::SeqCst) >= cap)
447 {
448 reject(&mut conn, &limits, 503, "too many connections");
449 continue;
450 }
451 active.fetch_add(1, Ordering::SeqCst);
452 let (s3, h, b, l, a) = (
453 s2.clone(),
454 handler.clone(),
455 b2.clone(),
456 limits.clone(),
457 active.clone(),
458 );
459 let spawned = std::thread::Builder::new().spawn(move || {
460 let _ = serve(conn, &s3, &b, &*h, &l);
461 a.fetch_sub(1, Ordering::SeqCst);
462 });
463 if spawned.is_err() {
464 active.fetch_sub(1, Ordering::SeqCst);
465 }
466 }
467 });
468 Ok(Self {
469 base,
470 addr,
471 seen,
472 stop,
473 })
474 }
475
476 pub fn shutdown_handle(&self) -> ShutdownHandle {
478 ShutdownHandle {
479 addr: self.addr,
480 stop: self.stop.clone(),
481 }
482 }
483
484 pub fn port(&self) -> u16 {
485 self.addr.port()
486 }
487 pub fn url(&self, path: &str) -> String {
489 format!("{}{path}", self.base)
490 }
491 pub fn seen(&self) -> Vec<Seen> {
492 self.seen.lock().unwrap().clone()
493 }
494 pub fn close(&self) {
495 self.stop.store(true, Ordering::SeqCst);
496 let _ = TcpStream::connect_timeout(&self.addr, Duration::from_millis(200));
497 }
498}
499
500impl Drop for MockServer {
501 fn drop(&mut self) {
502 self.close();
503 }
504}
505
506fn serve(
507 mut s: TcpStream,
508 seen: &Mutex<Vec<Seen>>,
509 base: &str,
510 handler: &Handler,
511 limits: &Limits,
512) -> std::io::Result<()> {
513 s.set_read_timeout(Some(Duration::from_secs(10)))?;
514 let o = &limits.opts;
515 let mut buf = Vec::new();
516 let mut chunk = [0u8; 16384];
517 let (head_end, content_len) = loop {
518 let n = s.read(&mut chunk)?;
519 if n == 0 {
520 return Ok(());
521 }
522 buf.extend_from_slice(&chunk[..n]);
523 let end = buf.windows(4).position(|w| w == b"\r\n\r\n");
524 if let Some(max) = o.max_header_bytes {
525 if end.unwrap_or(buf.len()) > max {
526 reject(&mut s, limits, 431, "request headers are too large");
527 return Ok(());
528 }
529 }
530 if let Some(p) = end {
531 let head = String::from_utf8_lossy(&buf[..p]).to_ascii_lowercase();
532 if o.reject_transfer_encoding
533 && head.lines().skip(1).any(|l| {
534 l.split_once(':')
535 .is_some_and(|(k, _)| k.trim() == "transfer-encoding")
536 })
537 {
538 reject(
539 &mut s,
540 limits,
541 501,
542 "Transfer-Encoding is not supported; send Content-Length",
543 );
544 return Ok(());
545 }
546 let mut lens = head.lines().skip(1).filter_map(|l| {
547 l.split_once(':')
548 .filter(|(k, _)| k.trim() == "content-length")
549 .map(|(_, v)| v.trim().to_string())
550 });
551 let len = match (lens.next(), lens.next()) {
552 (None, _) => 0,
553 (Some(v), None) if !v.is_empty() && v.bytes().all(|b| b.is_ascii_digit()) => {
554 match v.parse::<usize>() {
555 Ok(n) => n,
556 Err(_) => {
557 reject(&mut s, limits, 400, "invalid Content-Length");
558 return Ok(());
559 }
560 }
561 }
562 _ => {
563 reject(&mut s, limits, 400, "invalid or duplicate Content-Length");
564 return Ok(());
565 }
566 };
567 if o.max_body_bytes.is_some_and(|max| len > max) {
568 reject(&mut s, limits, 413, "request body is too large");
569 return Ok(());
570 }
571 break (p + 4, len);
572 }
573 };
574 let Some(total) = head_end.checked_add(content_len) else {
575 reject(&mut s, limits, 400, "invalid Content-Length");
576 return Ok(());
577 };
578 while buf.len() < total {
579 let n = s.read(&mut chunk)?;
580 if n == 0 {
581 reject(
582 &mut s,
583 limits,
584 400,
585 "request body ended before Content-Length",
586 );
587 return Ok(());
588 }
589 buf.extend_from_slice(&chunk[..n]);
590 }
591 let head = String::from_utf8_lossy(&buf[..head_end]).to_string();
592 let mut lines = head.lines();
593 let mut first = lines.next().unwrap_or("").split_whitespace();
594 let (method, path) = (
595 first.next().unwrap_or("").to_string(),
596 first.next().unwrap_or("").to_string(),
597 );
598 let headers = lines
599 .filter_map(|l| l.split_once(':'))
600 .map(|(k, v)| (k.trim().to_string(), v.trim().to_string()))
601 .collect();
602 let body_bytes = buf[head_end..total].to_vec();
603 let req = Seen {
604 method,
605 path,
606 headers,
607 body: String::from_utf8_lossy(&body_bytes).into_owned(),
608 body_bytes,
609 };
610 seen.lock().unwrap().push(req.clone());
611 let reply = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler(&req, base)))
612 .unwrap_or_else(|_| Reply::status(500, json!({"error": "mock handler panicked"})));
613 let log = |status: u16| {
614 if let Some(f) = &o.on_request {
615 f(&RequestLog {
616 method: req.method.clone(),
617 path: req.path.clone(),
618 status,
619 rejected: None,
620 });
621 }
622 };
623 if reply.is_drop() {
624 log(0);
625 return Ok(());
626 }
627 let status = reply.status;
628 let r = write_reply(&mut s, &req, reply);
629 log(status);
630 linger(&mut s, o);
631 r
632}
633
634fn reject(s: &mut TcpStream, limits: &Limits, status: u16, why: &'static str) {
636 let _ = s.set_write_timeout(Some(Duration::from_secs(10)));
637 let req = Seen {
638 method: String::new(),
639 path: String::new(),
640 headers: vec![],
641 body: String::new(),
642 body_bytes: vec![],
643 };
644 let body = json!({"error": {"code": "bad_request", "message": why}});
645 let _ = write_reply(s, &req, Reply::status(status, body));
646 if let Some(f) = &limits.opts.on_request {
647 f(&RequestLog {
648 method: String::new(),
649 path: String::new(),
650 status,
651 rejected: Some(why),
652 });
653 }
654 linger(s, &limits.opts);
655}
656
657fn linger(s: &mut TcpStream, o: &MockOptions) {
659 let Some(d) = o.linger else { return };
660 let _ = s.shutdown(std::net::Shutdown::Write);
661 let _ = s.set_read_timeout(Some(d));
662 let mut sink = [0u8; 1024];
663 let deadline = std::time::Instant::now() + d;
664 while std::time::Instant::now() < deadline && matches!(s.read(&mut sink), Ok(n) if n > 0) {}
665}
666
667fn write_reply(s: &mut TcpStream, req: &Request, mut reply: Reply) -> std::io::Result<()> {
668 let take = |reply: &mut Reply, name: &str| -> Option<String> {
669 let i = reply
670 .headers
671 .iter()
672 .position(|(k, _)| k.eq_ignore_ascii_case(name))?;
673 Some(reply.headers.remove(i).1)
674 };
675 let ignore_range = take(&mut reply, IGNORE_RANGE_HEADER).is_some();
676 let cut_after = take(&mut reply, CUT_AFTER_HEADER).and_then(|v| v.trim().parse::<u64>().ok());
677 let len = reply.body.len() as u64;
678 let ranged_method =
679 req.method.eq_ignore_ascii_case("GET") || req.method.eq_ignore_ascii_case("HEAD");
680 let plan = if reply.status == 200 && ranged_method && !ignore_range {
681 let honoured = req
682 .header("if-range")
683 .is_none_or(|v| if_range_holds(v, &reply.headers));
684 honoured
685 .then(|| plan_range(req.header("range"), len))
686 .flatten()
687 .unwrap_or(BodyPlan::Full)
688 } else {
689 BodyPlan::Full
690 };
691 let (status, reason, body, extra): (u16, &str, &[u8], Option<String>) = match plan {
692 BodyPlan::Full => (reply.status, "X", &reply.body[..], None),
693 BodyPlan::Partial(a, b) => (
694 206,
695 "Partial Content",
696 &reply.body[a as usize..=b as usize],
697 Some(format!(
698 "Content-Range: bytes {a}-{b}/{len}\r\nAccept-Ranges: bytes\r\n"
699 )),
700 ),
701 BodyPlan::Unsatisfiable => (
702 416,
703 "Range Not Satisfiable",
704 &[][..],
705 Some(format!(
706 "Content-Range: bytes */{len}\r\nAccept-Ranges: bytes\r\n"
707 )),
708 ),
709 };
710 let mut head = format!(
711 "HTTP/1.1 {status} {reason}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n",
712 reply.content_type,
713 body.len()
714 );
715 head.push_str(extra.as_deref().unwrap_or(""));
716 for (k, v) in &reply.headers {
717 head.push_str(&format!("{k}: {v}\r\n"));
718 }
719 head.push_str("\r\n");
720 s.write_all(head.as_bytes())?;
721 if !req.method.eq_ignore_ascii_case("HEAD") {
722 let n = cut_after.map_or(body.len(), |c| (c as usize).min(body.len()));
723 s.write_all(&body[..n])?;
724 if n < body.len() {
725 s.flush()?;
726 return s.shutdown(std::net::Shutdown::Both);
727 }
728 }
729 s.flush()
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735 use crate::http::request;
736 use std::sync::atomic::AtomicUsize;
737
738 fn get(m: &MockServer, method: &str, path: &str, body: Option<&str>) -> crate::http::Response {
739 request(
740 m.addr,
741 method,
742 path,
743 Some("Bearer k"),
744 body,
745 Duration::from_secs(5),
746 )
747 .unwrap()
748 }
749
750 #[test]
751 fn static_routes_still_answer_json_and_404() {
752 let m = MockServer::start(vec![Route {
753 method: Some("POST".into()),
754 path: "/v1/gen".into(),
755 status: 201,
756 body: json!({"id": "j1"}),
757 content_type: None,
758 }])
759 .unwrap();
760 let r = get(&m, "POST", "/v1/gen?x=1", Some(r#"{"prompt":"p"}"#));
761 assert_eq!(r.status, 201);
762 assert_eq!(r.content_type, "application/json");
763 assert_eq!(
764 serde_json::from_slice::<Value>(&r.body).unwrap()["id"],
765 "j1"
766 );
767 assert_eq!(get(&m, "GET", "/v1/gen", None).status, 404);
768 let seen = m.seen();
769 assert_eq!(seen.len(), 2);
770 assert_eq!(seen[0].json()["prompt"], "p");
771 assert_eq!(seen[0].header("authorization"), Some("Bearer k"));
772 assert_eq!(seen[0].query_param("x"), Some("1"));
773 assert_eq!(seen[0].to_value()["json"]["prompt"], "p");
774 }
775
776 #[test]
777 fn dynamic_handler_is_stateful_binary_and_knows_its_base() {
778 let png: Vec<u8> = (0..=255u8).cycle().take(70_000).collect();
779 let polls = Arc::new(AtomicUsize::new(0));
780 let (p2, png2) = (polls.clone(), png.clone());
781 let m = MockServer::start_with(move |req: &Request, base: &str| {
782 if req.is("POST", "/jobs") {
783 Some(Reply::json(json!({"poll": format!("{base}/jobs/1")})))
784 } else if req.is("GET", "/jobs/1") {
785 let n = p2.fetch_add(1, Ordering::SeqCst);
786 Some(if n < 2 {
787 Reply::json(json!({"status": "running"}))
788 } else {
789 Reply::json(json!({"status": "done", "url": format!("{base}/out.png")}))
790 })
791 } else if req.is("GET", "/out.png") {
792 Some(Reply::bytes("image/png", png2.clone()).with_header("X-Mock", "1"))
793 } else if req.is("GET", "/boom") {
794 panic!("handler bug")
795 } else {
796 None
797 }
798 })
799 .unwrap();
800 let first: Value =
801 serde_json::from_slice(&get(&m, "POST", "/jobs", Some("{}")).body).unwrap();
802 assert_eq!(first["poll"], m.url("/jobs/1"));
803 let states: Vec<String> = (0..3)
804 .map(|_| {
805 serde_json::from_slice::<Value>(&get(&m, "GET", "/jobs/1", None).body).unwrap()
806 ["status"]
807 .as_str()
808 .unwrap()
809 .to_string()
810 })
811 .collect();
812 assert_eq!(states, ["running", "running", "done"]);
813 let media = get(&m, "GET", "/out.png", None);
814 assert_eq!(media.status, 200);
815 assert_eq!(media.content_type, "image/png");
816 assert_eq!(media.body, png, "binary body survives byte-for-byte");
817 assert_eq!(get(&m, "GET", "/boom", None).status, 500);
818 assert_eq!(get(&m, "GET", "/nope", None).status, 404);
819 assert_eq!(m.seen().len(), 7);
820 }
821
822 #[test]
823 fn a_held_connection_does_not_block_other_requests() {
824 let m = MockServer::start_with(|_: &Request, _: &str| Reply::text("ok")).unwrap();
825 let _idle = TcpStream::connect(m.addr).unwrap();
827 let started = std::time::Instant::now();
828 let r = get(&m, "GET", "/", None);
829 assert_eq!(r.text(), "ok");
830 assert!(started.elapsed() < Duration::from_secs(3));
831 }
832
833 fn raw(
835 m: &MockServer,
836 method: &str,
837 path: &str,
838 extra: &[(&str, &str)],
839 ) -> (u16, Vec<(String, String)>, Vec<u8>) {
840 let mut s = TcpStream::connect(m.addr).unwrap();
841 s.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
842 let mut head = format!("{method} {path} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n");
843 for (k, v) in extra {
844 head.push_str(&format!("{k}: {v}\r\n"));
845 }
846 head.push_str("\r\n");
847 s.write_all(head.as_bytes()).unwrap();
848 let mut buf = Vec::new();
849 let _ = s.read_to_end(&mut buf);
850 let split = buf.windows(4).position(|w| w == b"\r\n\r\n").unwrap();
851 let text = String::from_utf8_lossy(&buf[..split]).to_string();
852 let mut lines = text.lines();
853 let status = lines
854 .next()
855 .unwrap()
856 .split_whitespace()
857 .nth(1)
858 .unwrap()
859 .parse()
860 .unwrap();
861 let headers = lines
862 .filter_map(|l| l.split_once(':'))
863 .map(|(k, v)| (k.trim().to_ascii_lowercase(), v.trim().to_string()))
864 .collect();
865 (status, headers, buf[split + 4..].to_vec())
866 }
867
868 fn hdr<'a>(h: &'a [(String, String)], name: &str) -> Option<&'a str> {
869 h.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str())
870 }
871
872 fn data() -> Vec<u8> {
873 (0..1000u32).map(|i| (i * 7 % 251) as u8).collect()
874 }
875
876 #[test]
877 fn static_and_dynamic_binary_replies_serve_byte_ranges() {
878 let body = data();
879 let file =
880 Reply::bytes("application/octet-stream", body.clone()).with_header("ETag", "\"abc\"");
881 let st = MockServer::start_static(vec![
882 (Some("GET".into()), "/f.bin".into(), file.clone()),
883 (Some("HEAD".into()), "/f.bin".into(), file.clone()),
884 ])
885 .unwrap();
886 let f2 = file.clone();
887 let dy = MockServer::start_with(move |_: &Request, _: &str| f2.clone()).unwrap();
888 for m in [&st, &dy] {
889 let (s, h, b) = raw(m, "GET", "/f.bin", &[]);
890 assert_eq!((s, b.as_slice()), (200, &body[..]), "no Range: full 200");
891 assert_eq!(hdr(&h, "content-range"), None);
892
893 let (s, h, b) = raw(m, "GET", "/f.bin", &[("Range", "bytes=10-19")]);
894 assert_eq!(s, 206);
895 assert_eq!(b, &body[10..20]);
896 assert_eq!(hdr(&h, "content-range"), Some("bytes 10-19/1000"));
897 assert_eq!(hdr(&h, "content-length"), Some("10"));
898 assert_eq!(hdr(&h, "etag"), Some("\"abc\""));
899
900 let (s, h, b) = raw(m, "GET", "/f.bin", &[("Range", "bytes=990-")]);
901 assert_eq!((s, b.as_slice()), (206, &body[990..]));
902 assert_eq!(hdr(&h, "content-range"), Some("bytes 990-999/1000"));
903
904 let (s, h, b) = raw(m, "GET", "/f.bin", &[("Range", "bytes=-5")]);
905 assert_eq!((s, b.as_slice()), (206, &body[995..]));
906 assert_eq!(hdr(&h, "content-range"), Some("bytes 995-999/1000"));
907
908 let (s, h, b) = raw(m, "GET", "/f.bin", &[("Range", "bytes=-5000")]);
909 assert_eq!(
910 (s, b.as_slice()),
911 (206, &body[..]),
912 "suffix longer than the body"
913 );
914 assert_eq!(hdr(&h, "content-range"), Some("bytes 0-999/1000"));
915
916 let (s, h, b) = raw(m, "GET", "/f.bin", &[("Range", "bytes=995-5000")]);
917 assert_eq!((s, b.as_slice()), (206, &body[995..]), "end clamped");
918 assert_eq!(hdr(&h, "content-range"), Some("bytes 995-999/1000"));
919
920 for unsat in ["bytes=1000-", "bytes=5000-6000", "bytes=-0"] {
921 let (s, h, b) = raw(m, "GET", "/f.bin", &[("Range", unsat)]);
922 assert_eq!(s, 416, "{unsat}");
923 assert!(b.is_empty());
924 assert_eq!(hdr(&h, "content-range"), Some("bytes */1000"));
925 }
926 for ignored in ["bytes=5-1", "bytes=0-1,5-6", "items=0-1", "bytes=x-"] {
927 let (s, _, b) = raw(m, "GET", "/f.bin", &[("Range", ignored)]);
928 assert_eq!((s, b.as_slice()), (200, &body[..]), "{ignored}");
929 }
930 let (s, _, b) = raw(
932 m,
933 "GET",
934 "/f.bin",
935 &[("Range", "bytes=10-"), ("If-Range", "\"abc\"")],
936 );
937 assert_eq!((s, b.as_slice()), (206, &body[10..]));
938 let (s, _, b) = raw(
939 m,
940 "GET",
941 "/f.bin",
942 &[("Range", "bytes=10-"), ("If-Range", "\"old\"")],
943 );
944 assert_eq!((s, b.as_slice()), (200, &body[..]));
945 let (s, h, b) = raw(m, "HEAD", "/f.bin", &[("Range", "bytes=0-9")]);
947 assert_eq!(s, 206);
948 assert!(b.is_empty());
949 assert_eq!(hdr(&h, "content-length"), Some("10"));
950 }
951 assert_eq!(raw(&st, "POST", "/f.bin", &[]).0, 404, "method filter");
952 let err =
954 MockServer::start_with(|_: &Request, _: &str| Reply::text("nope").with_status(500))
955 .unwrap();
956 let (s, _, b) = raw(&err, "GET", "/", &[("Range", "bytes=0-1")]);
957 assert_eq!((s, b.as_slice()), (500, &b"nope"[..]));
958 }
959
960 #[test]
961 fn ignore_range_and_cut_after_simulate_no_resume_and_dropped_connections() {
962 let body = data();
963 let b2 = body.clone();
964 let m = MockServer::start_with(move |req: &Request, _: &str| {
965 let r = Reply::bytes("application/octet-stream", b2.clone());
966 match req.route() {
967 "/norange" => r.ignore_range(),
968 "/drop" => r.cut_after(100),
969 _ => r,
970 }
971 })
972 .unwrap();
973 let (s, h, b) = raw(&m, "GET", "/norange", &[("Range", "bytes=10-")]);
974 assert_eq!((s, b.as_slice()), (200, &body[..]));
975 assert_eq!(hdr(&h, "content-range"), None);
976 assert!(
977 h.iter().all(|(k, _)| !k.starts_with("x-rightkit-mock")),
978 "reserved headers are never sent: {h:?}"
979 );
980
981 let (s, h, b) = raw(&m, "GET", "/drop", &[]);
982 assert_eq!(s, 200);
983 assert_eq!(
984 hdr(&h, "content-length"),
985 Some("1000"),
986 "head promises the full body"
987 );
988 assert_eq!(b, &body[..100], "then the connection drops");
989 let (s, h, b) = raw(&m, "GET", "/drop", &[("Range", "bytes=500-")]);
990 assert_eq!(s, 206);
991 assert_eq!(hdr(&h, "content-length"), Some("500"));
992 assert_eq!(b, &body[500..600], "cut applies after range slicing");
993 let st =
995 MockServer::start_static(vec![(None, "/x".into(), Reply::text("hello").cut_after(2))])
996 .unwrap();
997 let (_, h, b) = raw(&st, "GET", "/x", &[]);
998 assert_eq!(b, b"he");
999 assert!(h.iter().all(|(k, _)| !k.starts_with("x-rightkit-mock")));
1000 }
1001
1002 fn exchange(addr: SocketAddr, req: &[u8]) -> String {
1003 let mut st = TcpStream::connect(addr).unwrap();
1004 st.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
1005 let _ = st.write_all(req);
1006 let mut out = Vec::new();
1007 let _ = st.read_to_end(&mut out);
1008 String::from_utf8_lossy(&out).into_owned()
1009 }
1010
1011 #[test]
1012 fn opts_bind_chosen_port_and_drop_connection_and_log() {
1013 let free = TcpListener::bind(("127.0.0.1", 0))
1014 .unwrap()
1015 .local_addr()
1016 .unwrap()
1017 .port();
1018 let log = Arc::new(Mutex::new(Vec::<RequestLog>::new()));
1019 let l2 = log.clone();
1020 let m = MockServer::start_opts(
1021 MockOptions::default()
1022 .port(free)
1023 .on_request(move |r| l2.lock().unwrap().push(r.clone())),
1024 |req: &Request, _| {
1025 if req.is("POST", "/half") {
1026 Reply::drop_connection()
1027 } else {
1028 Reply::text("ok")
1029 }
1030 },
1031 )
1032 .unwrap();
1033 assert_eq!(m.port(), free);
1034 assert!(exchange(m.addr, b"POST /half HTTP/1.1\r\nContent-Length: 0\r\n\r\n").is_empty());
1035 assert!(exchange(m.addr, b"GET /a HTTP/1.1\r\n\r\n").starts_with("HTTP/1.1 200"));
1036 assert_eq!(m.seen().len(), 2);
1037 let l = log.lock().unwrap();
1038 assert_eq!((l[0].path.as_str(), l[0].status), ("/half", 0));
1039 assert_eq!((l[1].path.as_str(), l[1].status), ("/a", 200));
1040 }
1041
1042 #[test]
1043 fn malformed_framing_is_rejected_before_the_handler() {
1044 let calls = Arc::new(AtomicUsize::new(0));
1045 let c2 = calls.clone();
1046 let m = MockServer::start_opts(
1047 MockOptions::default().linger(Duration::from_millis(50)),
1048 move |_: &Request, _| {
1049 c2.fetch_add(1, Ordering::SeqCst);
1050 Reply::text("ok")
1051 },
1052 )
1053 .unwrap();
1054 for bad in [
1055 "POST /x HTTP/1.1\r\nContent-Length: abc\r\n\r\n",
1056 "POST /x HTTP/1.1\r\nContent-Length: -1\r\n\r\n",
1057 "POST /x HTTP/1.1\r\nContent-Length: 99999999999999999999999\r\n\r\n",
1058 "POST /x HTTP/1.1\r\nContent-Length: 1\r\nContent-Length: 1\r\n\r\nA",
1059 ] {
1060 assert!(
1061 exchange(m.addr, bad.as_bytes()).starts_with("HTTP/1.1 400"),
1062 "{bad}"
1063 );
1064 }
1065 let mut st = TcpStream::connect(m.addr).unwrap();
1067 st.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
1068 st.write_all(b"POST /x HTTP/1.1\r\nContent-Length: 10\r\n\r\nabc")
1069 .unwrap();
1070 st.shutdown(std::net::Shutdown::Write).unwrap();
1071 let mut out = Vec::new();
1072 let _ = st.read_to_end(&mut out);
1073 assert!(String::from_utf8_lossy(&out).starts_with("HTTP/1.1 400"));
1074 assert_eq!(calls.load(Ordering::SeqCst), 0);
1075 assert!(m.seen().is_empty());
1076 assert!(
1077 exchange(m.addr, b"POST /x HTTP/1.1\r\nContent-Length: 2\r\n\r\nab")
1078 .starts_with("HTTP/1.1 200")
1079 );
1080 }
1081
1082 #[test]
1083 fn opts_limits_answer_431_413_501_without_calling_the_handler() {
1084 let calls = Arc::new(AtomicUsize::new(0));
1085 let c2 = calls.clone();
1086 let m = MockServer::start_opts(
1087 MockOptions::default()
1088 .max_header_bytes(256)
1089 .max_body_bytes(10)
1090 .reject_transfer_encoding()
1091 .linger(Duration::from_millis(50)),
1092 move |_: &Request, _| {
1093 c2.fetch_add(1, Ordering::SeqCst);
1094 Reply::text("ok")
1095 },
1096 )
1097 .unwrap();
1098 let big = format!("GET / HTTP/1.1\r\nX: {}\r\n\r\n", "a".repeat(400));
1099 assert!(exchange(m.addr, big.as_bytes()).starts_with("HTTP/1.1 431"));
1100 let r = exchange(
1101 m.addr,
1102 b"POST / HTTP/1.1\r\nContent-Length: 11\r\n\r\nhello world",
1103 );
1104 assert!(r.starts_with("HTTP/1.1 413"), "{r}");
1105 let r = exchange(
1106 m.addr,
1107 b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n",
1108 );
1109 assert!(r.starts_with("HTTP/1.1 501"), "{r}");
1110 assert!(
1111 exchange(m.addr, b"POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\nhello")
1112 .starts_with("HTTP/1.1 200")
1113 );
1114 assert_eq!(calls.load(Ordering::SeqCst), 1);
1115 }
1116
1117 #[test]
1118 fn opts_connection_cap_answers_503_and_shutdown_handle_stops() {
1119 let m = MockServer::start_opts(
1120 MockOptions::default().max_connections(1),
1121 |_: &Request, _| {
1122 std::thread::sleep(Duration::from_millis(400));
1123 Reply::text("slow")
1124 },
1125 )
1126 .unwrap();
1127 let addr = m.addr;
1128 let t = std::thread::spawn(move || exchange(addr, b"GET /s HTTP/1.1\r\n\r\n"));
1129 std::thread::sleep(Duration::from_millis(150));
1130 assert!(exchange(addr, b"GET /s HTTP/1.1\r\n\r\n").starts_with("HTTP/1.1 503"));
1131 assert!(t.join().unwrap().starts_with("HTTP/1.1 200"));
1132 let h = m.shutdown_handle();
1133 let h2 = h.clone();
1134 let w = std::thread::spawn(move || h2.wait());
1135 h.shutdown();
1136 w.join().unwrap();
1137 assert!(h.is_shutdown());
1138 }
1139}