mangadex_api/v5/captcha/solve/
post.rs

1//! Builder for the CAPTCHA solve endpoint.
2//!
3//! <https://api.mangadex.org/docs/swagger.html#/Captcha/post-captcha-solve>
4//!
5//! Captchas can be solved explicitly through this endpoint, another way is to add a
6//! `X-Captcha-Result` header to any request.
7//! The same logic will verify the captcha and is probably more convenient because it takes one less request.
8//!
9//! Authentication is optional.
10//! Captchas are tracked for both the client IP and for the user ID, if you are logged in,
11//! you want to send your session token but that is not required.
12//!
13//! # Examples
14//!
15//! ```rust
16//! use mangadex_api::v5::MangaDexClient;
17//!
18//! # async fn run() -> anyhow::Result<()> {
19//! let client = MangaDexClient::default();
20//!
21//! let captcha_res = client
22//!     .captcha()
23//!     .solve()
24//!     .post()
25//!     .captcha_challenge("specialchallengetoken")
26//!     .send()
27//!     .await?;
28//!
29//! println!("captcha solve: {:?}", captcha_res);
30//! # Ok(())
31//! # }
32//! ```
33
34use derive_builder::Builder;
35use serde::Serialize;
36
37use crate::HttpClientRef;
38use mangadex_api_schema::NoData;
39
40/// Mark a chapter as read for the current user.
41///
42/// Makes a request to `POST /captcha/solve`.
43#[cfg_attr(
44    feature = "deserializable-endpoint",
45    derive(serde::Deserialize, getset::Getters, getset::Setters)
46)]
47#[derive(Debug, Serialize, Clone, Builder)]
48#[serde(rename_all = "camelCase")]
49#[builder(
50    setter(into, strip_option),
51    build_fn(error = "mangadex_api_types::error::BuilderError")
52)]
53pub struct SolveCaptcha {
54    /// This should never be set manually as this is only for internal use.
55    #[doc(hidden)]
56    #[serde(skip)]
57    #[builder(pattern = "immutable")]
58    #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
59    pub http_client: HttpClientRef,
60
61    pub captcha_challenge: String,
62}
63
64endpoint! {
65    POST "/captcha/solve",
66    #[body] SolveCaptcha,
67    #[rate_limited] NoData,
68    SolveCaptchaBuilder
69}
70
71#[cfg(test)]
72mod tests {
73    use serde_json::json;
74    use url::Url;
75    use wiremock::matchers::{body_json, header, method, path};
76    use wiremock::{Mock, MockServer, ResponseTemplate};
77
78    use crate::{HttpClient, MangaDexClient};
79    use mangadex_api_types::error::Error;
80
81    #[tokio::test]
82    async fn solve_captcha_fires_a_request_to_base_url() -> anyhow::Result<()> {
83        let mock_server = MockServer::start().await;
84        let http_client: HttpClient = HttpClient::builder()
85            .base_url(Url::parse(&mock_server.uri())?)
86            .build()?;
87        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
88
89        let expected_body = json!({
90            "captchaChallenge": "solution",
91        });
92        let response_body = json!({"result": "ok"});
93
94        Mock::given(method("POST"))
95            .and(path(r"/captcha/solve"))
96            .and(header("Content-Type", "application/json"))
97            .and(body_json(expected_body))
98            .respond_with(
99                ResponseTemplate::new(200)
100                    .insert_header("x-ratelimit-retry-after", "1698723860")
101                    .insert_header("x-ratelimit-limit", "40")
102                    .insert_header("x-ratelimit-remaining", "39")
103                    .set_body_json(response_body),
104            )
105            .expect(1)
106            .mount(&mock_server)
107            .await;
108
109        mangadex_client
110            .captcha()
111            .solve()
112            .post()
113            .captcha_challenge("solution")
114            .send()
115            .await?;
116
117        Ok(())
118    }
119
120    #[tokio::test]
121    async fn solve_captcha_handles_400() -> anyhow::Result<()> {
122        let mock_server = MockServer::start().await;
123        let http_client: HttpClient = HttpClient::builder()
124            .base_url(Url::parse(&mock_server.uri())?)
125            .build()?;
126        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
127
128        let expected_body = json!({
129            "captchaChallenge": "solution",
130        });
131        let response_body = json!({
132            "result": "error",
133            "errors": []
134        });
135
136        Mock::given(method("POST"))
137            .and(path(r"/captcha/solve"))
138            .and(header("Content-Type", "application/json"))
139            .and(body_json(expected_body))
140            .respond_with(
141                ResponseTemplate::new(400)
142                    .insert_header("x-ratelimit-retry-after", "1698723860")
143                    .insert_header("x-ratelimit-limit", "40")
144                    .insert_header("x-ratelimit-remaining", "39")
145                    .set_body_json(response_body),
146            )
147            .expect(1)
148            .mount(&mock_server)
149            .await;
150
151        let res = mangadex_client
152            .captcha()
153            .solve()
154            .post()
155            .captcha_challenge("solution")
156            .send()
157            .await
158            .expect_err("expected error");
159
160        if let Error::Api(errors) = res {
161            assert_eq!(errors.errors.len(), 0);
162        }
163
164        Ok(())
165    }
166}