use std::collections::{HashMap, HashSet};
use std::future::{Future, ready};
use std::sync::{Arc, Mutex};
use ::http::{HeaderMap, StatusCode, Uri};
use bytes::Bytes;
use super::{MockPage, Visit};
use crate::backend::{Backend, Session, StatusPolicy};
use crate::frontier::seed::Seed;
use crate::http::{Request, Response, ResponseExt};
use crate::identity::Identity;
use crate::sync::lock;
use crate::{Error, Result};
const MAX_REDIRECTS: usize = 10;
#[derive(Debug, Clone, Default)]
pub struct MockSite {
pages: Arc<HashMap<Uri, MockPage>>,
status_policy: StatusPolicy,
unreachable: Arc<HashSet<String>>,
sent: Arc<Mutex<Vec<Sent>>>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Sent {
pub uri: Uri,
pub identity: Option<String>,
pub headers: HeaderMap,
}
impl MockSite {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn page(mut self, uri: &str, page: MockPage) -> Self {
Arc::make_mut(&mut self.pages).insert(parse(uri), page);
self
}
#[must_use]
pub const fn with_status_policy(mut self, policy: StatusPolicy) -> Self {
self.status_policy = policy;
self
}
#[must_use]
pub fn unreachable_through(mut self, name: impl Into<String>) -> Self {
Arc::make_mut(&mut self.unreachable).insert(name.into());
self
}
#[must_use]
pub fn requests(&self) -> Vec<Uri> {
lock(&self.sent)
.iter()
.map(|sent| sent.uri.clone())
.collect()
}
#[must_use]
pub fn sent(&self) -> Vec<Sent> {
lock(&self.sent).clone()
}
#[must_use]
pub fn visit(&self, seed: Seed) -> Visit {
Visit::new(self.clone(), seed)
}
fn serve(&self, uri: &Uri) -> Result<(MockPage, Uri)> {
let not_found = || MockPage::status(StatusCode::NOT_FOUND);
let mut uri = uri.clone();
for _ in 0..=MAX_REDIRECTS {
let page = self.pages.get(&uri).cloned().unwrap_or_else(not_found);
match page.location {
Some(location) => uri = location,
None => return Ok((page, uri)),
}
}
Err(Error::new(crate::ErrorKind::Http)
.with_message("too many redirects")
.with_uri(uri))
}
}
fn not_modified(request: &Request, headers: &::http::HeaderMap) -> bool {
use ::http::header::{ETAG, IF_MODIFIED_SINCE, IF_NONE_MATCH, LAST_MODIFIED};
let asked = request.headers();
if let (Some(etag), Some(none_match)) = (headers.get(ETAG), asked.get(IF_NONE_MATCH)) {
let none_match = none_match.to_str().unwrap_or_default();
return none_match
.split(',')
.any(|tag| tag.trim() == "*" || tag.trim().as_bytes() == etag.as_bytes());
}
matches!(
(headers.get(LAST_MODIFIED), asked.get(IF_MODIFIED_SINCE)),
(Some(modified), Some(since)) if modified == since
)
}
pub(super) fn parse(uri: &str) -> Uri {
match uri.parse::<Uri>() {
Ok(parsed) if parsed.scheme().is_some() && parsed.authority().is_some() => {
Seed::get(uri).map_or(parsed, |seed| seed.uri().clone())
}
_ => panic!("`{uri}` is not an absolute URI"),
}
}
impl Backend for MockSite {
type Session = MockSession;
fn connect(&self) -> impl Future<Output = Result<Self::Session>> + Send {
ready(Ok(self.session(None)))
}
fn connect_as(
&self,
identity: &Identity,
) -> impl Future<Output = Result<Self::Session>> + Send {
ready(Ok(self.session(Some(identity.clone()))))
}
}
impl MockSite {
fn session(&self, identity: Option<Identity>) -> MockSession {
MockSession {
site: self.clone(),
identity,
}
}
}
#[derive(Debug)]
pub struct MockSession {
site: MockSite,
identity: Option<Identity>,
}
impl MockSession {
async fn serve(&self, request: &Request) -> Result<Response> {
let identity = self
.identity
.as_ref()
.map(|identity| identity.name().to_owned());
lock(&self.site.sent).push(Sent {
uri: request.uri().clone(),
identity: identity.clone(),
headers: request.headers().clone(),
});
if identity.is_some_and(|name| self.site.unreachable.contains(&name)) {
return Err(Error::new(crate::ErrorKind::Http)
.with_message("the proxy refused the connection")
.with_uri(request.uri().clone()));
}
let (page, served_from) = self.site.serve(request.uri())?;
if !page.latency.is_zero() {
tokio::time::sleep(page.latency).await;
}
let mut head = Response::new(());
*head.status_mut() = page.status;
let unchanged = page.status.is_success() && not_modified(request, &page.headers);
if unchanged {
*head.status_mut() = StatusCode::NOT_MODIFIED;
}
*head.headers_mut() = page.headers;
head.set_final_uri(served_from);
let head = self.site.status_policy.check(head)?;
Ok(head.map(|()| if unchanged { Bytes::new() } else { page.body }))
}
}
impl Session for MockSession {
fn fetch(&self, request: &Request) -> impl Future<Output = Result<Response>> + Send {
self.serve(request)
}
}