1use log::{debug, trace};
8use reqwest::{Method, Response, Url};
9use serde::Serialize;
10
11use super::error::Result;
12use super::model::{
13 Token,
14 Auth,
15 Refresh,
16 User,
17 UserPassword,
18 UserChangePassword,
19 NewPassword,
20 Passwords
21};
22
23const USER_AGENT: &str = concat!(
24 env!("CARGO_PKG_NAME"),
25 "/",
26 env!("CARGO_PKG_VERSION")
27);
28
29#[derive(Clone, Debug)]
31pub struct Client {
32 pub url: String,
33 pub client: reqwest::Client
34}
35
36impl Client {
46
47 pub fn new(url: impl Into<String>) -> Client {
49 Client {
50 url: url.into(),
51 client: reqwest::Client::builder()
52 .connection_verbose(true)
53 .user_agent(USER_AGENT)
54 .build()
55 .expect("Client::new()")
56 }
57 }
58
59 fn build_url(&self, path: &str) -> Result<Url> {
61 Ok(Url::parse(&self.url)?.join(path)?)
62 }
63
64 async fn empty_request(&self, method: Method, url: &Url, token: &str) -> Result<Response> {
66 let authorization = format!("Bearer {}", token);
67 Ok(self.client.request(method, url.as_str())
68 .header("Authorization", authorization)
69 .send().await?
70 .error_for_status()?)
71 }
72
73 async fn request<J: Serialize + ?Sized>(&self, method: Method, url: &Url, token: Option<&str>, json: &J) -> Result<Response> {
75 match token {
76 Some(token) => {
77 let authorization = format!("Bearer {}", token);
78 Ok(self.client.request(method, url.as_str())
79 .header("Authorization", authorization)
80 .json(&json).send().await?
81 .error_for_status()?)
82 },
83 None => Ok(self.client.request(method, url.as_str()).json(&json).send().await?.error_for_status()?)
84 }
85 }
86
87 async fn get(&self, path: &str, token: &str) -> Result<Response> {
89 let url = self.build_url(path)?;
90 trace!("GET: {url}");
91 self.empty_request(Method::GET, &url, token).await
92 }
93
94 async fn post<J: Serialize + ?Sized>(&self, path: &str, token: Option<&str>, json: &J) -> Result<Response> {
96 let url = self.build_url(path)?;
97 trace!("POST: {url}");
98 self.request(Method::POST, &url, token, json).await
99 }
100
101 async fn put<J: Serialize + ?Sized>(&self, path: &str, token: &str, json: &J) -> Result<Response> {
103 let url = self.build_url(path)?;
104 trace!("PUT: {url}");
105 self.request(Method::PUT, &url, Some(token), json).await
106 }
107
108 async fn empty_delete(&self, path: &str, token: &str) -> Result<Response> {
110 let url = self.build_url(path)?;
111 trace!("DELETE: {url}");
112 self.empty_request(Method::DELETE, &url, token).await
113 }
114
115 async fn delete<J: Serialize + ?Sized>(&self, path: &str, token: &str, json: &J) -> Result<Response> {
117 let url = self.build_url(path)?;
118 trace!("DELETE: {url}");
119 self.request(Method::DELETE, &url, Some(token), json).await
120 }
121
122 pub fn parse_url(&self) -> Result<Url> {
124 Ok(Url::parse(&self.url)?)
125 }
126
127 pub async fn create_token(&self, email: &str, password: &str) -> Result<Token> {
129 debug!("Requesting new token");
130 let body = Auth { email: email.into(), password: password.into() };
131 Ok(self.post("auth/jwt/create/", None, &body).await?.json::<Token>().await?)
132 }
133
134 pub async fn refresh_token(&self, token: &str) -> Result<Token> {
138 debug!("Requesting refreshed token");
139 let body = Refresh { refresh: token.into() };
140 Ok(self.post("auth/jwt/refresh/", None, &body).await?.json::<Token>().await?)
141 }
142
143 pub async fn create_user(&self, email: &str, password: &str) -> Result<()> {
145 debug!("Requesting new user");
146 let body = Auth { email: email.into(), password: password.into() };
147 self.post("auth/users/", None, &body).await?;
148 Ok(())
149 }
150
151 pub async fn get_user(&self, token: &str) -> Result<User> {
155 debug!("Requesting user info");
156 Ok(self.get("auth/users/me/", token).await?.json::<User>().await?)
157 }
158
159 pub async fn change_user_password(&self, token: &str, current_password: &str, new_password: &str) -> Result<()> {
163 debug!("Requesting a password change");
164 let body = UserChangePassword { current_password: current_password.into(), new_password: new_password.into() };
165 self.post("auth/users/set_password/", Some(token), &body).await?;
166 Ok(())
167 }
168
169 pub async fn delete_user(&self, token: &str, current_password: &str) -> Result<()> {
173 debug!("Requesting user deletion");
174 let body = UserPassword { current_password: current_password.into() };
175 self.delete("auth/users/me/", token, &body).await?;
176 Ok(())
177 }
178
179 pub async fn get_passwords(&self, token: &str) -> Result<Passwords> {
183 Ok(self.get("passwords/", token).await?.json::<Passwords>().await?)
184 }
185
186 pub async fn post_password(&self, token: &str, password: &NewPassword) -> Result<()> {
190 self.post("passwords/", Some(token), password).await?;
191 Ok(())
192 }
193
194 pub async fn put_password(&self, token: &str, id: &str, password: &NewPassword) -> Result<()> {
198 self.put(&format!("passwords/{id}/"), token, password).await?;
199 Ok(())
200 }
201
202 pub async fn delete_password(&self, token: &str, id: &str) -> Result<()> {
206 self.empty_delete(&format!("passwords/{id}/"), token).await?;
207 Ok(())
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 use chrono::{NaiveDate, Utc};
216 use mockito::{Matcher, Server};
217
218 const JH: (&str, &str) = ("content-type", "application/json");
219
220 #[test]
221 fn parse_url() {
222 let url = Client::new("http://localhost").parse_url().unwrap();
223 assert_eq!("http", url.scheme());
224 assert_eq!(Some("localhost"), url.host_str());
225 assert_eq!(Some(80), url.port_or_known_default());
226 let url = Client::new("https://localhost").parse_url().unwrap();
227 assert_eq!("https", url.scheme());
228 assert_eq!(Some(443), url.port_or_known_default());
229 let url = Client::new("https://example.com:6666").parse_url().unwrap();
230 assert_eq!(Some("example.com"), url.host_str());
231 assert_eq!(Some(6666), url.port_or_known_default());
232 let url_error = Client::new("bad").parse_url().unwrap_err();
234 assert_eq!("URL parse error, relative URL without a base", url_error.to_string());
235 }
236
237 #[tokio::test]
238 async fn create_token() {
239 let mut server = Server::new_async().await;
240 let client = Client::new(&server.url());
241 let request_body = r#"{"email": "user@example.com", "password": "password"}"#;
242 let response_body = r#"{"access": "access-token", "refresh": "refresh-token"}"#;
243 let _m = server.mock("POST", "/auth/jwt/create/")
244 .with_status(201)
245 .with_header(JH.0, JH.1)
246 .match_body(Matcher::JsonString(request_body.to_string()))
247 .with_body(response_body)
248 .create_async()
249 .await;
250 let token = client.create_token("user@example.com", "password").await.unwrap();
252 assert_eq!("access-token", &token.access);
253 assert_eq!("refresh-token", &token.refresh);
254 let error_token = client.create_token("bad", "bad").await.unwrap_err();
256 assert_eq!(Some(501), error_token.status());
257 let _m = server.mock("POST", "/auth/jwt/create/")
258 .with_status(201)
259 .with_header(JH.0, JH.1)
260 .with_body("unexpected")
261 .create_async()
262 .await;
263 let error_body = client.create_token("bad", "bad").await.unwrap_err();
265 assert_eq!("reqwest error, error decoding response body", error_body.to_string());
266 }
267
268 #[tokio::test]
269 async fn refresh_token() {
270 let mut server = Server::new_async().await;
271 let client = Client::new(&server.url());
272 let request_body = r#"{"refresh": "refresh-token"}"#;
273 let response_body = r#"{"access": "new-access-token", "refresh": "new-refresh-token"}"#;
274 let _m = server.mock("POST", "/auth/jwt/refresh/")
275 .with_status(201)
276 .with_header(JH.0, JH.1)
277 .match_body(Matcher::JsonString(request_body.to_string()))
278 .with_body(response_body)
279 .create_async()
280 .await;
281 let token = client.refresh_token("refresh-token").await.unwrap();
283 assert_eq!("new-access-token", &token.access);
284 assert_eq!("new-refresh-token", &token.refresh);
285 let error_token = client.refresh_token("bad-token").await.unwrap_err();
287 assert_eq!(Some(501), error_token.status());
288 let _m = server.mock("POST", "/auth/jwt/refresh/")
289 .with_status(201)
290 .with_header(JH.0, JH.1)
291 .with_body("unexpected")
292 .create_async()
293 .await;
294 let error_body = client.refresh_token("bad-token").await.unwrap_err();
296 assert_eq!("reqwest error, error decoding response body", error_body.to_string());
297 }
298
299 #[tokio::test]
300 async fn create_user() {
301 let mut server = Server::new_async().await;
302 let client = Client::new(&server.url());
303 let request_body = r#"{"email": "newuser@example.com", "password": "newpassword"}"#;
304 let _m = server.mock("POST", "/auth/users/")
305 .with_status(201)
306 .with_header(JH.0, JH.1)
307 .match_body(Matcher::JsonString(request_body.to_string()))
308 .create_async()
309 .await;
310 let user = client.create_user("newuser@example.com", "newpassword").await.unwrap();
312 assert_eq!((), user);
313 }
314
315 #[tokio::test]
316 async fn get_user() {
317 let mut server = Server::new_async().await;
318 let client = Client::new(&server.url());
319 let response_body = r#"{"id":1, "email": "newuser@example.com"}"#;
320 let _m = server.mock("GET", "/auth/users/me/")
321 .with_status(200)
322 .with_header(JH.0, JH.1)
323 .match_header("authorization", "Bearer access-token")
324 .with_body(response_body)
325 .create_async()
326 .await;
327 let user = client.get_user("access-token").await.unwrap();
329 assert_eq!("1", user.id);
330 assert_eq!("newuser@example.com", user.email);
331 let error_in_token = client.get_user("bad-token").await.unwrap_err();
333 assert_eq!(Some(501), error_in_token.status());
334 }
335
336 #[tokio::test]
337 async fn change_user_password() {
338 let mut server = Server::new_async().await;
339 let client = Client::new(&server.url());
340 let request_body = r#"{"current_password": "current", "new_password": "new"}"#;
341 let _m = server.mock("POST", "/auth/users/set_password/")
342 .with_status(201)
343 .with_header(JH.0, JH.1)
344 .match_header("authorization", "Bearer access-token")
345 .match_body(Matcher::JsonString(request_body.to_string()))
346 .create_async()
347 .await;
348 let change_password = client.change_user_password("access-token", "current", "new").await.unwrap();
350 assert_eq!((), change_password);
351 let error_in_token = client.change_user_password("bad-token", "current", "new").await.unwrap_err();
353 assert_eq!(Some(501), error_in_token.status());
354 }
355
356 #[tokio::test]
357 async fn delete_user() {
358 let mut server = Server::new_async().await;
359 let client = Client::new(&server.url());
360 let request_body = r#"{"current_password": "current"}"#;
361 let _m = server.mock("DELETE", "/auth/users/me/")
362 .with_status(200)
363 .with_header(JH.0, JH.1)
364 .match_header("authorization", "Bearer access-token")
365 .match_body(Matcher::JsonString(request_body.to_string()))
366 .create_async()
367 .await;
368 let delete_user = client.delete_user("access-token", "current").await.unwrap();
370 assert_eq!((), delete_user);
371 let error_in_token = client.delete_user("bad-token", "current").await.unwrap_err();
373 assert_eq!(Some(501), error_in_token.status());
374 }
375
376 #[tokio::test]
377 async fn get_passwords() {
378 let mut server = Server::new_async().await;
379 let client = Client::new(&server.url());
380 let response_body = r#"
381 {
382 "count": 3,
383 "next": null,
384 "previous": null,
385 "results": [
386 {
387 "id": "e1a7e83c-9014-4585-95f5-4595160afe99",
388 "login": "user@example.com",
389 "site": "alice.example.com",
390 "lowercase": true,
391 "uppercase": true,
392 "symbols": true,
393 "digits": true,
394 "counter": 10,
395 "length": 16,
396 "version": 2,
397 "created": "2021-12-06T11:39:47.874027Z",
398 "modified": "2021-12-06T11:39:47.874143Z"
399 },
400 {
401 "id": "5f01f483-2b63-4faa-9c0c-b2dae03440f1",
402 "login": "user@example.com",
403 "site": "bob.example.com",
404 "lowercase": false,
405 "uppercase": true,
406 "symbols": true,
407 "numbers": false,
408 "counter": 1,
409 "length": 35,
410 "version": 2,
411 "created": "2021-11-21T11:34:18.361454Z",
412 "modified": "2021-12-07T04:12:05.131415Z"
413 },
414 {
415 "id": "10",
416 "login": "user@example.com",
417 "site": "charlie.example.com",
418 "lowercase": false,
419 "uppercase": true,
420 "symbols": true,
421 "digits": false,
422 "numbers": true,
423 "counter": 1,
424 "length": 8,
425 "version": 2,
426 "created": "2023-05-10T12:05:36",
427 "modified": "2023-06-02T17:33:54"
428 }
429 ]
430 }
431 "#;
432 let _m = server.mock("GET", "/passwords/")
433 .with_status(200)
434 .with_header(JH.0, JH.1)
435 .match_header("authorization", "Bearer access-token")
436 .with_body(response_body)
437 .create_async()
438 .await;
439 let passwords = client.get_passwords("access-token").await.unwrap();
441 assert_eq!(3, passwords.count);
442 assert_eq!("e1a7e83c-9014-4585-95f5-4595160afe99", &passwords.results[0].id);
443 assert_eq!("10", &passwords.results[2].id);
444 assert_eq!(true, passwords.results[0].lowercase);
445 assert_eq!("bob.example.com", &passwords.results[1].site);
446 assert_eq!(false, passwords.results[1].digits);
447 assert_eq!(NaiveDate::from_ymd_opt(2021, 11, 21).unwrap().and_hms_micro_opt(11, 34, 18, 361454).unwrap().and_local_timezone(Utc).unwrap(), passwords.results[1].created);
448 assert_eq!(NaiveDate::from_ymd_opt(2021, 12, 7).unwrap().and_hms_micro_opt(4, 12, 5, 131415).unwrap().and_local_timezone(Utc).unwrap(), passwords.results[1].modified);
449 assert_eq!(NaiveDate::from_ymd_opt(2023, 06, 2).unwrap().and_hms_micro_opt(17, 33, 54, 0).unwrap().and_local_timezone(Utc).unwrap(), passwords.results[2].modified);
450 let error_in_token = client.get_passwords("bad-token").await.unwrap_err();
452 assert_eq!(Some(501), error_in_token.status());
453 }
454
455 #[tokio::test]
456 async fn post_password() {
457 let mut server = Server::new_async().await;
458 let client = Client::new(&server.url());
459 let request_body = r#"
460 {
461 "login": "newuser@example.com",
462 "site": "new.example.com",
463 "uppercase": true,
464 "lowercase": true,
465 "digits": false,
466 "symbols": true,
467 "length": 18,
468 "counter": 5,
469 "version": 2
470 }
471 "#;
472 let password = NewPassword {
473 site: "new.example.com".to_string(),
474 login: "newuser@example.com".to_string(),
475 lowercase: true,
476 uppercase: true,
477 symbols: true,
478 digits: false,
479 length: 18,
480 counter: 5,
481 version: 2
482 };
483 let _m = server.mock("POST", "/passwords/")
484 .with_status(201)
485 .with_header(JH.0, JH.1)
486 .match_header("authorization", "Bearer access-token")
487 .match_body(Matcher::JsonString(request_body.to_string()))
488 .create_async()
489 .await;
490 let post_password = client.post_password("access-token", &password).await.unwrap();
492 assert_eq!((), post_password);
493 let error_in_token = client.post_password("bad-token", &password).await.unwrap_err();
495 assert_eq!(Some(501), error_in_token.status());
496 }
497
498 #[tokio::test]
499 async fn put_password() {
500 let mut server = Server::new_async().await;
501 let client = Client::new(&server.url());
502 let request_body = r#"
503 {
504 "login": "updateuser@example.com",
505 "site": "update.example.com",
506 "uppercase": true,
507 "lowercase": true,
508 "digits": false,
509 "symbols": false,
510 "length": 22,
511 "counter": 1,
512 "version": 2
513 }
514 "#;
515 let password = NewPassword {
516 site: "update.example.com".to_string(),
517 login: "updateuser@example.com".to_string(),
518 lowercase: true,
519 uppercase: true,
520 symbols: false,
521 digits: false,
522 length: 22,
523 counter: 1,
524 version: 2
525 };
526 let _m = server.mock("PUT", "/passwords/ce2835da-9047-43eb-a107-bad4f01d22a0/")
527 .with_status(200)
528 .with_header(JH.0, JH.1)
529 .match_header("authorization", "Bearer access-token")
530 .match_body(Matcher::JsonString(request_body.to_string()))
531 .create_async()
532 .await;
533 let put_password = client.put_password("access-token", "ce2835da-9047-43eb-a107-bad4f01d22a0", &password).await.unwrap();
535 assert_eq!((), put_password);
536 let error_in_token = client.put_password("bad-token", "ce2835da-9047-43eb-a107-bad4f01d22a0", &password).await.unwrap_err();
538 assert_eq!(Some(501), error_in_token.status());
539 let error_in_id = client.put_password("access-token", "bad-id", &password).await.unwrap_err();
541 assert_eq!(Some(501), error_in_id.status());
542 }
543
544 #[tokio::test]
545 async fn delete_password() {
546 let mut server = Server::new_async().await;
547 let client = Client::new(&server.url());
548 let _m = server.mock("DELETE", "/passwords/1c461df9-11eb-4bf1-976b-1c49d5598b8f/")
549 .with_status(204)
550 .with_header(JH.0, JH.1)
551 .match_header("authorization", "Bearer access-token")
552 .create_async()
553 .await;
554 let delete_password = client.delete_password("access-token", "1c461df9-11eb-4bf1-976b-1c49d5598b8f").await.unwrap();
556 assert_eq!((), delete_password);
557 let error_in_token = client.delete_password("bad-token", "1c461df9-11eb-4bf1-976b-1c49d5598b8f").await.unwrap_err();
559 assert_eq!(Some(501), error_in_token.status());
560 let error_in_id = client.delete_password("access-token", "bad-id").await.unwrap_err();
562 assert_eq!(Some(501), error_in_id.status());
563 }
564}