1use std::io::{Read, Write};
10use std::net::{TcpListener, TcpStream};
11use std::rc::Rc;
12use std::time::Duration;
13
14use crate::error::{DogeError, DogeResult};
15use crate::ordered_map::OrderedMap;
16use crate::stdlib::{bytes_arg, int_arg, str_arg};
17use crate::value::{SocketData, SocketState, Value};
18
19const HTTP_TIMEOUT: Duration = Duration::from_secs(30);
22
23const HTTP_METHODS: &[&str] = &["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
26
27const LINE_CHUNK: usize = 1024;
29
30fn socket_arg<'a>(fname: &str, v: &'a Value) -> DogeResult<&'a Rc<SocketData>> {
33 match v {
34 Value::Socket(s) => Ok(s),
35 _ => Err(DogeError::type_error(format!(
36 "howl.{fname} needs a Socket, got {}",
37 v.describe()
38 ))),
39 }
40}
41
42fn host_port<'a>(fname: &str, host: &'a Value, port: &Value) -> DogeResult<(&'a str, u16)> {
45 let host = str_arg("howl", fname, host)?;
46 let port = int_arg("howl", fname, port)?;
47 let port = u16::try_from(port).map_err(|_| {
48 DogeError::value_error(format!("a port must be between 0 and 65535, got {port}"))
49 })?;
50 Ok((host, port))
51}
52
53pub fn howl_listen(host: &Value, port: &Value) -> DogeResult {
57 let (host, port) = host_port("listen", host, port)?;
58 match TcpListener::bind((host, port)) {
59 Ok(listener) => Ok(Value::socket(SocketState::Listener(listener))),
60 Err(err) => Err(DogeError::io_error(format!(
61 "cannot listen on {host}:{port}: {err}"
62 ))),
63 }
64}
65
66pub fn howl_connect(host: &Value, port: &Value) -> DogeResult {
69 let (host, port) = host_port("connect", host, port)?;
70 match TcpStream::connect((host, port)) {
71 Ok(stream) => Ok(Value::socket(SocketState::Conn {
72 stream,
73 buf: Vec::new(),
74 })),
75 Err(err) => Err(DogeError::io_error(format!(
76 "cannot connect to {host}:{port}: {err}"
77 ))),
78 }
79}
80
81pub fn howl_accept(listener: &Value) -> DogeResult {
85 let sock = socket_arg("accept", listener)?;
86 let state = sock.state.borrow();
87 match &*state {
88 SocketState::Listener(l) => match l.accept() {
89 Ok((stream, _)) => Ok(Value::socket(SocketState::Conn {
90 stream,
91 buf: Vec::new(),
92 })),
93 Err(err) => Err(DogeError::io_error(format!("cannot accept: {err}"))),
94 },
95 SocketState::Conn { .. } => Err(DogeError::type_error(
96 "howl.accept needs a listening socket, not a connection",
97 )),
98 SocketState::Closed => Err(closed()),
99 }
100}
101
102pub fn howl_port(sock: &Value) -> DogeResult {
105 let sock = socket_arg("port", sock)?;
106 let state = sock.state.borrow();
107 let addr = match &*state {
108 SocketState::Listener(l) => l.local_addr(),
109 SocketState::Conn { stream, .. } => stream.local_addr(),
110 SocketState::Closed => return Err(closed()),
111 };
112 match addr {
113 Ok(addr) => Ok(Value::int(addr.port())),
114 Err(err) => Err(DogeError::io_error(format!("cannot read the port: {err}"))),
115 }
116}
117
118pub fn howl_send(conn: &Value, text: &Value) -> DogeResult {
121 let sock = socket_arg("send", conn)?;
122 let text = str_arg("howl", "send", text)?;
123 let mut state = sock.state.borrow_mut();
124 match &mut *state {
125 SocketState::Conn { stream, .. } => match stream.write_all(text.as_bytes()) {
126 Ok(()) => Ok(Value::None),
127 Err(err) => Err(DogeError::io_error(format!("cannot send: {err}"))),
128 },
129 SocketState::Listener(_) => Err(DogeError::type_error(
130 "howl.send needs a connection, not a listening socket",
131 )),
132 SocketState::Closed => Err(closed()),
133 }
134}
135
136pub fn howl_send_bytes(conn: &Value, bytes: &Value) -> DogeResult {
141 let sock = socket_arg("send_bytes", conn)?;
142 let bytes = bytes_arg("howl", "send_bytes", bytes)?;
143 let mut state = sock.state.borrow_mut();
144 match &mut *state {
145 SocketState::Conn { stream, .. } => match stream.write_all(bytes) {
146 Ok(()) => Ok(Value::None),
147 Err(err) => Err(DogeError::io_error(format!("cannot send: {err}"))),
148 },
149 SocketState::Listener(_) => Err(DogeError::type_error(
150 "howl.send_bytes needs a connection, not a listening socket",
151 )),
152 SocketState::Closed => Err(closed()),
153 }
154}
155
156pub fn howl_recv(conn: &Value, max_bytes: &Value) -> DogeResult {
162 let sock = socket_arg("recv", conn)?;
163 let max = int_arg("howl", "recv", max_bytes)?;
164 if max <= 0 {
165 return Err(DogeError::value_error(format!(
166 "howl.recv needs a positive byte count, got {max}"
167 )));
168 }
169 let max = max as usize;
170 let mut state = sock.state.borrow_mut();
171 match &mut *state {
172 SocketState::Conn { stream, buf } => {
173 loop {
177 let valid_len = match std::str::from_utf8(buf) {
178 Ok(_) => buf.len(),
179 Err(e) => {
180 if e.error_len().is_some() {
181 return Err(DogeError::io_error("received bytes were not valid text"));
182 }
183 e.valid_up_to()
184 }
185 };
186 if valid_len > 0 {
187 let bytes: Vec<u8> = buf.drain(..valid_len).collect();
188 let text = String::from_utf8(bytes)
189 .expect("compiler bug: valid_up_to bytes are valid UTF-8");
190 return Ok(Value::str(text));
191 }
192 let mut chunk = vec![0u8; max];
193 match stream.read(&mut chunk) {
194 Ok(0) => {
195 if buf.is_empty() {
196 return Ok(Value::None);
197 }
198 return Err(DogeError::io_error(
199 "connection closed in the middle of a character",
200 ));
201 }
202 Ok(got) => buf.extend_from_slice(&chunk[..got]),
203 Err(err) => return Err(DogeError::io_error(format!("cannot recv: {err}"))),
204 }
205 }
206 }
207 SocketState::Listener(_) => Err(DogeError::type_error(
208 "howl.recv needs a connection, not a listening socket",
209 )),
210 SocketState::Closed => Err(closed()),
211 }
212}
213
214pub fn howl_recv_bytes(conn: &Value, max_bytes: &Value) -> DogeResult {
222 let sock = socket_arg("recv_bytes", conn)?;
223 let max = int_arg("howl", "recv_bytes", max_bytes)?;
224 if max <= 0 {
225 return Err(DogeError::value_error(format!(
226 "howl.recv_bytes needs a positive byte count, got {max}"
227 )));
228 }
229 let max = max as usize;
230 let mut state = sock.state.borrow_mut();
231 match &mut *state {
232 SocketState::Conn { stream, buf } => {
233 if !buf.is_empty() {
234 let take = buf.len().min(max);
235 let bytes: Vec<u8> = buf.drain(..take).collect();
236 return Ok(Value::bytes(bytes));
237 }
238 let mut chunk = vec![0u8; max];
239 match stream.read(&mut chunk) {
240 Ok(0) => Ok(Value::None),
241 Ok(got) => Ok(Value::bytes(&chunk[..got])),
242 Err(err) => Err(DogeError::io_error(format!("cannot recv: {err}"))),
243 }
244 }
245 SocketState::Listener(_) => Err(DogeError::type_error(
246 "howl.recv_bytes needs a connection, not a listening socket",
247 )),
248 SocketState::Closed => Err(closed()),
249 }
250}
251
252pub fn howl_recv_line(conn: &Value) -> DogeResult {
256 let sock = socket_arg("recv_line", conn)?;
257 let mut state = sock.state.borrow_mut();
258 match &mut *state {
259 SocketState::Conn { stream, buf } => loop {
260 if let Some(pos) = buf.iter().position(|&b| b == b'\n') {
261 let mut line: Vec<u8> = buf.drain(..=pos).collect();
262 line.pop(); if line.last() == Some(&b'\r') {
264 line.pop();
265 }
266 return line_to_value(line);
267 }
268 let mut chunk = [0u8; LINE_CHUNK];
269 match stream.read(&mut chunk) {
270 Ok(0) => {
271 if buf.is_empty() {
272 return Ok(Value::None);
273 }
274 return line_to_value(std::mem::take(buf));
275 }
276 Ok(got) => buf.extend_from_slice(&chunk[..got]),
277 Err(err) => return Err(DogeError::io_error(format!("cannot recv: {err}"))),
278 }
279 },
280 SocketState::Listener(_) => Err(DogeError::type_error(
281 "howl.recv_line needs a connection, not a listening socket",
282 )),
283 SocketState::Closed => Err(closed()),
284 }
285}
286
287pub fn howl_close(sock: &Value) -> DogeResult {
291 let sock = socket_arg("close", sock)?;
292 *sock.state.borrow_mut() = SocketState::Closed;
293 Ok(Value::None)
294}
295
296pub fn howl_get(url: &Value) -> DogeResult {
300 let url = str_arg("howl", "get", url)?;
301 request_result(url, agent().get(url).call())
302}
303
304pub fn howl_post(url: &Value, body: &Value) -> DogeResult {
307 let url = str_arg("howl", "post", url)?;
308 let body = str_arg("howl", "post", body)?;
309 request_result(
310 url,
311 agent()
312 .post(url)
313 .set("Content-Type", "text/plain; charset=utf-8")
314 .send_string(body),
315 )
316}
317
318pub fn howl_request(method: &Value, url: &Value, opts: &Value) -> DogeResult {
324 let method = str_arg("howl", "request", method)?.to_ascii_uppercase();
325 if !HTTP_METHODS.contains(&method.as_str()) {
326 return Err(DogeError::value_error(format!(
327 "unknown HTTP method {method:?}, expected one of {}",
328 HTTP_METHODS.join(", ")
329 )));
330 }
331 let url = str_arg("howl", "request", url)?;
332
333 let opts = opts_dict(opts)?;
334 let mut request = agent().request(&method, url);
335 let mut body: Option<Value> = None;
336 if let Some(opts) = &opts {
337 let opts = opts.borrow();
338 for (key, value) in opts.iter() {
339 match key.as_str() {
340 "headers" => {
341 for (name, header) in headers_dict(value)?.borrow().iter() {
342 let header = str_arg("howl", "request", header).map_err(|_| {
343 DogeError::type_error(format!(
344 "howl.request header {name:?} needs a Str value, got {}",
345 header.describe()
346 ))
347 })?;
348 request = request.set(name, header);
349 }
350 }
351 "body" => body = Some(value.clone()),
352 other => {
353 return Err(DogeError::value_error(format!(
354 "howl.request got an unknown option {other:?}, expected \"headers\" or \"body\""
355 )));
356 }
357 }
358 }
359 }
360
361 let sent = match &body {
362 None | Some(Value::None) => request.call(),
363 Some(Value::Str(text)) => request.send_string(text),
364 Some(Value::Bytes(bytes)) => request.send_bytes(bytes),
365 Some(other) => {
366 return Err(DogeError::type_error(format!(
367 "howl.request body needs a Str or Bytes, got {}",
368 other.describe()
369 )));
370 }
371 };
372 request_result(url, sent)
373}
374
375fn opts_dict(opts: &Value) -> DogeResult<Option<Rc<std::cell::RefCell<OrderedMap>>>> {
378 match opts {
379 Value::None => Ok(None),
380 Value::Dict(entries) => Ok(Some(Rc::clone(entries))),
381 _ => Err(DogeError::type_error(format!(
382 "howl.request options need a Dict, got {}",
383 opts.describe()
384 ))),
385 }
386}
387
388fn headers_dict(value: &Value) -> DogeResult<&Rc<std::cell::RefCell<OrderedMap>>> {
390 match value {
391 Value::Dict(entries) => Ok(entries),
392 _ => Err(DogeError::type_error(format!(
393 "howl.request headers need a Dict, got {}",
394 value.describe()
395 ))),
396 }
397}
398
399fn agent() -> ureq::Agent {
401 ureq::AgentBuilder::new().timeout(HTTP_TIMEOUT).build()
402}
403
404fn request_result(url: &str, result: Result<ureq::Response, ureq::Error>) -> DogeResult {
408 match result {
409 Ok(response) => response_dict(response),
410 Err(ureq::Error::Status(_, response)) => response_dict(response),
411 Err(ureq::Error::Transport(transport)) => Err(DogeError::io_error(format!(
412 "cannot fetch {url}: {}",
413 transport.message().unwrap_or("the request failed")
414 ))),
415 }
416}
417
418fn response_dict(response: ureq::Response) -> DogeResult {
422 let status = response.status() as i64;
423 let mut headers = OrderedMap::new();
424 for name in response.headers_names() {
425 let value = response.header(&name).unwrap_or("");
426 headers.insert(name.to_ascii_lowercase(), Value::str(value));
427 }
428 let body = response
429 .into_string()
430 .map_err(|err| DogeError::io_error(format!("cannot read the response: {err}")))?;
431 let mut entries = OrderedMap::new();
432 entries.insert("status".to_string(), Value::int(status));
433 entries.insert("body".to_string(), Value::str(body));
434 entries.insert("headers".to_string(), Value::dict(headers));
435 Ok(Value::dict(entries))
436}
437
438fn closed() -> DogeError {
440 DogeError::io_error("socket is closed")
441}
442
443fn line_to_value(bytes: Vec<u8>) -> DogeResult {
446 String::from_utf8(bytes)
447 .map(Value::str)
448 .map_err(|_| DogeError::io_error("received bytes were not valid text"))
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use crate::error::ErrorKind;
455 use bigdecimal::ToPrimitive;
456 use std::thread;
457
458 fn loopback() -> (Value, u16) {
460 let listener = howl_listen(&Value::str("127.0.0.1"), &Value::int(0)).unwrap();
461 let port = match howl_port(&listener).unwrap() {
462 Value::Int(p) => p.to_u16().unwrap(),
463 _ => panic!("port is an Int"),
464 };
465 (listener, port)
466 }
467
468 fn recv_str(conn: &Value, n: i64) -> Option<String> {
469 match howl_recv(conn, &Value::int(n)).unwrap() {
470 Value::Str(s) => Some(s.to_string()),
471 Value::None => None,
472 other => panic!("recv gave {}", other.type_name()),
473 }
474 }
475
476 fn recv_bytes(conn: &Value, n: i64) -> Option<Vec<u8>> {
477 match howl_recv_bytes(conn, &Value::int(n)).unwrap() {
478 Value::Bytes(b) => Some(b.to_vec()),
479 Value::None => None,
480 other => panic!("recv_bytes gave {}", other.type_name()),
481 }
482 }
483
484 #[test]
485 fn tcp_round_trip_and_close() {
486 let (listener, port) = loopback();
487 let client = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap();
490 let server = howl_accept(&listener).unwrap();
491
492 howl_send(&client, &Value::str("much hello\n")).unwrap();
493 assert_eq!(howl_recv_line(&server).unwrap().to_string(), "much hello");
494
495 howl_send(&server, &Value::str("wow\n")).unwrap();
496 assert_eq!(howl_recv_line(&client).unwrap().to_string(), "wow");
497
498 howl_close(&server).unwrap();
500 assert!(matches!(howl_recv_line(&client).unwrap(), Value::None));
501
502 assert_eq!(
504 howl_send(&server, &Value::str("x")).unwrap_err().kind,
505 ErrorKind::IOError
506 );
507 howl_close(&server).unwrap();
509 }
510
511 #[test]
512 fn recv_reassembles_a_split_multibyte_character() {
513 let (listener, port) = loopback();
514 let client = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap();
515 let server = howl_accept(&listener).unwrap();
516
517 howl_send(&client, &Value::str("é")).unwrap();
520 assert_eq!(recv_str(&server, 1).as_deref(), Some("é"));
521 }
522
523 #[test]
524 fn recv_reports_eof_as_none() {
525 let (listener, port) = loopback();
526 let client = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap();
527 let server = howl_accept(&listener).unwrap();
528 howl_close(&client).unwrap();
529 assert_eq!(recv_str(&server, 16), None);
530 }
531
532 #[test]
533 fn bytes_round_trip_preserves_non_text_data() {
534 let (listener, port) = loopback();
535 let client = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap();
536 let server = howl_accept(&listener).unwrap();
537
538 let payload = vec![0xffu8, 0x00, 0x50, 0x44, 0x46];
541 howl_send_bytes(&client, &Value::bytes(&payload)).unwrap();
542 assert_eq!(recv_bytes(&server, 16).as_deref(), Some(&payload[..]));
543 }
544
545 #[test]
546 fn recv_bytes_reports_eof_as_none() {
547 let (listener, port) = loopback();
548 let client = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap();
549 let server = howl_accept(&listener).unwrap();
550 howl_close(&client).unwrap();
551 assert_eq!(recv_bytes(&server, 16), None);
552 }
553
554 #[test]
555 fn recv_bytes_drains_buffer_left_by_recv_line() {
556 let (listener, port) = loopback();
557 let client = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap();
558 let server = howl_accept(&listener).unwrap();
559
560 howl_send(&client, &Value::str("Head: v\r\nBODY")).unwrap();
564 assert_eq!(howl_recv_line(&server).unwrap().to_string(), "Head: v");
565 assert_eq!(recv_bytes(&server, 4).as_deref(), Some(&b"BODY"[..]));
566 }
567
568 #[test]
569 fn recv_bytes_and_send_bytes_reject_bad_args() {
570 let (listener, port) = loopback();
571 let client = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap();
572 assert_eq!(
574 howl_send_bytes(&listener, &Value::bytes(b"x"))
575 .unwrap_err()
576 .kind,
577 ErrorKind::TypeError
578 );
579 assert_eq!(
580 howl_send_bytes(&client, &Value::str("x")).unwrap_err().kind,
581 ErrorKind::TypeError
582 );
583 assert_eq!(
585 howl_recv_bytes(&client, &Value::int(0)).unwrap_err().kind,
586 ErrorKind::ValueError
587 );
588 }
589
590 #[test]
591 fn wrong_socket_role_and_types_are_catchable() {
592 let (listener, port) = loopback();
593 let client = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap();
594 assert_eq!(howl_accept(&client).unwrap_err().kind, ErrorKind::TypeError);
596 assert_eq!(
597 howl_send(&listener, &Value::str("x")).unwrap_err().kind,
598 ErrorKind::TypeError
599 );
600 assert_eq!(
602 howl_connect(&Value::int(1), &Value::int(port as i64))
603 .unwrap_err()
604 .kind,
605 ErrorKind::TypeError
606 );
607 assert_eq!(
608 howl_send(&Value::int(1), &Value::str("x"))
609 .unwrap_err()
610 .kind,
611 ErrorKind::TypeError
612 );
613 assert_eq!(
614 howl_recv(&client, &Value::int(0)).unwrap_err().kind,
615 ErrorKind::ValueError
616 );
617 assert_eq!(
618 howl_listen(&Value::str("127.0.0.1"), &Value::int(99999))
619 .unwrap_err()
620 .kind,
621 ErrorKind::ValueError
622 );
623 }
624
625 #[test]
626 fn connection_refused_is_a_catchable_io_error() {
627 let port = {
629 let (listener, port) = loopback();
630 howl_close(&listener).unwrap();
631 port
632 };
633 let err = howl_connect(&Value::str("127.0.0.1"), &Value::int(port as i64)).unwrap_err();
634 assert_eq!(err.kind, ErrorKind::IOError);
635 }
636
637 #[test]
638 fn http_get_returns_status_and_body() {
639 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
642 let port = listener.local_addr().unwrap().port();
643 let handle = thread::spawn(move || {
644 if let Ok((mut stream, _)) = listener.accept() {
645 let mut buf = [0u8; 1024];
646 let _ = stream.read(&mut buf);
647 let body = "much not found";
648 let response = format!(
649 "HTTP/1.1 404 Not Found\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
650 body.len(),
651 body
652 );
653 let _ = stream.write_all(response.as_bytes());
654 }
655 });
656
657 let url = Value::str(format!("http://127.0.0.1:{port}/"));
658 let result = howl_get(&url).unwrap();
659 handle.join().unwrap();
660
661 match result {
662 Value::Dict(entries) => {
663 let entries = entries.borrow();
664 assert!(entries
665 .get("status")
666 .is_some_and(|v| crate::values_equal(v, &Value::int(404))));
667 assert!(
668 matches!(entries.get("body"), Some(Value::Str(s)) if &**s == "much not found")
669 );
670 }
671 other => panic!("expected a Dict, got {}", other.type_name()),
672 }
673 }
674
675 #[test]
676 fn http_get_on_a_dead_port_is_a_catchable_io_error() {
677 let port = {
679 let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
680 l.local_addr().unwrap().port()
681 };
682 let err = howl_get(&Value::str(format!("http://127.0.0.1:{port}/"))).unwrap_err();
683 assert_eq!(err.kind, ErrorKind::IOError);
684 }
685
686 fn capture_server() -> (u16, thread::JoinHandle<String>) {
690 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
691 let port = listener.local_addr().unwrap().port();
692 let handle = thread::spawn(move || {
693 let (mut stream, _) = listener.accept().unwrap();
694 let mut request = Vec::new();
695 let mut buf = [0u8; 1024];
696 loop {
697 let n = stream.read(&mut buf).unwrap();
698 if n == 0 {
699 break;
700 }
701 request.extend_from_slice(&buf[..n]);
702 if let Some(head_end) = find_subslice(&request, b"\r\n\r\n") {
703 let head = String::from_utf8_lossy(&request[..head_end]).to_lowercase();
704 let content_length = head
705 .split("\r\n")
706 .find_map(|line| line.strip_prefix("content-length:"))
707 .and_then(|v| v.trim().parse::<usize>().ok())
708 .unwrap_or(0);
709 if request.len() >= head_end + 4 + content_length {
710 break;
711 }
712 }
713 }
714 let body = "wow ok";
715 let response = format!(
716 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
717 body.len(),
718 body
719 );
720 let _ = stream.write_all(response.as_bytes());
721 String::from_utf8_lossy(&request).into_owned()
722 });
723 (port, handle)
724 }
725
726 fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
727 haystack.windows(needle.len()).position(|w| w == needle)
728 }
729
730 fn dict_of(pairs: &[(&str, Value)]) -> Value {
732 let mut entries = OrderedMap::new();
733 for (key, value) in pairs {
734 entries.insert((*key).to_string(), value.clone());
735 }
736 Value::dict(entries)
737 }
738
739 #[test]
740 fn http_request_sends_method_headers_and_body() {
741 let (port, handle) = capture_server();
742 let url = Value::str(format!("http://127.0.0.1:{port}/invoices"));
743 let opts = dict_of(&[
744 (
745 "headers",
746 dict_of(&[
747 ("Authorization", Value::str("Bearer secret")),
748 ("Content-Type", Value::str("application/json")),
749 ]),
750 ),
751 ("body", Value::str("{\"much\":\"json\"}")),
752 ]);
753 let result = howl_request(&Value::str("post"), &url, &opts).unwrap();
754 let request = handle.join().unwrap();
755
756 assert!(request.starts_with("POST /invoices HTTP/1.1\r\n"));
757 assert!(request.contains("Authorization: Bearer secret\r\n"));
758 assert!(request.contains("Content-Type: application/json\r\n"));
759 assert!(request.ends_with("{\"much\":\"json\"}"));
760
761 match result {
762 Value::Dict(entries) => {
763 let entries = entries.borrow();
764 assert!(entries
765 .get("status")
766 .is_some_and(|v| crate::values_equal(v, &Value::int(200))));
767 let headers = match entries.get("headers") {
768 Some(Value::Dict(h)) => h.borrow(),
769 other => panic!("expected a headers Dict, got {other:?}"),
770 };
771 assert!(matches!(
772 headers.get("content-type"),
773 Some(Value::Str(s)) if &**s == "application/json"
774 ));
775 }
776 other => panic!("expected a Dict, got {}", other.type_name()),
777 }
778 }
779
780 #[test]
781 fn http_request_sends_a_bytes_body_raw() {
782 let (port, handle) = capture_server();
783 let url = Value::str(format!("http://127.0.0.1:{port}/"));
784 let opts = dict_of(&[("body", Value::bytes([0x50, 0x44, 0x46]))]);
785 howl_request(&Value::str("PUT"), &url, &opts).unwrap();
786 let request = handle.join().unwrap();
787 assert!(request.starts_with("PUT / HTTP/1.1\r\n"));
788 assert!(request.ends_with("PDF"));
789 }
790
791 #[test]
792 fn http_request_without_options_is_a_bare_request() {
793 let (port, handle) = capture_server();
794 let url = Value::str(format!("http://127.0.0.1:{port}/"));
795 let result = howl_request(&Value::str("GET"), &url, &Value::None).unwrap();
797 let request = handle.join().unwrap();
798 assert!(request.starts_with("GET / HTTP/1.1\r\n"));
799 assert!(matches!(result, Value::Dict(_)));
800 }
801
802 #[test]
803 fn http_request_rejects_an_unknown_method() {
804 let err =
805 howl_request(&Value::str("FETCH"), &Value::str("http://x/"), &Value::None).unwrap_err();
806 assert_eq!(err.kind, ErrorKind::ValueError);
807 }
808
809 #[test]
810 fn http_request_rejects_an_unknown_option() {
811 let opts = dict_of(&[("query", Value::str("nope"))]);
812 let err = howl_request(&Value::str("GET"), &Value::str("http://x/"), &opts).unwrap_err();
813 assert_eq!(err.kind, ErrorKind::ValueError);
814 }
815
816 #[test]
817 fn http_request_rejects_a_non_text_body() {
818 let opts = dict_of(&[("body", Value::int(42))]);
819 let err = howl_request(&Value::str("POST"), &Value::str("http://x/"), &opts).unwrap_err();
820 assert_eq!(err.kind, ErrorKind::TypeError);
821 }
822
823 #[test]
824 fn http_request_rejects_a_non_str_header_value() {
825 let opts = dict_of(&[("headers", dict_of(&[("X-Count", Value::int(3))]))]);
826 let err = howl_request(&Value::str("GET"), &Value::str("http://x/"), &opts).unwrap_err();
827 assert_eq!(err.kind, ErrorKind::TypeError);
828 }
829
830 #[test]
831 fn http_get_response_includes_headers() {
832 let (port, handle) = capture_server();
833 let result = howl_get(&Value::str(format!("http://127.0.0.1:{port}/"))).unwrap();
834 handle.join().unwrap();
835 match result {
836 Value::Dict(entries) => {
837 let entries = entries.borrow();
838 let headers = match entries.get("headers") {
839 Some(Value::Dict(h)) => h.borrow(),
840 other => panic!("expected a headers Dict, got {other:?}"),
841 };
842 assert!(headers.get("content-type").is_some());
843 }
844 other => panic!("expected a Dict, got {}", other.type_name()),
845 }
846 }
847}