noxious_client/
client.rs

1use std::collections::HashMap;
2
3use crate::{
4    error::{ApiErrorResponse, ClientError},
5    proxy::Proxy,
6};
7use noxious::proxy::{ProxyConfig, ProxyWithToxics};
8use reqwest::{Client as HttpClient, Response, StatusCode};
9
10/// A client for Noxious and Toxiproxy
11/// It follows the same naming conventions for the methods.
12#[derive(Debug)]
13pub struct Client {
14    base_url: String,
15}
16
17// TODO: fix the error type
18pub type Result<T> = std::result::Result<T, ClientError>;
19
20impl Client {
21    /// Create a new client
22    ///
23    /// Panics if the given url starts with `https://`.
24    pub fn new(url: &str) -> Client {
25        if url.starts_with("https://") {
26            panic!("the toxiproxy client does not support https");
27        }
28        let base_url = if !url.starts_with("http://") {
29            format!("http://{}", url)
30        } else {
31            url.to_owned()
32        };
33        Client { base_url }
34    }
35
36    /// Returns a proxy by name, if it already exists
37    pub async fn proxy(&self, name: &str) -> Result<Proxy> {
38        let res = HttpClient::new()
39            .get(self.base_url.clone() + "/proxies/" + name)
40            .send()
41            .await?;
42        if res.status().is_success() {
43            Ok(Proxy::from_proxy_with_toxics(
44                &self.base_url,
45                res.json::<ProxyWithToxics>().await?,
46            ))
47        } else {
48            Err(get_error_body(res, StatusCode::OK).await)
49        }
50    }
51
52    /// Returns a map with all the proxies and their toxics
53    pub async fn proxies(&self) -> Result<HashMap<String, Proxy>> {
54        let res = HttpClient::new()
55            .get(self.base_url.clone() + "/proxies")
56            .send()
57            .await?;
58        if res.status().is_success() {
59            Ok(res
60                .json::<HashMap<String, ProxyWithToxics>>()
61                .await?
62                .into_iter()
63                .map(|(name, proxy)| (name, Proxy::from_proxy_with_toxics(&self.base_url, proxy)))
64                .collect())
65        } else {
66            Err(get_error_body(res, StatusCode::OK).await)
67        }
68    }
69
70    /// Instantiates a new proxy config, sends it to the server
71    /// The server starts listening on the specified address
72    pub async fn create_proxy(&self, name: &str, listen: &str, upstream: &str) -> Result<Proxy> {
73        let mut proxy = Proxy::new(&self.base_url, name, listen, upstream);
74        proxy.save().await?;
75        Ok(proxy)
76    }
77
78    /// Create a list of proxies using a configuration list. If a proxy already exists,
79    /// it will be replaced with the specified configuration.
80    pub async fn populate(&self, proxies: &[ProxyConfig]) -> Result<Vec<Proxy>> {
81        let res = HttpClient::new()
82            .post(self.base_url.clone() + "/populate")
83            .json(proxies)
84            .send()
85            .await?;
86        if res.status().is_success() {
87            Ok(res
88                .json::<Vec<ProxyWithToxics>>()
89                .await?
90                .into_iter()
91                .map(|item| Proxy::from_proxy_with_toxics(&self.base_url, item))
92                .collect::<Vec<Proxy>>())
93        } else {
94            Err(get_error_body(res, StatusCode::CREATED).await)
95        }
96    }
97
98    /// Resets the state of all proxies by removing all the toxic from all proxies
99    pub async fn reset_state(&self) -> Result<()> {
100        let res = HttpClient::new()
101            .post(self.base_url.clone() + "/reset")
102            .send()
103            .await?;
104        if res.status().is_success() {
105            Ok(())
106        } else {
107            Err(get_error_body(res, StatusCode::NO_CONTENT).await)
108        }
109    }
110}
111
112pub(crate) async fn get_error_body(res: Response, expected_status: StatusCode) -> ClientError {
113    let code = res.status();
114    if let Ok(api_error) = res.json::<ApiErrorResponse>().await {
115        ClientError::ApiError(api_error)
116    } else {
117        ClientError::UnexpectedStatusCode(code.into(), expected_status.into())
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    #[should_panic]
127    fn client_does_not_allow_https() {
128        let _client = Client::new("https://blahblah");
129    }
130
131    #[test]
132    fn client_adds_protocol() {
133        let client = Client::new("blahblah");
134        assert_eq!("http://blahblah", client.base_url);
135    }
136}