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, 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 cut_after(self, bytes: u64) -> Self {
167 self.with_header(CUT_AFTER_HEADER, &bytes.to_string())
168 }
169}
170
171pub const IGNORE_RANGE_HEADER: &str = "X-RightKit-Mock-Ignore-Range";
173pub const CUT_AFTER_HEADER: &str = "X-RightKit-Mock-Cut-After";
175
176#[derive(Debug, Clone, PartialEq, Eq)]
178enum BodyPlan {
179 Full,
180 Partial(u64, u64),
182 Unsatisfiable,
183}
184
185fn plan_range(range: Option<&str>, len: u64) -> Option<BodyPlan> {
188 let spec = range?.trim().strip_prefix("bytes=")?.trim();
189 if spec.contains(',') {
190 return None;
191 }
192 let (a, b) = spec.split_once('-')?;
193 let (a, b) = (a.trim(), b.trim());
194 let digits = |s: &str| !s.is_empty() && s.bytes().all(|c| c.is_ascii_digit());
195 if a.is_empty() {
196 if !digits(b) {
197 return None;
198 }
199 let n: u64 = b.parse().ok()?;
200 if n == 0 || len == 0 {
201 return Some(BodyPlan::Unsatisfiable);
202 }
203 return Some(BodyPlan::Partial(len.saturating_sub(n), len - 1));
204 }
205 if !digits(a) || !(b.is_empty() || digits(b)) {
206 return None;
207 }
208 let start: u64 = a.parse().ok()?;
209 let end = if b.is_empty() {
210 None
211 } else {
212 Some(b.parse::<u64>().ok()?)
213 };
214 if end.is_some_and(|e| e < start) {
215 return None;
216 }
217 if start >= len {
218 return Some(BodyPlan::Unsatisfiable);
219 }
220 Some(BodyPlan::Partial(
221 start,
222 end.map_or(len - 1, |e| e.min(len - 1)),
223 ))
224}
225
226fn if_range_holds(if_range: &str, headers: &[(String, String)]) -> bool {
228 let v = if_range.trim();
229 let get = |n: &str| {
230 headers
231 .iter()
232 .find(|(k, _)| k.eq_ignore_ascii_case(n))
233 .map(|(_, v)| v.trim())
234 };
235 if v.starts_with('"') || v.starts_with("W/") {
236 !v.starts_with("W/") && get("etag").is_some_and(|e| e == v && !e.starts_with("W/"))
237 } else {
238 get("last-modified").is_some_and(|m| m == v)
239 }
240}
241
242impl From<Option<Reply>> for Reply {
244 fn from(r: Option<Reply>) -> Self {
245 r.unwrap_or_else(Reply::unmocked)
246 }
247}
248
249type Handler = dyn Fn(&Request, &str) -> Reply + Send + Sync;
250
251pub struct MockServer {
252 pub base: String,
253 addr: SocketAddr,
254 seen: Arc<Mutex<Vec<Seen>>>,
255 stop: Arc<AtomicBool>,
256}
257
258impl MockServer {
259 pub fn start(routes: Vec<Route>) -> Result<Self> {
261 Self::start_with(move |req: &Request, _base: &str| {
262 routes
263 .iter()
264 .find(|r| {
265 r.path == req.route()
266 && r.method
267 .as_deref()
268 .map(|m| m.eq_ignore_ascii_case(&req.method))
269 .unwrap_or(true)
270 })
271 .map(|r| Reply {
272 status: r.status,
273 content_type: r
274 .content_type
275 .clone()
276 .unwrap_or_else(|| "application/json".into()),
277 headers: vec![],
278 body: r.body.to_string().into_bytes(),
279 })
280 })
281 }
282
283 pub fn start_static(routes: Vec<(Option<String>, String, Reply)>) -> Result<Self> {
287 Self::start_with(move |req: &Request, _base: &str| {
288 routes
289 .iter()
290 .find(|(m, p, _)| {
291 *p == req.route()
292 && m.as_deref()
293 .is_none_or(|m| m.eq_ignore_ascii_case(&req.method))
294 })
295 .map(|(_, _, r)| r.clone())
296 })
297 }
298
299 pub fn start_with<R: Into<Reply>>(
304 handler: impl Fn(&Request, &str) -> R + Send + Sync + 'static,
305 ) -> Result<Self> {
306 let listener = TcpListener::bind(("127.0.0.1", 0))?;
307 let addr = listener.local_addr()?;
308 let base = format!("http://{addr}");
309 let seen = Arc::new(Mutex::new(Vec::new()));
310 let stop = Arc::new(AtomicBool::new(false));
311 let handler: Arc<Handler> = Arc::new(move |r: &Request, b: &str| handler(r, b).into());
312 let (s2, st2, b2) = (seen.clone(), stop.clone(), base.clone());
313 std::thread::spawn(move || {
314 for conn in listener.incoming().flatten() {
315 if st2.load(Ordering::SeqCst) {
316 break;
317 }
318 let (s3, h, b) = (s2.clone(), handler.clone(), b2.clone());
319 std::thread::spawn(move || {
320 let _ = serve(conn, &s3, &b, &*h);
321 });
322 }
323 });
324 Ok(Self {
325 base,
326 addr,
327 seen,
328 stop,
329 })
330 }
331
332 pub fn port(&self) -> u16 {
333 self.addr.port()
334 }
335 pub fn url(&self, path: &str) -> String {
337 format!("{}{path}", self.base)
338 }
339 pub fn seen(&self) -> Vec<Seen> {
340 self.seen.lock().unwrap().clone()
341 }
342 pub fn close(&self) {
343 self.stop.store(true, Ordering::SeqCst);
344 let _ = TcpStream::connect_timeout(&self.addr, Duration::from_millis(200));
345 }
346}
347
348impl Drop for MockServer {
349 fn drop(&mut self) {
350 self.close();
351 }
352}
353
354fn serve(
355 mut s: TcpStream,
356 seen: &Mutex<Vec<Seen>>,
357 base: &str,
358 handler: &Handler,
359) -> std::io::Result<()> {
360 s.set_read_timeout(Some(Duration::from_secs(10)))?;
361 let mut buf = Vec::new();
362 let mut chunk = [0u8; 16384];
363 let (head_end, content_len) = loop {
364 let n = s.read(&mut chunk)?;
365 if n == 0 {
366 return Ok(());
367 }
368 buf.extend_from_slice(&chunk[..n]);
369 if let Some(p) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
370 let head = String::from_utf8_lossy(&buf[..p]).to_ascii_lowercase();
371 let len = head
372 .lines()
373 .find_map(|l| {
374 l.strip_prefix("content-length:")
375 .and_then(|v| v.trim().parse::<usize>().ok())
376 })
377 .unwrap_or(0);
378 break (p + 4, len);
379 }
380 };
381 while buf.len() < head_end + content_len {
382 let n = s.read(&mut chunk)?;
383 if n == 0 {
384 break;
385 }
386 buf.extend_from_slice(&chunk[..n]);
387 }
388 let head = String::from_utf8_lossy(&buf[..head_end]).to_string();
389 let mut lines = head.lines();
390 let mut first = lines.next().unwrap_or("").split_whitespace();
391 let (method, path) = (
392 first.next().unwrap_or("").to_string(),
393 first.next().unwrap_or("").to_string(),
394 );
395 let headers = lines
396 .filter_map(|l| l.split_once(':'))
397 .map(|(k, v)| (k.trim().to_string(), v.trim().to_string()))
398 .collect();
399 let body_bytes = buf[head_end..(head_end + content_len).min(buf.len())].to_vec();
400 let req = Seen {
401 method,
402 path,
403 headers,
404 body: String::from_utf8_lossy(&body_bytes).into_owned(),
405 body_bytes,
406 };
407 seen.lock().unwrap().push(req.clone());
408 let reply = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler(&req, base)))
409 .unwrap_or_else(|_| Reply::status(500, json!({"error": "mock handler panicked"})));
410 write_reply(&mut s, &req, reply)
411}
412
413fn write_reply(s: &mut TcpStream, req: &Request, mut reply: Reply) -> std::io::Result<()> {
414 let take = |reply: &mut Reply, name: &str| -> Option<String> {
415 let i = reply
416 .headers
417 .iter()
418 .position(|(k, _)| k.eq_ignore_ascii_case(name))?;
419 Some(reply.headers.remove(i).1)
420 };
421 let ignore_range = take(&mut reply, IGNORE_RANGE_HEADER).is_some();
422 let cut_after = take(&mut reply, CUT_AFTER_HEADER).and_then(|v| v.trim().parse::<u64>().ok());
423 let len = reply.body.len() as u64;
424 let ranged_method =
425 req.method.eq_ignore_ascii_case("GET") || req.method.eq_ignore_ascii_case("HEAD");
426 let plan = if reply.status == 200 && ranged_method && !ignore_range {
427 let honoured = req
428 .header("if-range")
429 .is_none_or(|v| if_range_holds(v, &reply.headers));
430 honoured
431 .then(|| plan_range(req.header("range"), len))
432 .flatten()
433 .unwrap_or(BodyPlan::Full)
434 } else {
435 BodyPlan::Full
436 };
437 let (status, reason, body, extra): (u16, &str, &[u8], Option<String>) = match plan {
438 BodyPlan::Full => (reply.status, "X", &reply.body[..], None),
439 BodyPlan::Partial(a, b) => (
440 206,
441 "Partial Content",
442 &reply.body[a as usize..=b as usize],
443 Some(format!(
444 "Content-Range: bytes {a}-{b}/{len}\r\nAccept-Ranges: bytes\r\n"
445 )),
446 ),
447 BodyPlan::Unsatisfiable => (
448 416,
449 "Range Not Satisfiable",
450 &[][..],
451 Some(format!(
452 "Content-Range: bytes */{len}\r\nAccept-Ranges: bytes\r\n"
453 )),
454 ),
455 };
456 let mut head = format!(
457 "HTTP/1.1 {status} {reason}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n",
458 reply.content_type,
459 body.len()
460 );
461 head.push_str(extra.as_deref().unwrap_or(""));
462 for (k, v) in &reply.headers {
463 head.push_str(&format!("{k}: {v}\r\n"));
464 }
465 head.push_str("\r\n");
466 s.write_all(head.as_bytes())?;
467 if !req.method.eq_ignore_ascii_case("HEAD") {
468 let n = cut_after.map_or(body.len(), |c| (c as usize).min(body.len()));
469 s.write_all(&body[..n])?;
470 if n < body.len() {
471 s.flush()?;
472 return s.shutdown(std::net::Shutdown::Both);
473 }
474 }
475 s.flush()
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use crate::http::request;
482 use std::sync::atomic::AtomicUsize;
483
484 fn get(m: &MockServer, method: &str, path: &str, body: Option<&str>) -> crate::http::Response {
485 request(
486 m.addr,
487 method,
488 path,
489 Some("Bearer k"),
490 body,
491 Duration::from_secs(5),
492 )
493 .unwrap()
494 }
495
496 #[test]
497 fn static_routes_still_answer_json_and_404() {
498 let m = MockServer::start(vec![Route {
499 method: Some("POST".into()),
500 path: "/v1/gen".into(),
501 status: 201,
502 body: json!({"id": "j1"}),
503 content_type: None,
504 }])
505 .unwrap();
506 let r = get(&m, "POST", "/v1/gen?x=1", Some(r#"{"prompt":"p"}"#));
507 assert_eq!(r.status, 201);
508 assert_eq!(r.content_type, "application/json");
509 assert_eq!(
510 serde_json::from_slice::<Value>(&r.body).unwrap()["id"],
511 "j1"
512 );
513 assert_eq!(get(&m, "GET", "/v1/gen", None).status, 404);
514 let seen = m.seen();
515 assert_eq!(seen.len(), 2);
516 assert_eq!(seen[0].json()["prompt"], "p");
517 assert_eq!(seen[0].header("authorization"), Some("Bearer k"));
518 assert_eq!(seen[0].query_param("x"), Some("1"));
519 assert_eq!(seen[0].to_value()["json"]["prompt"], "p");
520 }
521
522 #[test]
523 fn dynamic_handler_is_stateful_binary_and_knows_its_base() {
524 let png: Vec<u8> = (0..=255u8).cycle().take(70_000).collect();
525 let polls = Arc::new(AtomicUsize::new(0));
526 let (p2, png2) = (polls.clone(), png.clone());
527 let m = MockServer::start_with(move |req: &Request, base: &str| {
528 if req.is("POST", "/jobs") {
529 Some(Reply::json(json!({"poll": format!("{base}/jobs/1")})))
530 } else if req.is("GET", "/jobs/1") {
531 let n = p2.fetch_add(1, Ordering::SeqCst);
532 Some(if n < 2 {
533 Reply::json(json!({"status": "running"}))
534 } else {
535 Reply::json(json!({"status": "done", "url": format!("{base}/out.png")}))
536 })
537 } else if req.is("GET", "/out.png") {
538 Some(Reply::bytes("image/png", png2.clone()).with_header("X-Mock", "1"))
539 } else if req.is("GET", "/boom") {
540 panic!("handler bug")
541 } else {
542 None
543 }
544 })
545 .unwrap();
546 let first: Value =
547 serde_json::from_slice(&get(&m, "POST", "/jobs", Some("{}")).body).unwrap();
548 assert_eq!(first["poll"], m.url("/jobs/1"));
549 let states: Vec<String> = (0..3)
550 .map(|_| {
551 serde_json::from_slice::<Value>(&get(&m, "GET", "/jobs/1", None).body).unwrap()
552 ["status"]
553 .as_str()
554 .unwrap()
555 .to_string()
556 })
557 .collect();
558 assert_eq!(states, ["running", "running", "done"]);
559 let media = get(&m, "GET", "/out.png", None);
560 assert_eq!(media.status, 200);
561 assert_eq!(media.content_type, "image/png");
562 assert_eq!(media.body, png, "binary body survives byte-for-byte");
563 assert_eq!(get(&m, "GET", "/boom", None).status, 500);
564 assert_eq!(get(&m, "GET", "/nope", None).status, 404);
565 assert_eq!(m.seen().len(), 7);
566 }
567
568 #[test]
569 fn a_held_connection_does_not_block_other_requests() {
570 let m = MockServer::start_with(|_: &Request, _: &str| Reply::text("ok")).unwrap();
571 let _idle = TcpStream::connect(m.addr).unwrap();
573 let started = std::time::Instant::now();
574 let r = get(&m, "GET", "/", None);
575 assert_eq!(r.text(), "ok");
576 assert!(started.elapsed() < Duration::from_secs(3));
577 }
578
579 fn raw(
581 m: &MockServer,
582 method: &str,
583 path: &str,
584 extra: &[(&str, &str)],
585 ) -> (u16, Vec<(String, String)>, Vec<u8>) {
586 let mut s = TcpStream::connect(m.addr).unwrap();
587 s.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
588 let mut head = format!("{method} {path} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n");
589 for (k, v) in extra {
590 head.push_str(&format!("{k}: {v}\r\n"));
591 }
592 head.push_str("\r\n");
593 s.write_all(head.as_bytes()).unwrap();
594 let mut buf = Vec::new();
595 let _ = s.read_to_end(&mut buf);
596 let split = buf.windows(4).position(|w| w == b"\r\n\r\n").unwrap();
597 let text = String::from_utf8_lossy(&buf[..split]).to_string();
598 let mut lines = text.lines();
599 let status = lines
600 .next()
601 .unwrap()
602 .split_whitespace()
603 .nth(1)
604 .unwrap()
605 .parse()
606 .unwrap();
607 let headers = lines
608 .filter_map(|l| l.split_once(':'))
609 .map(|(k, v)| (k.trim().to_ascii_lowercase(), v.trim().to_string()))
610 .collect();
611 (status, headers, buf[split + 4..].to_vec())
612 }
613
614 fn hdr<'a>(h: &'a [(String, String)], name: &str) -> Option<&'a str> {
615 h.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str())
616 }
617
618 fn data() -> Vec<u8> {
619 (0..1000u32).map(|i| (i * 7 % 251) as u8).collect()
620 }
621
622 #[test]
623 fn static_and_dynamic_binary_replies_serve_byte_ranges() {
624 let body = data();
625 let file =
626 Reply::bytes("application/octet-stream", body.clone()).with_header("ETag", "\"abc\"");
627 let st = MockServer::start_static(vec![
628 (Some("GET".into()), "/f.bin".into(), file.clone()),
629 (Some("HEAD".into()), "/f.bin".into(), file.clone()),
630 ])
631 .unwrap();
632 let f2 = file.clone();
633 let dy = MockServer::start_with(move |_: &Request, _: &str| f2.clone()).unwrap();
634 for m in [&st, &dy] {
635 let (s, h, b) = raw(m, "GET", "/f.bin", &[]);
636 assert_eq!((s, b.as_slice()), (200, &body[..]), "no Range: full 200");
637 assert_eq!(hdr(&h, "content-range"), None);
638
639 let (s, h, b) = raw(m, "GET", "/f.bin", &[("Range", "bytes=10-19")]);
640 assert_eq!(s, 206);
641 assert_eq!(b, &body[10..20]);
642 assert_eq!(hdr(&h, "content-range"), Some("bytes 10-19/1000"));
643 assert_eq!(hdr(&h, "content-length"), Some("10"));
644 assert_eq!(hdr(&h, "etag"), Some("\"abc\""));
645
646 let (s, h, b) = raw(m, "GET", "/f.bin", &[("Range", "bytes=990-")]);
647 assert_eq!((s, b.as_slice()), (206, &body[990..]));
648 assert_eq!(hdr(&h, "content-range"), Some("bytes 990-999/1000"));
649
650 let (s, h, b) = raw(m, "GET", "/f.bin", &[("Range", "bytes=-5")]);
651 assert_eq!((s, b.as_slice()), (206, &body[995..]));
652 assert_eq!(hdr(&h, "content-range"), Some("bytes 995-999/1000"));
653
654 let (s, h, b) = raw(m, "GET", "/f.bin", &[("Range", "bytes=-5000")]);
655 assert_eq!(
656 (s, b.as_slice()),
657 (206, &body[..]),
658 "suffix longer than the body"
659 );
660 assert_eq!(hdr(&h, "content-range"), Some("bytes 0-999/1000"));
661
662 let (s, h, b) = raw(m, "GET", "/f.bin", &[("Range", "bytes=995-5000")]);
663 assert_eq!((s, b.as_slice()), (206, &body[995..]), "end clamped");
664 assert_eq!(hdr(&h, "content-range"), Some("bytes 995-999/1000"));
665
666 for unsat in ["bytes=1000-", "bytes=5000-6000", "bytes=-0"] {
667 let (s, h, b) = raw(m, "GET", "/f.bin", &[("Range", unsat)]);
668 assert_eq!(s, 416, "{unsat}");
669 assert!(b.is_empty());
670 assert_eq!(hdr(&h, "content-range"), Some("bytes */1000"));
671 }
672 for ignored in ["bytes=5-1", "bytes=0-1,5-6", "items=0-1", "bytes=x-"] {
673 let (s, _, b) = raw(m, "GET", "/f.bin", &[("Range", ignored)]);
674 assert_eq!((s, b.as_slice()), (200, &body[..]), "{ignored}");
675 }
676 let (s, _, b) = raw(
678 m,
679 "GET",
680 "/f.bin",
681 &[("Range", "bytes=10-"), ("If-Range", "\"abc\"")],
682 );
683 assert_eq!((s, b.as_slice()), (206, &body[10..]));
684 let (s, _, b) = raw(
685 m,
686 "GET",
687 "/f.bin",
688 &[("Range", "bytes=10-"), ("If-Range", "\"old\"")],
689 );
690 assert_eq!((s, b.as_slice()), (200, &body[..]));
691 let (s, h, b) = raw(m, "HEAD", "/f.bin", &[("Range", "bytes=0-9")]);
693 assert_eq!(s, 206);
694 assert!(b.is_empty());
695 assert_eq!(hdr(&h, "content-length"), Some("10"));
696 }
697 assert_eq!(raw(&st, "POST", "/f.bin", &[]).0, 404, "method filter");
698 let err =
700 MockServer::start_with(|_: &Request, _: &str| Reply::text("nope").with_status(500))
701 .unwrap();
702 let (s, _, b) = raw(&err, "GET", "/", &[("Range", "bytes=0-1")]);
703 assert_eq!((s, b.as_slice()), (500, &b"nope"[..]));
704 }
705
706 #[test]
707 fn ignore_range_and_cut_after_simulate_no_resume_and_dropped_connections() {
708 let body = data();
709 let b2 = body.clone();
710 let m = MockServer::start_with(move |req: &Request, _: &str| {
711 let r = Reply::bytes("application/octet-stream", b2.clone());
712 match req.route() {
713 "/norange" => r.ignore_range(),
714 "/drop" => r.cut_after(100),
715 _ => r,
716 }
717 })
718 .unwrap();
719 let (s, h, b) = raw(&m, "GET", "/norange", &[("Range", "bytes=10-")]);
720 assert_eq!((s, b.as_slice()), (200, &body[..]));
721 assert_eq!(hdr(&h, "content-range"), None);
722 assert!(
723 h.iter().all(|(k, _)| !k.starts_with("x-rightkit-mock")),
724 "reserved headers are never sent: {h:?}"
725 );
726
727 let (s, h, b) = raw(&m, "GET", "/drop", &[]);
728 assert_eq!(s, 200);
729 assert_eq!(
730 hdr(&h, "content-length"),
731 Some("1000"),
732 "head promises the full body"
733 );
734 assert_eq!(b, &body[..100], "then the connection drops");
735 let (s, h, b) = raw(&m, "GET", "/drop", &[("Range", "bytes=500-")]);
736 assert_eq!(s, 206);
737 assert_eq!(hdr(&h, "content-length"), Some("500"));
738 assert_eq!(b, &body[500..600], "cut applies after range slicing");
739 let st =
741 MockServer::start_static(vec![(None, "/x".into(), Reply::text("hello").cut_after(2))])
742 .unwrap();
743 let (_, h, b) = raw(&st, "GET", "/x", &[]);
744 assert_eq!(b, b"he");
745 assert!(h.iter().all(|(k, _)| !k.starts_with("x-rightkit-mock")));
746 }
747}