mangadex_api/v5/user/follows/group/id/
get.rs

1//! Builder for checking if the logged-in user follows a scanlation group.
2//!
3//! <https://api.mangadex.org/docs/swagger.html#/Follows/get-user-follows-group-id>
4//!
5//! # Examples
6//!
7//! ```rust
8//! use uuid::Uuid;
9//!
10//! use mangadex_api::MangaDexClient;
11//! // use mangadex_api_types::{Password, Username};
12//!
13//! # async fn run() -> anyhow::Result<()> {
14//! let client = MangaDexClient::default();
15//!
16//! /*
17//!
18//!     let _login_res = client
19//!         .auth()
20//!         .login()
21//!         .post()
22//!         .username(Username::parse("myusername")?)
23//!         .password(Password::parse("hunter23")?)
24//!         .send()
25//!         .await?;
26//!
27//!  */
28//!
29//! let group_id = Uuid::new_v4();
30//! let res = client
31//!     .user()
32//!     .follows()
33//!     .group()
34//!     .id(group_id)
35//!     .get()
36//!     .send()
37//!     .await?;
38//!
39//! println!("check: {:?}", res);
40//! # Ok(())
41//! # }
42//! ```
43
44use derive_builder::Builder;
45use mangadex_api_schema::v5::IsFollowingResponse;
46use mangadex_api_schema::{FromResponse, NoData};
47use serde::Serialize;
48use uuid::Uuid;
49
50use crate::HttpClientRef;
51use mangadex_api_types::error::{Error, Result};
52
53/// Check if the logged-in user follows a scanlation group.
54///
55/// Makes a request to `GET /user/follows/group/{id}`.
56#[cfg_attr(
57    feature = "deserializable-endpoint",
58    derive(serde::Deserialize, getset::Getters, getset::Setters)
59)]
60#[derive(Debug, Builder, Serialize, Clone)]
61#[serde(rename_all = "camelCase")]
62#[builder(
63    setter(into, strip_option),
64    build_fn(error = "mangadex_api_types::error::BuilderError")
65)]
66#[cfg_attr(
67    feature = "custom_list_v2",
68    deprecated(
69        since = "3.0.0-rc.1",
70        note = "After the introduction of the Subscription system, this endpoint will be removed in v3"
71    )
72)]
73pub struct IsFollowingGroup {
74    /// This should never be set manually as this is only for internal use.
75    #[doc(hidden)]
76    #[serde(skip)]
77    #[builder(pattern = "immutable")]
78    #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
79    pub http_client: HttpClientRef,
80
81    pub group_id: Uuid,
82}
83
84impl IsFollowingGroup {
85    pub async fn send(&mut self) -> Result<IsFollowingResponse> {
86        #[cfg(all(
87            not(feature = "multi-thread"),
88            not(feature = "tokio-multi-thread"),
89            not(feature = "rw-multi-thread")
90        ))]
91        let res = self
92            .http_client
93            .try_borrow()?
94            .send_request_without_deserializing(self)
95            .await?;
96        #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
97        let res = self
98            .http_client
99            .lock()
100            .await
101            .send_request_without_deserializing(self)
102            .await?;
103        #[cfg(feature = "rw-multi-thread")]
104        let res = self
105            .http_client
106            .read()
107            .await
108            .send_request_without_deserializing(self)
109            .await?;
110
111        match res.status() {
112            reqwest::StatusCode::OK => Ok(IsFollowingResponse { is_following: true }),
113            reqwest::StatusCode::NOT_FOUND => {
114                let result = res
115                    .json::<<Result<NoData> as FromResponse>::Response>()
116                    .await?;
117                match result.into_result() {
118                    Ok(_) => Ok(IsFollowingResponse {
119                        is_following: false,
120                    }),
121                    Err(err) => Err(Error::Api(err)),
122                }
123            }
124            other_status => Err(Error::ServerError(other_status.as_u16(), res.text().await?)),
125        }
126    }
127}
128
129endpoint! {
130    GET ("/user/follows/group/{}", group_id),
131    #[no_data auth] IsFollowingGroup,
132    #[no_send] Result<IsFollowingResponse>
133}
134
135builder_send! {
136    #[builder] IsFollowingGroupBuilder,
137    IsFollowingResponse
138}
139
140#[cfg(test)]
141mod tests {
142    use mangadex_api_types::error::Error;
143    use serde_json::json;
144    use url::Url;
145    use uuid::Uuid;
146    use wiremock::matchers::{header, method, path_regex};
147    use wiremock::{Mock, MockServer, ResponseTemplate};
148
149    use crate::v5::AuthTokens;
150    use crate::{HttpClient, MangaDexClient};
151
152    #[tokio::test]
153    async fn user_follows_scanlation_group() -> anyhow::Result<()> {
154        let mock_server = MockServer::start().await;
155        let http_client: HttpClient = HttpClient::builder()
156            .base_url(Url::parse(&mock_server.uri())?)
157            .auth_tokens(AuthTokens {
158                session: "sessiontoken".to_string(),
159                refresh: "refreshtoken".to_string(),
160            })
161            .build()?;
162        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
163
164        let scanlation_group_id = Uuid::new_v4();
165        let response_body = json!({
166            "result": "ok"
167        });
168
169        Mock::given(method("GET"))
170            .and(path_regex(r"/user/follows/group/[0-9a-fA-F-]+"))
171            .and(header("Authorization", "Bearer sessiontoken"))
172            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
173            .expect(1)
174            .mount(&mock_server)
175            .await;
176
177        let res = mangadex_client
178            .user()
179            .follows()
180            .group()
181            .id(scanlation_group_id)
182            .get()
183            .send()
184            .await?;
185
186        assert!(res.is_following);
187
188        Ok(())
189    }
190
191    #[tokio::test]
192    async fn user_does_not_follow_scanlation_group() -> anyhow::Result<()> {
193        let mock_server = MockServer::start().await;
194        let http_client: HttpClient = HttpClient::builder()
195            .base_url(Url::parse(&mock_server.uri())?)
196            .auth_tokens(AuthTokens {
197                session: "sessiontoken".to_string(),
198                refresh: "refreshtoken".to_string(),
199            })
200            .build()?;
201        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
202
203        let scanlation_group_id = Uuid::new_v4();
204        let response_body = json!({
205            "result": "ok"
206        });
207
208        Mock::given(method("GET"))
209            .and(path_regex(r"/user/follows/group/[0-9a-fA-F-]+"))
210            .and(header("Authorization", "Bearer sessiontoken"))
211            .respond_with(ResponseTemplate::new(404).set_body_json(response_body))
212            .expect(1)
213            .mount(&mock_server)
214            .await;
215
216        let res = mangadex_client
217            .user()
218            .follows()
219            .group()
220            .id(scanlation_group_id)
221            .get()
222            .send()
223            .await?;
224
225        assert!(!res.is_following);
226
227        Ok(())
228    }
229
230    #[tokio::test]
231    async fn scanlation_group_does_not_exist() -> anyhow::Result<()> {
232        let mock_server = MockServer::start().await;
233        let http_client: HttpClient = HttpClient::builder()
234            .base_url(Url::parse(&mock_server.uri())?)
235            .auth_tokens(AuthTokens {
236                session: "sessiontoken".to_string(),
237                refresh: "refreshtoken".to_string(),
238            })
239            .build()?;
240        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
241
242        let scanlation_group_id = Uuid::new_v4();
243        let error_id = Uuid::new_v4();
244        let response_body = json!({
245            "result": "error",
246            "errors": [{
247                "id": error_id.to_string(),
248                "status": 404,
249                "title": "Scanlation group does not exist",
250                "detail": "The provided scanlation group does not exist or has been deleted"
251            }]
252        });
253
254        Mock::given(method("GET"))
255            .and(path_regex(r"/user/follows/group/[0-9a-fA-F-]+"))
256            .and(header("Authorization", "Bearer sessiontoken"))
257            .respond_with(ResponseTemplate::new(404).set_body_json(response_body))
258            .expect(1)
259            .mount(&mock_server)
260            .await;
261
262        let res = mangadex_client
263            .user()
264            .follows()
265            .group()
266            .id(scanlation_group_id)
267            .get()
268            .send()
269            .await
270            .unwrap_err();
271
272        match res {
273            Error::Api(error_res) => {
274                assert_eq!(error_res.errors.len(), 1);
275                assert_eq!(error_res.errors[0].status, 404);
276                assert_eq!(
277                    error_res.errors[0].title.as_ref().unwrap(),
278                    "Scanlation group does not exist"
279                );
280            }
281            _ => panic!("did not get Error::Api"),
282        }
283
284        Ok(())
285    }
286}