mangadex_api/v5/oauth/
refresh_token.rs1use 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#[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 #[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(¶ms)
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(¶ms)
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}