Skip to main content

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 crate::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 = "crate::error::BuilderError")
73)]
74#[non_exhaustive]
75pub struct RefreshTokens {
76    /// This should never be set manually as this is only for internal use.
77    #[doc(hidden)]
78    #[cfg_attr(feature = "deserializable-endpoint", serde(skip))]
79    #[builder(pattern = "immutable")]
80    #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
81    pub http_client: HttpClientRef,
82}
83
84#[derive(Clone, Serialize)]
85struct RefreshTokenBody {
86    grant_type: GrantTypeSupported,
87    refresh_token: String,
88    client_id: String,
89    client_secret: String,
90}
91
92impl RefreshTokens {
93    pub async fn send(&mut self) -> Result<OAuthTokenResponse> {
94        let res = {
95            let client = self.http_client.read().await;
96            let client_info = client
97                .get_client_info()
98                .ok_or(crate::error::Error::MissingClientInfo)?;
99            let auth_tokens = client
100                .get_tokens()
101                .ok_or(crate::error::Error::MissingTokens)?;
102            let params = RefreshTokenBody {
103                grant_type: GrantTypeSupported::RefreshToken,
104                refresh_token: auth_tokens.refresh.to_owned(),
105                client_id: client_info.client_id.to_owned(),
106                client_secret: client_info.client_secret.to_owned(),
107            };
108            #[cfg(test)]
109            let res = client
110                .client
111                .request(
112                    Method::POST,
113                    client
114                        .base_url
115                        .join("/realms/mangadex/protocol/openid-connect/token")?,
116                )
117                .form(&params)
118                .send()
119                .await?;
120            #[cfg(not(test))]
121            let res = client
122                .client
123                .request(
124                    Method::POST,
125                    Url::parse(crate::AUTH_URL)?
126                        .join("/realms/mangadex/protocol/openid-connect/token")?,
127                )
128                .form(&params)
129                .send()
130                .await?;
131            if res.status().is_client_error() || res.status().is_server_error() {
132                return Err(super::OAuthError::handle_resp(res).await);
133            }
134            res.json::<OAuthTokenResponse>().await?
135        };
136        {
137            let auth_tokens: AuthTokens = From::from(res.clone());
138            let mut client = self.http_client.write().await;
139            client.set_auth_tokens(&auth_tokens);
140        };
141        Ok(res)
142    }
143}
144
145builder_send! {
146    #[builder] RefreshTokensBuilder,
147    OAuthTokenResponse
148}
149
150#[cfg(test)]
151mod tests {
152    use mangadex_api_schema::v5::oauth::ClientInfo;
153    use mangadex_api_types::oauth::GrantTypeSupported;
154    use serde_json::json;
155    use url::Url;
156    use wiremock::matchers::{body_string, header, method, path};
157    use wiremock::{Mock, MockServer, ResponseTemplate};
158
159    use crate::v5::oauth::refresh_token::RefreshTokenBody;
160    use crate::v5::AuthTokens;
161    use crate::{HttpClient, MangaDexClient};
162    use serde_urlencoded::to_string;
163
164    #[tokio::test]
165    async fn refresh_token_fires_a_request_to_base_url() -> anyhow::Result<()> {
166        let mock_server = MockServer::start().await;
167        let http_client: HttpClient = HttpClient::builder()
168            .base_url(Url::parse(&mock_server.uri())?)
169            .build()?;
170        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
171
172        let client_info: ClientInfo = non_exhaustive::non_exhaustive!(ClientInfo {
173            client_id: "someClientId".to_string(),
174            client_secret: "someClientSecret".to_string(),
175        });
176
177        mangadex_client.set_client_info(&client_info).await?;
178
179        let auth_tokens = non_exhaustive::non_exhaustive!(AuthTokens {
180            session: "sessiontoken".to_string(),
181            refresh: "refreshtoken".to_string(),
182        });
183
184        mangadex_client.set_auth_tokens(&auth_tokens).await?;
185
186        let response_body = json!({
187            "access_token": auth_tokens.session.clone(),
188            "expires_in": 900,
189            "refresh_expires_in": 2414162,
190            "refresh_token": auth_tokens.refresh.clone(),
191            "token_type": "Bearer",
192            "not-before-policy": 0,
193            "session_state": "c176499d-6e8d-4ddf-ad59-6d922be66431",
194            "scope": "groups email profile",
195            "client_type": "personal"
196        });
197        let expected_body: String = to_string(RefreshTokenBody {
198            grant_type: GrantTypeSupported::RefreshToken,
199            refresh_token: auth_tokens.refresh.to_owned(),
200            client_id: client_info.client_id.clone(),
201            client_secret: client_info.client_secret.clone(),
202        })?;
203
204        Mock::given(method("POST"))
205            .and(path(r"/realms/mangadex/protocol/openid-connect/token"))
206            .and(header("Content-Type", "application/x-www-form-urlencoded"))
207            .and(body_string(expected_body))
208            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
209            .expect(1)
210            .mount(&mock_server)
211            .await;
212
213        let _ = mangadex_client.oauth().refresh().send().await?;
214
215        assert_eq!(
216            mangadex_client.http_client.read().await.get_tokens(),
217            Some(&auth_tokens)
218        );
219
220        Ok(())
221    }
222}