mangadex_api/v5/rating/manga_id/
post.rs

1//! Builder for the create or update Manga rating endpoint.
2//!
3//! This endpoint requires authentication.
4//!
5//! <https://api.mangadex.org/docs/swagger.html#/Rating/post-rating-manga-id>
6//!
7//! # Examples
8//!
9//! ```rust
10//! // use mangadex_api_types::{Password, Username};
11//! use mangadex_api::v5::MangaDexClient;
12//! use uuid::Uuid;
13//!
14//! # async fn run() -> anyhow::Result<()> {
15//! let client = MangaDexClient::default();
16//!
17//! /*
18//!
19//!     let _login_res = client
20//!         .auth()
21//!         .login()
22//!         .post()
23//!         .username(Username::parse("myusername")?)
24//!         .password(Password::parse("hunter2")?)
25//!         .send()
26//!         .await?;
27//!
28//!  */
29//!
30//! // Official Test Manga ID.
31//! let manga_id = Uuid::parse_str("f9c33607-9180-4ba6-b85c-e4b5faee7192")?;
32//!
33//! let res = client
34//!     .rating()
35//!     .manga_id(manga_id)
36//!     .post()
37//!     .rating(9)
38//!     .send()
39//!     .await?;
40//!
41//! println!("Response: {:?}", res);
42//! # Ok(())
43//! # }
44//! ```
45
46use derive_builder::Builder;
47use serde::Serialize;
48use uuid::Uuid;
49
50use crate::HttpClientRef;
51use mangadex_api_schema::NoData;
52use mangadex_api_types::error::Result;
53
54#[cfg_attr(
55    feature = "deserializable-endpoint",
56    derive(serde::Deserialize, getset::Getters, getset::Setters)
57)]
58#[derive(Debug, Serialize, Clone, Builder)]
59#[serde(rename_all = "camelCase")]
60#[builder(
61    setter(into, strip_option),
62    build_fn(error = "mangadex_api_types::error::BuilderError")
63)]
64pub struct CreateUpdateMangaRating {
65    /// This should never be set manually as this is only for internal use.
66    #[doc(hidden)]
67    #[serde(skip)]
68    #[builder(pattern = "immutable")]
69    #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
70    pub http_client: HttpClientRef,
71
72    #[serde(skip_serializing)]
73    pub manga_id: Uuid,
74
75    /// `[ 1 .. 10 ]`.
76    ///
77    /// Numbers below `1` will be set at `1` and numbers above `10` will be set at `10`.
78    pub rating: u8,
79}
80
81impl CreateUpdateMangaRating {
82    pub async fn send(&mut self) -> Result<NoData> {
83        self.rating = self.rating.clamp(1, 10);
84
85        #[cfg(all(
86            not(feature = "multi-thread"),
87            not(feature = "tokio-multi-thread"),
88            not(feature = "rw-multi-thread")
89        ))]
90        let res = self.http_client.try_borrow()?.send_request(self).await??;
91        #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
92        let res = self.http_client.lock().await.send_request(self).await??;
93
94        #[cfg(feature = "rw-multi-thread")]
95        let res = self.http_client.read().await.send_request(self).await??;
96
97        Ok(res)
98    }
99}
100
101endpoint! {
102    POST ("/rating/{}", manga_id),
103    #[body auth] CreateUpdateMangaRating,
104    #[no_send] Result<NoData>
105}
106
107builder_send! {
108    #[builder] CreateUpdateMangaRatingBuilder,
109    NoData
110}
111
112#[cfg(test)]
113mod tests {
114    use serde_json::json;
115    use url::Url;
116    use uuid::Uuid;
117    use wiremock::matchers::{body_json, header, method, path_regex};
118    use wiremock::{Mock, MockServer, ResponseTemplate};
119
120    use crate::v5::AuthTokens;
121    use crate::{HttpClient, MangaDexClient};
122    use mangadex_api_types::error::Error;
123
124    #[tokio::test]
125    async fn create_update_manga_rating_fires_a_request_to_base_url() -> anyhow::Result<()> {
126        let mock_server = MockServer::start().await;
127        let http_client = HttpClient::builder()
128            .base_url(Url::parse(&mock_server.uri())?)
129            .auth_tokens(AuthTokens {
130                session: "sessiontoken".to_string(),
131                refresh: "refreshtoken".to_string(),
132            })
133            .build()?;
134        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
135
136        let expected_body = json!({
137            "rating": 9
138        });
139
140        let manga_id = Uuid::new_v4();
141        let response_body = json!({
142            "result": "ok",
143        });
144
145        Mock::given(method("POST"))
146            .and(path_regex(r"/rating/[0-9a-fA-F-]+"))
147            .and(header("Content-Type", "application/json"))
148            .and(body_json(expected_body))
149            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
150            .expect(1)
151            .mount(&mock_server)
152            .await;
153
154        let _res = mangadex_client
155            .rating()
156            .manga_id(manga_id)
157            .post()
158            .rating(9)
159            .send()
160            .await?;
161
162        Ok(())
163    }
164
165    #[tokio::test]
166    async fn create_update_manga_rating_sets_rating_below_min_to_1() -> anyhow::Result<()> {
167        let mock_server = MockServer::start().await;
168        let http_client = HttpClient::builder()
169            .base_url(Url::parse(&mock_server.uri())?)
170            .auth_tokens(AuthTokens {
171                session: "sessiontoken".to_string(),
172                refresh: "refreshtoken".to_string(),
173            })
174            .build()?;
175        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
176
177        let expected_body = json!({
178            "rating": 1
179        });
180
181        let manga_id = Uuid::new_v4();
182        let response_body = json!({
183            "result": "ok",
184        });
185
186        Mock::given(method("POST"))
187            .and(path_regex(r"/rating/[0-9a-fA-F-]+"))
188            .and(header("Content-Type", "application/json"))
189            .and(body_json(expected_body))
190            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
191            .expect(1)
192            .mount(&mock_server)
193            .await;
194
195        let _res = mangadex_client
196            .rating()
197            .manga_id(manga_id)
198            .post()
199            .rating(0)
200            .send()
201            .await?;
202
203        Ok(())
204    }
205
206    #[tokio::test]
207    async fn create_update_manga_rating_sets_rating_above_max_to_10() -> anyhow::Result<()> {
208        let mock_server = MockServer::start().await;
209        let http_client = HttpClient::builder()
210            .base_url(Url::parse(&mock_server.uri())?)
211            .auth_tokens(AuthTokens {
212                session: "sessiontoken".to_string(),
213                refresh: "refreshtoken".to_string(),
214            })
215            .build()?;
216        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
217
218        let expected_body = json!({
219            "rating": 10
220        });
221
222        let manga_id = Uuid::new_v4();
223        let response_body = json!({
224            "result": "ok",
225        });
226
227        Mock::given(method("POST"))
228            .and(path_regex(r"/rating/[0-9a-fA-F-]+"))
229            .and(header("Content-Type", "application/json"))
230            .and(body_json(expected_body))
231            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
232            .expect(1)
233            .mount(&mock_server)
234            .await;
235
236        let _res = mangadex_client
237            .rating()
238            .manga_id(manga_id)
239            .post()
240            .rating(11)
241            .send()
242            .await?;
243
244        Ok(())
245    }
246
247    #[tokio::test]
248    async fn create_update_manga_rating_requires_auth() -> anyhow::Result<()> {
249        let mock_server = MockServer::start().await;
250        let http_client: HttpClient = HttpClient::builder()
251            .base_url(Url::parse(&mock_server.uri())?)
252            .build()?;
253        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
254
255        let manga_id = Uuid::new_v4();
256        let error_id = Uuid::new_v4();
257        let response_body = json!({
258            "result": "error",
259            "errors": [{
260                "id": error_id.to_string(),
261                "status": 403,
262                "title": "Forbidden",
263                "detail": "You must be logged in to continue."
264            }]
265        });
266
267        Mock::given(method("POST"))
268            .and(path_regex(r"/rating/[0-9a-fA-F-]+"))
269            .and(header("Content-Type", "application/json"))
270            .respond_with(ResponseTemplate::new(403).set_body_json(response_body))
271            .expect(0)
272            .mount(&mock_server)
273            .await;
274
275        let res = mangadex_client
276            .rating()
277            .manga_id(manga_id)
278            .post()
279            .rating(7)
280            .send()
281            .await
282            .expect_err("expected error");
283
284        match res {
285            Error::MissingTokens => {}
286            _ => panic!("unexpected error: {:#?}", res),
287        }
288
289        Ok(())
290    }
291}