freedom_api/
client.rs

1use bytes::Bytes;
2use freedom_config::Config;
3use reqwest::{Response, StatusCode};
4use url::Url;
5
6use crate::api::{Api, Inner, Value};
7
8/// An asynchronous `Client` for interfacing with the ATLAS freedom API.
9///
10/// The client is primarily defined based on it's [`Env`](crate::config::Env)
11/// and it's credentials (username and password).
12#[derive(Clone, Debug)]
13pub struct Client {
14    pub(crate) config: Config,
15    pub(crate) client: reqwest::Client,
16}
17
18impl PartialEq for Client {
19    fn eq(&self, other: &Self) -> bool {
20        self.config == other.config
21    }
22}
23
24impl Client {
25    /// Construct an API client from the provided Freedom config
26    ///
27    /// # Example
28    ///
29    /// ```
30    /// # use freedom_api::prelude::*;
31    /// let config = Config::builder()
32    ///     .environment(Test)
33    ///     .key("foo")
34    ///     .secret("bar")
35    ///     .build()
36    ///     .unwrap();
37    /// let client = Client::from_config(config);
38    ///
39    /// assert_eq!(client.config().key(), "foo");
40    /// ```
41    pub fn from_config(config: Config) -> Self {
42        Self {
43            config,
44            client: reqwest::Client::new(),
45        }
46    }
47
48    /// A convenience method for constructing an FPS client from environment variables.
49    ///
50    /// This function expects the following environment variables:
51    ///
52    /// + ATLAS_ENV: [possible values: test, prod]
53    /// + ATLAS_KEY: The ATLAS freedom key registered with an account
54    /// + ATLAS_SECRET: The ATLAS freedom secret registered with an account
55    pub fn from_env() -> Result<Self, freedom_config::Error> {
56        let config = Config::from_env()?;
57        Ok(Self::from_config(config))
58    }
59}
60
61impl Api for Client {
62    type Container<T: Value> = Inner<T>;
63
64    async fn get(&self, url: Url) -> Result<(Bytes, StatusCode), crate::error::Error> {
65        tracing::trace!("GET to {}", url);
66
67        let resp = self
68            .client
69            .get(url.clone())
70            .basic_auth(self.config.key(), Some(&self.config.expose_secret()))
71            .send()
72            .await?;
73
74        let status = resp.status();
75        let body = resp
76            .bytes()
77            .await
78            .inspect_err(|error| tracing::warn!(%url, %error, %status, "Failed to get response body"))
79            .inspect(|body| tracing::info!(%url, body = %String::from_utf8_lossy(body), %status, "Received response body"))?;
80
81        Ok((body, status))
82    }
83
84    async fn delete(&self, url: Url) -> Result<Response, crate::error::Error> {
85        tracing::trace!("DELETE to {}", url);
86
87        self.client
88            .delete(url.clone())
89            .basic_auth(self.config.key(), Some(self.config.expose_secret()))
90            .send()
91            .await
92            .inspect_err(|error| tracing::warn!(%error, %url, "Failed to DELETE"))
93            .inspect(|ok| tracing::warn!(?ok, %url, "Received response"))
94            .map_err(From::from)
95    }
96
97    async fn post<S>(&self, url: Url, msg: S) -> Result<Response, crate::error::Error>
98    where
99        S: serde::Serialize + Sync + Send,
100    {
101        tracing::trace!("POST to {}", url);
102
103        self.client
104            .post(url.clone())
105            .basic_auth(self.config.key(), Some(self.config.expose_secret()))
106            .json(&msg)
107            .send()
108            .await
109            .inspect_err(|error| tracing::warn!(%error, %url, "Failed to POST"))
110            .inspect(|ok| tracing::warn!(?ok, %url, "Received response"))
111            .map_err(From::from)
112    }
113
114    fn config(&self) -> &Config {
115        &self.config
116    }
117
118    fn config_mut(&mut self) -> &mut Config {
119        &mut self.config
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use freedom_config::Test;
126    use httpmock::{
127        Method::{GET, POST},
128        MockServer,
129    };
130
131    use crate::Container;
132
133    use super::*;
134
135    fn default_client() -> Client {
136        let config = Config::builder()
137            .environment(Test)
138            .key("foo")
139            .secret("bar")
140            .build()
141            .unwrap();
142
143        Client::from_config(config)
144    }
145
146    #[test]
147    fn clients_are_eq_based_on_config() {
148        let config = Config::builder()
149            .environment(Test)
150            .key("foo")
151            .secret("bar")
152            .build()
153            .unwrap();
154
155        let client_1 = Client::from_config(config.clone());
156        let client_2 = Client::from_config(config);
157        assert_eq!(client_1, client_2);
158    }
159
160    #[test]
161    fn wrap_and_unwrap_inner() {
162        let val = String::from("foobar");
163        let inner = Inner::new(val.clone());
164        assert_eq!(*inner, val);
165        let unwrapped = inner.into_inner();
166        assert_eq!(val, unwrapped);
167    }
168
169    #[test]
170    fn load_from_env() {
171        unsafe {
172            std::env::set_var("ATLAS_ENV", "TEST");
173            std::env::set_var("ATLAS_KEY", "foo");
174            std::env::set_var("ATLAS_SECRET", "bar");
175        };
176        let client = Client::from_env().unwrap();
177        assert_eq!(client.config().key(), "foo");
178        assert_eq!(client.config().expose_secret(), "bar");
179    }
180
181    #[tokio::test]
182    async fn get_ok_response() {
183        const RESPONSE: &str = "it's working";
184        let client = default_client();
185        let server = MockServer::start();
186        let addr = server.address();
187        let mock = server.mock(|when, then| {
188            when.method(GET).path("/testing");
189            then.body(RESPONSE.as_bytes());
190        });
191        let url = Url::parse(&format!("http://{}/testing", addr)).unwrap();
192        let (response, status) = client.get(url).await.unwrap();
193
194        assert_eq!(response, RESPONSE.as_bytes());
195        assert_eq!(status, StatusCode::OK);
196        mock.assert_hits(1);
197    }
198
199    #[tokio::test]
200    async fn get_err_response() {
201        const RESPONSE: &str = "NOPE";
202        let client = default_client();
203        let server = MockServer::start();
204        let addr = server.address();
205        let mock = server.mock(|when, then| {
206            when.method(GET).path("/testing");
207            then.body(RESPONSE.as_bytes()).status(404);
208        });
209        let url = Url::parse(&format!("http://{}/testing", addr)).unwrap();
210        let (response, status) = client.get(url).await.unwrap();
211
212        assert_eq!(response, RESPONSE.as_bytes());
213        assert_eq!(status, StatusCode::NOT_FOUND);
214        mock.assert_hits(1);
215    }
216
217    #[tokio::test]
218    async fn post_json() {
219        let client = default_client();
220        let server = MockServer::start();
221        let addr = server.address();
222        let json = serde_json::json!({
223            "name": "foo",
224            "data": 12
225        });
226        let json_clone = json.clone();
227        let mock = server.mock(|when, then| {
228            when.method(POST).path("/testing").json_body(json_clone);
229            then.body(b"OK").status(200);
230        });
231        let url = Url::parse(&format!("http://{}/testing", addr)).unwrap();
232        client.post(url, &json).await.unwrap();
233
234        mock.assert_hits(1);
235    }
236}