mangadex_api/v5/auth/
login.rs

1//! Builder for the auth login endpoint.
2//!
3//! This does not support 2-factor authentication currently.
4//!
5//! <https://api.mangadex.org/swagger.html#/Auth/post-auth-login>
6//!
7//! # Examples
8//!
9//! ```rust
10//! use mangadex_api::types::{Password, Username};
11//! use mangadex_api::v5::MangaDexClient;
12//!
13//! # async fn run() -> anyhow::Result<()> {
14//! let client = MangaDexClient::default();
15//!
16//! let login_res = client
17//!     .auth()
18//!     .login()
19//!     .username(Username::parse("myusername")?)
20//!     .password(Password::parse("hunter2")?)
21//!     .build()?
22//!     .send()
23//!     .await?;
24//!
25//! println!("login: {:?}", login_res);
26//! # Ok(())
27//! # }
28//! ```
29
30use derive_builder::Builder;
31use serde::Serialize;
32
33use crate::v5::HttpClientRef;
34use mangadex_api_schema::v5::LoginResponse;
35use mangadex_api_types::error::Result;
36use mangadex_api_types::{Password, Username};
37
38/// Log into an account.
39///
40/// Makes a request to `POST /auth/login`.
41#[derive(Debug, Builder, Serialize, Clone)]
42#[serde(rename_all = "camelCase")]
43#[builder(setter(into, strip_option))]
44pub struct Login<'a> {
45    /// This should never be set manually as this is only for internal use.
46    #[doc(hidden)]
47    #[serde(skip)]
48    #[builder(pattern = "immutable")]
49    pub(crate) http_client: HttpClientRef,
50
51    #[serde(skip_serializing_if = "Option::is_none")]
52    #[builder(default)]
53    pub username: Option<Username>,
54
55    #[serde(skip_serializing_if = "Option::is_none")]
56    #[builder(default)]
57    pub email: Option<&'a str>,
58
59    pub password: Password,
60}
61
62impl Login<'_> {
63    pub async fn send(&self) -> Result<LoginResponse> {
64        #[cfg(not(feature = "multi-thread"))]
65        let res = {
66            let res = self.http_client.borrow().send_request(self).await??;
67
68            self.http_client.borrow_mut().set_auth_tokens(&res.token);
69
70            res
71        };
72        #[cfg(feature = "multi-thread")]
73        let res = {
74            let res = self.http_client.lock().await.send_request(self).await??;
75
76            self.http_client.lock().await.set_auth_tokens(&res.token);
77
78            res
79        };
80
81        Ok(res)
82    }
83}
84
85endpoint! {
86    POST "/auth/login",
87    #[body] Login<'_>,
88    #[no_send] Result<LoginResponse>
89}
90
91#[cfg(test)]
92mod tests {
93    use serde_json::json;
94    use url::Url;
95    use wiremock::matchers::{header, method, path};
96    use wiremock::{Mock, MockServer, ResponseTemplate};
97
98    use crate::v5::AuthTokens;
99    use crate::{HttpClient, MangaDexClient};
100    use mangadex_api_types::error::Error;
101    use mangadex_api_types::{Password, Username};
102
103    #[tokio::test]
104    async fn login_fires_a_request_to_base_url() -> anyhow::Result<()> {
105        let mock_server = MockServer::start().await;
106        let http_client: HttpClient = HttpClient::builder()
107            .base_url(Url::parse(&mock_server.uri())?)
108            .build()?;
109        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
110
111        let _expected_body = json!({
112            "username": "myusername",
113            "password": "mypassword"
114        });
115        let response_body = json!({
116            "result": "ok",
117            "token": {
118                "session": "sessiontoken",
119                "refresh": "refreshtoken"
120            }
121        });
122
123        Mock::given(method("POST"))
124            .and(path(r"/auth/login"))
125            .and(header("Content-Type", "application/json"))
126            // TODO: Make the request body check work.
127            // .and(body_json(expected_body))
128            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
129            .expect(1)
130            .mount(&mock_server)
131            .await;
132
133        let _ = mangadex_client
134            .auth()
135            .login()
136            .username(Username::parse("myusername")?)
137            .password(Password::parse("mypassword")?)
138            .build()?
139            .send()
140            .await?;
141
142        #[cfg(not(feature = "multi-thread"))]
143        assert_eq!(
144            mangadex_client.http_client.borrow().get_tokens(),
145            Some(&AuthTokens {
146                session: "sessiontoken".to_string(),
147                refresh: "refreshtoken".to_string(),
148            })
149        );
150        #[cfg(feature = "multi-thread")]
151        assert_eq!(
152            mangadex_client.http_client.lock().await.get_tokens(),
153            Some(&AuthTokens {
154                session: "sessiontoken".to_string(),
155                refresh: "refreshtoken".to_string(),
156            })
157        );
158
159        Ok(())
160    }
161
162    #[tokio::test]
163    async fn logout_handles_400() -> anyhow::Result<()> {
164        let mock_server = MockServer::start().await;
165        let http_client: HttpClient = HttpClient::builder()
166            .base_url(Url::parse(&mock_server.uri())?)
167            .build()?;
168        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
169
170        let _expected_body = json!({
171            "username": "myusername",
172            "password": "mypassword"
173        });
174        let response_body = json!({
175            "result": "error",
176            "errors": []
177        });
178
179        Mock::given(method("POST"))
180            .and(path(r"/auth/login"))
181            .and(header("Content-Type", "application/json"))
182            // TODO: Make the request body check work.
183            // .and(body_json(expected_body))
184            .respond_with(ResponseTemplate::new(400).set_body_json(response_body))
185            .expect(1)
186            .mount(&mock_server)
187            .await;
188
189        let res = mangadex_client
190            .auth()
191            .login()
192            .username(Username::parse("myusername")?)
193            .password(Password::parse("mypassword")?)
194            .build()?
195            .send()
196            .await
197            .expect_err("expected error");
198
199        #[cfg(not(feature = "multi-thread"))]
200        assert_eq!(mangadex_client.http_client.borrow().get_tokens(), None);
201        #[cfg(feature = "multi-thread")]
202        assert_eq!(mangadex_client.http_client.lock().await.get_tokens(), None);
203
204        if let Error::Api(errors) = res {
205            assert_eq!(errors.errors.len(), 0);
206        }
207
208        Ok(())
209    }
210
211    #[tokio::test]
212    async fn logout_handles_401() -> anyhow::Result<()> {
213        let mock_server = MockServer::start().await;
214        let http_client: HttpClient = HttpClient::builder()
215            .base_url(Url::parse(&mock_server.uri())?)
216            .build()?;
217        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
218
219        let _expected_body = json!({
220            "username": "myusername",
221            "password": "mypassword"
222        });
223
224        Mock::given(method("POST"))
225            .and(path(r"/auth/login"))
226            .and(header("Content-Type", "application/json"))
227            // TODO: Make the request body check work.
228            // .and(body_json(expected_body))
229            .respond_with(ResponseTemplate::new(401))
230            .expect(1)
231            .mount(&mock_server)
232            .await;
233
234        let res = mangadex_client
235            .auth()
236            .login()
237            .username(Username::parse("myusername")?)
238            .password(Password::parse("mypassword")?)
239            .build()?
240            .send()
241            .await
242            .expect_err("expected error");
243
244        #[cfg(not(feature = "multi-thread"))]
245        assert_eq!(mangadex_client.http_client.borrow().get_tokens(), None);
246        #[cfg(feature = "multi-thread")]
247        assert_eq!(mangadex_client.http_client.lock().await.get_tokens(), None);
248
249        match res {
250            Error::RequestError(_) => {}
251            _ => panic!("unexpected error"),
252        }
253
254        Ok(())
255    }
256
257    #[tokio::test]
258    async fn logout_handles_http_503() -> anyhow::Result<()> {
259        let mock_server = MockServer::start().await;
260        let http_client: HttpClient = HttpClient::builder()
261            .base_url(Url::parse(&mock_server.uri())?)
262            .build()?;
263        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
264
265        let _expected_body = json!({
266            "username": "myusername",
267            "password": "mypassword"
268        });
269
270        Mock::given(method("POST"))
271            .and(path(r"/auth/login"))
272            .and(header("Content-Type", "application/json"))
273            // TODO: Make the request body check work.
274            // .and(body_json(expected_body))
275            .respond_with(ResponseTemplate::new(503))
276            .expect(1)
277            .mount(&mock_server)
278            .await;
279
280        let res = mangadex_client
281            .auth()
282            .login()
283            .username(Username::parse("myusername")?)
284            .password(Password::parse("mypassword")?)
285            .build()?
286            .send()
287            .await
288            .expect_err("expected error");
289
290        #[cfg(not(feature = "multi-thread"))]
291        assert_eq!(mangadex_client.http_client.borrow().get_tokens(), None);
292        #[cfg(feature = "multi-thread")]
293        assert_eq!(mangadex_client.http_client.lock().await.get_tokens(), None);
294
295        match res {
296            Error::ServerError(..) => {}
297            _ => panic!("unexpected error"),
298        }
299
300        Ok(())
301    }
302}