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