Skip to main content

standard_plugin/testing/
http.rs

1//! Scripted HTTP for daemon plugins under test: `daemon::http` requests go
2//! to the [`MockHost`], which checks the `fetch:` grants exactly as
3//! `standardd` does, records every request it lets through, and answers
4//! each from the first queued response whose method and URL prefix match.
5
6use alloc::collections::VecDeque;
7use alloc::string::{String, ToString};
8use alloc::vec::Vec;
9
10use super::MockHost;
11use crate::daemon::http::{Request, Response, Target, too_long};
12use crate::error::{Error, Result};
13
14/// One queued answer: for requests of `method` (any when `*`) whose URL
15/// starts with `prefix`.
16#[derive(Clone, Debug)]
17struct Scripted {
18    method: String,
19    prefix: String,
20    answer: Result<Response>,
21}
22
23/// The HTTP state of a [`MockHost`].
24#[derive(Clone, Debug, Default)]
25pub(crate) struct MockHttp {
26    queue: VecDeque<Scripted>,
27    requests: Vec<Request>,
28}
29
30impl MockHost {
31    /// Queues `response` for the next request of `method` (`GET`, `POST`,
32    /// ...; `*` for any) whose URL starts with `url_prefix`. Each queued
33    /// answer is used once, in the order queued; a request nothing matches
34    /// fails with `Error::Unavailable`.
35    pub fn http(mut self, method: &str, url_prefix: &str, response: Response) -> Self {
36        self.queue_http(method, url_prefix, Ok(response));
37        self
38    }
39
40    /// Queues an answer, a failure included (`Error::Unavailable("timeout")`
41    /// is what a request past its timeout gets), on a running host:
42    /// `harness.host(|host| host.queue_http(...))`.
43    pub fn queue_http(&mut self, method: &str, url_prefix: &str, answer: Result<Response>) {
44        self.http.queue.push_back(Scripted {
45            method: method.to_ascii_uppercase(),
46            prefix: url_prefix.to_string(),
47            answer,
48        });
49    }
50
51    /// Every request the grants let through, in order, as the plugin built
52    /// it (method, URL, headers, body, timeout, body cap).
53    pub fn http_requests(&self) -> &[Request] {
54        &self.http.requests
55    }
56
57    /// Answers queued and not used yet.
58    pub fn http_pending(&self) -> usize {
59        self.http.queue.len()
60    }
61
62    fn send_http(&mut self, request: &Request, target: &Target<'_>) -> Result<Response> {
63        if !self
64            .grants
65            .allows_fetch(target.scheme, &target.host(), target.port())
66        {
67            let Error::GrantDenied { grant } = target.denied() else {
68                unreachable!("a fetch refusal names its grant")
69            };
70            self.denials.push(grant.clone());
71            return Err(Error::GrantDenied { grant });
72        }
73        self.http.requests.push(request.clone());
74        let method = request.method.to_ascii_uppercase();
75        let index = self.http.queue.iter().position(|scripted| {
76            (scripted.method == "*" || scripted.method == method)
77                && request.url.starts_with(&scripted.prefix)
78        });
79        let Some(scripted) = index.and_then(|index| self.http.queue.remove(index)) else {
80            return Err(Error::Unavailable(alloc::format!(
81                "no response queued for {method} {}",
82                request.url
83            )));
84        };
85        let response = scripted.answer?;
86        if response.body.len() > request.max_body {
87            return Err(too_long(request.max_body));
88        }
89        Ok(response)
90    }
91}
92
93/// `daemon::http`'s transport natively.
94pub(crate) fn send(request: &Request, target: &Target<'_>) -> Result<Response> {
95    super::with_host(|host| host.send_http(request, target))
96}