mangadex_api/v5/api_client/
get.rs1use derive_builder::Builder;
26use serde::Serialize;
27
28use crate::HttpClientRef;
29use mangadex_api_schema::v5::ApiClientListResponse;
30use mangadex_api_types::{ApiClientState, ReferenceExpansionResource};
31
32#[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}