mangadex_api/v5/oauth/
refresh_token.rs

1//! Builder for the OAuth login endpoint.
2//!
3//! <https://api.mangadex.org/docs/02-authentication/>
4//!
5//! It's the support for [Personal Client](https://api.mangadex.org/docs/02-authentication/personal-clients/)
6//!
7//! # Examples
8//!
9//! ```rust
10//! use mangadex_api_types::{Password, Username};
11//! use mangadex_api::v5::MangaDexClient;
12//! use mangadex_api_schema::v5::oauth::ClientInfo;
13//!
14//! # async fn run() -> anyhow::Result<()> {
15//!
16//! let mut client = MangaDexClient::default();
17//!
18//! client.set_client_info(&ClientInfo {
19//!     client_id: "someClientId".to_string(),
20//!     client_secret: "someClientSecret".to_string()
21//! }).await?;
22//!
23//! // Login to your account
24//!
25//! let login_res = client
26//!     .oauth()
27//!     .login()
28//!     .username(Username::parse("myusername")?)
29//!     .password(Password::parse("hunter2")?)
30//!     .send()
31//!     .await?;
32//!
33//! println!("login: {:?}", login_res);
34//!
35//! // Wait until the access token expires
36//! tokio::time::sleep(tokio::time::Duration::from_secs(<usize as TryInto<u64>>::try_into(login_res.expires_in)?)).await;
37//!
38//! let refresh_res = client
39//!     .oauth()
40//!     .refresh()
41//!     .send()
42//!     .await?;
43//!
44//! println!("refresh: {:?}", refresh_res);
45//!
46//! # Ok(())
47//! # }
48//! ```
49
50use derive_builder::Builder;
51use mangadex_api_schema::v5::oauth::OAuthTokenResponse;
52use mangadex_api_schema::v5::AuthTokens;
53use mangadex_api_types::oauth::GrantTypeSupported;
54use reqwest::Method;
55use serde::Serialize;
56#[cfg(not(test))]
57use url::Url;
58
59use crate::v5::HttpClientRef;
60use mangadex_api_types::error::Result;
61
62/// Log into an account.
63///
64/// Makes a request to `POST https://auth.mangadex.org/realms/mangadex/protocol/openid-connect/token`.
65#[cfg_attr(
66    feature = "deserializable-endpoint",
67    derive(serde::Deserialize, getset::Getters, getset::Setters)
68)]
69#[derive(Debug, Clone, Builder)]
70#[builder(
71    setter(into, strip_option),
72    build_fn(error = "mangadex_api_types::error::BuilderError")
73)]
74pub struct RefreshTokens {
75    /// This should never be set manually as this is only for internal use.
76    #[doc(hidden)]
77    #[cfg_attr(feature = "deserializable-endpoint", serde(skip))]
78    #[builder(pattern = "immutable")]
79    #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
80    pub http_client: HttpClientRef,
81}
82
83#[derive(Clone, Serialize)]
84struct RefreshTokenBody {
85    grant_type: GrantTypeSupported,
86    refresh_token: String,
87    client_id: String,
88    client_secret: String,
89}
90
91impl RefreshTokens {
92    pub async fn send(&mut self) -> Result<OAuthTokenResponse> {
93        let res = {
94            let client = {
95                #[cfg(all(
96                    not(feature = "multi-thread"),
97                    not(feature = "tokio-multi-thread"),
98                    not(feature = "rw-multi-thread")
99                ))]
100                {
101                    &self.http_client.try_borrow()?
102                }
103                #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
104                {
105                    &self.http_client.lock().await
106                }
107                #[cfg(feature = "rw-multi-thread")]
108                {
109                    &self.http_client.read().await
110                }
111            };
112            let client_info = client
113                .get_client_info()
114                .ok_or(mangadex_api_types::error::Error::MissingClientInfo)?;
115            let auth_tokens = client
116                .get_tokens()
117                .ok_or(mangadex_api_types::error::Error::MissingTokens)?;
118            let params = RefreshTokenBody {
119                grant_type: GrantTypeSupported::RefreshToken,
120                refresh_token: auth_tokens.refresh.to_owned(),
121                client_id: client_info.client_id.to_owned(),
122                client_secret: client_info.client_secret.to_owned(),
123            };
124            #[cfg(test)]
125            let res = client
126                .client
127                .request(
128                    Method::POST,
129                    client
130                        .base_url
131                        .join("/realms/mangadex/protocol/openid-connect/token")?,
132                )
133                .form(&params)
134                .send()
135                .await?;
136            #[cfg(not(test))]
137            let res = client
138                .client
139                .request(
140                    Method::POST,
141                    Url::parse(crate::AUTH_URL)?
142                        .join("/realms/mangadex/protocol/openid-connect/token")?,
143                )
144                .form(&params)
145                .send()
146                .await?;
147            res.json::<OAuthTokenResponse>().await?
148        };
149        {
150            let auth_tokens: AuthTokens = From::from(res.clone());
151            let client = {
152                #[cfg(all(
153                    not(feature = "multi-thread"),
154                    not(feature = "tokio-multi-thread"),
155                    not(feature = "rw-multi-thread")
156                ))]
157                {
158                    &mut self.http_client.try_borrow_mut()?
159                }
160                #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
161                {
162                    &mut self.http_client.lock().await
163                }
164                #[cfg(feature = "rw-multi-thread")]
165                {
166                    &mut self.http_client.write().await
167                }
168            };
169            client.set_auth_tokens(&auth_tokens);
170        };
171        Ok(res)
172    }
173}
174
175builder_send! {
176    #[builder] RefreshTokensBuilder,
177    OAuthTokenResponse
178}
179
180#[cfg(test)]
181mod tests {
182    use mangadex_api_schema::v5::oauth::ClientInfo;
183    use mangadex_api_types::oauth::GrantTypeSupported;
184    use serde_json::json;
185    use url::Url;
186    use wiremock::matchers::{body_string, header, method, path};
187    use wiremock::{Mock, MockServer, ResponseTemplate};
188
189    use crate::v5::oauth::refresh_token::RefreshTokenBody;
190    use crate::v5::AuthTokens;
191    use crate::{HttpClient, MangaDexClient};
192    use serde_urlencoded::to_string;
193
194    #[tokio::test]
195    async fn refresh_token_fires_a_request_to_base_url() -> anyhow::Result<()> {
196        let mock_server = MockServer::start().await;
197        let http_client: HttpClient = HttpClient::builder()
198            .base_url(Url::parse(&mock_server.uri())?)
199            .build()?;
200        let mut mangadex_client = MangaDexClient::new_with_http_client(http_client);
201
202        let client_info: ClientInfo = ClientInfo {
203            client_id: "someClientId".to_string(),
204            client_secret: "someClientSecret".to_string(),
205        };
206
207        mangadex_client.set_client_info(&client_info).await?;
208
209        let auth_tokens = AuthTokens {
210            session: "sessiontoken".to_string(),
211            refresh: "refreshtoken".to_string(),
212        };
213
214        mangadex_client.set_auth_tokens(&auth_tokens).await?;
215
216        let response_body = json!({
217            "access_token": auth_tokens.session.clone(),
218            "expires_in": 900,
219            "refresh_expires_in": 2414162,
220            "refresh_token": auth_tokens.refresh.clone(),
221            "token_type": "Bearer",
222            "not-before-policy": 0,
223            "session_state": "c176499d-6e8d-4ddf-ad59-6d922be66431",
224            "scope": "groups email profile",
225            "client_type": "personal"
226        });
227        let expected_body: String = to_string(RefreshTokenBody {
228            grant_type: GrantTypeSupported::RefreshToken,
229            refresh_token: auth_tokens.refresh.to_owned(),
230            client_id: client_info.client_id.clone(),
231            client_secret: client_info.client_secret.clone(),
232        })?;
233
234        Mock::given(method("POST"))
235            .and(path(r"/realms/mangadex/protocol/openid-connect/token"))
236            .and(header("Content-Type", "application/x-www-form-urlencoded"))
237            .and(body_string(expected_body))
238            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
239            .expect(1)
240            .mount(&mock_server)
241            .await;
242
243        let _ = mangadex_client.oauth().refresh().send().await?;
244
245        #[cfg(all(
246            not(feature = "multi-thread"),
247            not(feature = "tokio-multi-thread"),
248            not(feature = "rw-multi-thread")
249        ))]
250        assert_eq!(
251            mangadex_client.http_client.try_borrow()?.get_tokens(),
252            Some(&auth_tokens)
253        );
254        #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
255        assert_eq!(
256            mangadex_client.http_client.lock().await.get_tokens(),
257            Some(&auth_tokens)
258        );
259        #[cfg(feature = "rw-multi-thread")]
260        assert_eq!(
261            mangadex_client.http_client.read().await.get_tokens(),
262            Some(&auth_tokens)
263        );
264
265        Ok(())
266    }
267}