spire_core/testing/
site.rs1use std::collections::HashMap;
2use std::future::{Future, ready};
3use std::sync::{Arc, Mutex};
4
5use bytes::Bytes;
6use http::{StatusCode, Uri};
7
8use super::{MockPage, Visit};
9use crate::backend::{Backend, Client, StatusPolicy};
10use crate::frontier::seed::Seed;
11use crate::request::{Request, Response, ResponseExt};
12use crate::sync::lock;
13use crate::{Error, Result};
14
15const MAX_REDIRECTS: usize = 10;
17
18#[derive(Debug, Clone, Default)]
53pub struct MockSite {
54 pages: Arc<HashMap<Uri, MockPage>>,
55 status_policy: StatusPolicy,
56 requests: Arc<Mutex<Vec<Uri>>>,
57}
58
59impl MockSite {
60 #[must_use]
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 #[must_use]
72 pub fn page(mut self, uri: &str, page: MockPage) -> Self {
73 Arc::make_mut(&mut self.pages).insert(parse(uri), page);
74 self
75 }
76
77 #[must_use]
80 pub const fn with_status_policy(mut self, policy: StatusPolicy) -> Self {
81 self.status_policy = policy;
82 self
83 }
84
85 #[must_use]
89 pub fn requests(&self) -> Vec<Uri> {
90 lock(&self.requests).clone()
91 }
92
93 #[must_use]
95 pub fn visit(&self, seed: Seed) -> Visit {
96 Visit::new(self.clone(), seed)
97 }
98
99 fn serve(&self, uri: &Uri) -> Result<(MockPage, Uri)> {
102 let not_found = || MockPage::status(StatusCode::NOT_FOUND);
103 let mut uri = uri.clone();
104 for _ in 0..=MAX_REDIRECTS {
105 let page = self.pages.get(&uri).cloned().unwrap_or_else(not_found);
106 match page.location {
107 Some(location) => uri = location,
108 None => return Ok((page, uri)),
109 }
110 }
111 Err(Error::new(crate::ErrorKind::Http)
112 .with_message("too many redirects")
113 .with_uri(uri))
114 }
115}
116
117pub(super) fn parse(uri: &str) -> Uri {
119 match uri.parse::<Uri>() {
120 Ok(parsed) if parsed.scheme().is_some() && parsed.authority().is_some() => {
121 Seed::get(uri).map_or(parsed, |seed| seed.uri().clone())
123 }
124 _ => panic!("`{uri}` is not an absolute URI"),
125 }
126}
127
128impl Backend for MockSite {
129 type Client = MockClient;
130
131 fn connect(&self) -> impl Future<Output = Result<Self::Client>> + Send {
132 ready(Ok(MockClient {
133 site: self.clone(),
134 body: Mutex::new(None),
135 }))
136 }
137}
138
139#[derive(Debug)]
141pub struct MockClient {
142 site: MockSite,
143 body: Mutex<Option<Bytes>>,
144}
145
146impl MockClient {
147 async fn serve(&self, request: &Request) -> Result<Response<()>> {
148 lock(&self.site.requests).push(request.uri().clone());
149 let (page, served_from) = self.site.serve(request.uri())?;
150 if !page.latency.is_zero() {
151 tokio::time::sleep(page.latency).await;
152 }
153
154 let mut head = Response::new(());
155 *head.status_mut() = page.status;
156 *head.headers_mut() = page.headers;
157 head.set_final_uri(served_from);
158
159 let head = self.site.status_policy.check(head)?;
161 *lock(&self.body) = Some(page.body);
162 Ok(head)
163 }
164}
165
166impl Client for MockClient {
167 fn fetch(&self, request: &Request) -> impl Future<Output = Result<Response<()>>> + Send {
168 self.serve(request)
169 }
170
171 fn body(&self) -> impl Future<Output = Result<Bytes>> + Send {
172 let body = lock(&self.body).clone();
173 ready(body.ok_or_else(|| Error::worker("the request was not fetched yet")))
174 }
175}