1use bytes::Bytes;
2use freedom_config::Config;
3use reqwest::{Response, StatusCode};
4use url::Url;
5
6use crate::api::{Api, Inner, Value};
7
8#[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 pub fn from_config(config: Config) -> Self {
42 Self {
43 config,
44 client: reqwest::Client::new(),
45 }
46 }
47
48 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}