mangadex_api/v5/api_client/
get.rs

1//! Builder for the client list endpoint.
2//!
3//! <https://api.mangadex.org/docs/swagger.html#/ApiClient/get-list-apiclients>
4//! <https://api.mangadex.org/docs/redoc.html#tag/ApiClient/operation/get-list-apiclients>
5//!
6//! # Examples
7//!
8//! ```rust
9//! use mangadex_api::v5::MangaDexClient;
10//!
11//! # async fn run() -> anyhow::Result<()> {
12//! let client = MangaDexClient::default();
13//!
14//! let client_res = client
15//!     .client()
16//!     .get()
17//!     .send()
18//!     .await?;
19//!
20//! println!("Clients : {:?}", client_res);
21//! # Ok(())
22//! # }
23//! ```
24
25use derive_builder::Builder;
26use serde::Serialize;
27
28use crate::HttpClientRef;
29use mangadex_api_schema::v5::ApiClientListResponse;
30use mangadex_api_types::{ApiClientState, ReferenceExpansionResource};
31
32// Make a request to `GET /client`
33#[cfg_attr(
34    feature = "deserializable-endpoint",
35    derive(serde::Deserialize, getset::Getters, getset::Setters)
36)]
37#[derive(Debug, Serialize, Clone, Builder, Default)]
38#[serde(rename_all = "camelCase")]
39#[builder(
40    setter(into, strip_option),
41    default,
42    build_fn(error = "mangadex_api_types::error::BuilderError")
43)]
44#[cfg_attr(feature = "non_exhaustive", non_exhaustive)]
45pub struct ListClients {
46    #[doc(hidden)]
47    #[serde(skip)]
48    #[builder(pattern = "immutable")]
49    #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
50    pub http_client: HttpClientRef,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    #[builder(default)]
53    pub limit: Option<u32>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    #[builder(default)]
56    pub offset: Option<u32>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    #[builder(default)]
59    pub state: Option<ApiClientState>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    #[builder(default)]
62    pub name: Option<String>,
63    #[serde(skip_serializing_if = "Vec::is_empty")]
64    #[builder(default)]
65    pub includes: Vec<ReferenceExpansionResource>,
66}
67
68endpoint! {
69    GET "/client",
70    #[query auth] ListClients,
71    #[flatten_result] ApiClientListResponse,
72    ListClientsBuilder
73}
74
75#[cfg(test)]
76mod tests {
77    use mangadex_api_schema::v5::AuthTokens;
78    use serde_json::json;
79    use time::OffsetDateTime;
80    use url::Url;
81    use uuid::Uuid;
82    use wiremock::matchers::{header, method, path};
83    use wiremock::{Mock, MockServer, ResponseTemplate};
84
85    use crate::{HttpClient, MangaDexClient};
86    use mangadex_api_types::error::Error;
87    use mangadex_api_types::{
88        ApiClientProfile, ApiClientState, MangaDexDateTime, RelationshipType, ResponseType,
89    };
90
91    #[tokio::test]
92    async fn list_client_fires_a_request_to_base_url() -> anyhow::Result<()> {
93        let mock_server = MockServer::start().await;
94        let http_client = HttpClient::builder()
95            .base_url(Url::parse(&mock_server.uri())?)
96            .auth_tokens(AuthTokens {
97                session: "myToken".to_string(),
98                refresh: "myRefreshToken".to_string(),
99            })
100            .build()?;
101        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
102
103        let client_id = Uuid::new_v4();
104        let client_name = "Test Client".to_string();
105        let client_description = "A local test client for the Mangadex API".to_string();
106        let state = ApiClientState::Requested;
107        let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
108
109        let response_body = json!({
110            "result": "ok",
111            "response": "collection",
112            "data": [
113                {
114                    "id": client_id,
115                    "type": "api_client",
116                    "attributes": {
117                        "name": client_name,
118                        "description": client_description,
119                        "profile": "personal",
120                        "externalClientId": null,
121                        "isActive": false,
122                        "state": state,
123                        "createdAt": datetime.to_string(),
124                        "updatedAt": datetime.to_string(),
125                        "version": 1
126                    },
127                    "relationships": []
128                }
129            ],
130            "limit": 1,
131            "offset": 0,
132            "total": 1
133        });
134
135        Mock::given(method("GET"))
136            .and(path("/client"))
137            .and(header("Authorization", "Bearer myToken"))
138            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
139            .expect(1)
140            .mount(&mock_server)
141            .await;
142
143        let res = mangadex_client.client().get().limit(1u32).send().await?;
144
145        assert_eq!(res.response, ResponseType::Collection);
146        let client: &mangadex_api_schema::ApiObject<mangadex_api_schema::v5::ApiClientAttributes> =
147            &res.data[0];
148        assert_eq!(client.id, client_id);
149        assert_eq!(client.type_, RelationshipType::ApiClient);
150        assert_eq!(client.attributes.name, client_name);
151        assert_eq!(client.attributes.description, Some(client_description));
152        assert_eq!(client.attributes.profile, ApiClientProfile::Personal);
153        assert_eq!(client.attributes.external_client_id, None);
154        assert!(!client.attributes.is_active);
155        assert_eq!(client.attributes.state, state);
156        assert_eq!(
157            client.attributes.created_at.to_string(),
158            datetime.to_string()
159        );
160        assert_eq!(
161            client.attributes.updated_at.to_string(),
162            datetime.to_string()
163        );
164        assert_eq!(client.attributes.version, 1);
165        Ok(())
166    }
167
168    #[tokio::test]
169    async fn list_client_handles_400() -> anyhow::Result<()> {
170        let mock_server = MockServer::start().await;
171        let http_client: HttpClient = HttpClient::builder()
172            .base_url(Url::parse(&mock_server.uri())?)
173            .auth_tokens(AuthTokens {
174                session: "myToken".to_string(),
175                refresh: "myRefreshToken".to_string(),
176            })
177            .build()?;
178        let mangadex_client = MangaDexClient::new_with_http_client(http_client);
179
180        let error_id = Uuid::new_v4();
181
182        let response_body = json!({
183            "result": "error",
184            "errors": [{
185                "id": error_id.to_string(),
186                "status": 400,
187                "title": "Invalid limit",
188                "detail": "Limit must be between 1 and 100"
189            }]
190        });
191
192        Mock::given(method("GET"))
193            .and(path("/client"))
194            .respond_with(ResponseTemplate::new(400).set_body_json(response_body))
195            .expect(1)
196            .mount(&mock_server)
197            .await;
198
199        let res = mangadex_client
200            .client()
201            .get()
202            .limit(0u32)
203            .send()
204            .await
205            .expect_err("expected error");
206
207        if let Error::Api(errors) = res {
208            assert_eq!(errors.errors.len(), 1);
209
210            assert_eq!(errors.errors[0].id, error_id);
211            assert_eq!(errors.errors[0].status, 400);
212            assert_eq!(errors.errors[0].title, Some("Invalid limit".to_string()));
213            assert_eq!(
214                errors.errors[0].detail,
215                Some("Limit must be between 1 and 100".to_string())
216            );
217        }
218
219        Ok(())
220    }
221}