mangadex_api/v5/manga/draft/
get.rs1use derive_builder::Builder;
42use serde::Serialize;
43use uuid::Uuid;
44
45use crate::HttpClientRef;
46use mangadex_api_schema::v5::MangaListResponse;
47use mangadex_api_types::{MangaDraftsSortOrder, MangaState, ReferenceExpansionResource};
48
49#[cfg_attr(
50 feature = "deserializable-endpoint",
51 derive(serde::Deserialize, getset::Getters, getset::Setters)
52)]
53#[derive(Debug, Serialize, Clone, Builder, Default)]
54#[serde(rename_all = "camelCase")]
55#[builder(
56 setter(into, strip_option),
57 default,
58 build_fn(error = "mangadex_api_types::error::BuilderError")
59)]
60#[cfg_attr(feature = "non_exhaustive", non_exhaustive)]
61pub struct ListMangaDrafts {
62 #[doc(hidden)]
63 #[serde(skip)]
64 #[builder(pattern = "immutable")]
65 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
66 pub http_client: HttpClientRef,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
74 pub limit: Option<u32>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub offset: Option<u32>,
78 #[deprecated(since = "1.2.1", note = "MangaDex removed this in 5.4.9 of their API")]
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub user: Option<Uuid>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub state: Option<MangaState>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub order: Option<MangaDraftsSortOrder>,
85 #[serde(skip_serializing_if = "Vec::is_empty")]
86 #[builder(setter(each = "include"))]
87 pub includes: Vec<ReferenceExpansionResource>,
88}
89
90endpoint! {
91 GET "/manga/draft",
92 #[query auth] ListMangaDrafts,
93 #[flatten_result] MangaListResponse,
94 ListMangaDraftsBuilder
95}
96
97#[cfg(test)]
98mod tests {
99 use serde_json::json;
100 use time::OffsetDateTime;
101 use url::Url;
102 use uuid::Uuid;
103 use wiremock::matchers::{header, method, path};
104 use wiremock::{Mock, MockServer, ResponseTemplate};
105
106 use crate::v5::AuthTokens;
107 use crate::{HttpClient, MangaDexClient};
108 use mangadex_api_types::error::Error;
109 use mangadex_api_types::{
110 ContentRating, Demographic, Language, MangaDexDateTime, MangaStatus, ResponseType,
111 };
112
113 #[tokio::test]
114 async fn list_manga_drafts_fires_a_request_to_base_url() -> anyhow::Result<()> {
115 let mock_server = MockServer::start().await;
116 let http_client: HttpClient = HttpClient::builder()
117 .base_url(Url::parse(&mock_server.uri())?)
118 .auth_tokens(AuthTokens {
119 session: "sessiontoken".to_string(),
120 refresh: "refreshtoken".to_string(),
121 })
122 .build()?;
123 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
124
125 let manga_id = Uuid::new_v4();
126 let manga_title = "Test Manga".to_string();
127
128 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
129
130 let response_body = json!({
131 "result": "ok",
132 "response": "collection",
133 "data": [
134 {
135 "id": manga_id,
136 "type": "manga",
137 "attributes": {
138 "title": {
139 "en": manga_title
140 },
141 "altTitles": [],
142 "description": {},
143 "isLocked": false,
144 "links": null,
145 "originalLanguage": "ja",
146 "lastVolume": null,
147 "lastChapter": null,
148 "publicationDemographic": "shoujo",
149 "status": "ongoing",
150 "year": null,
151 "contentRating": "safe",
152 "chapterNumbersResetOnNewVolume": true,
153 "availableTranslatedLanguages": ["en"],
154 "tags": [],
155 "state": "draft",
156 "createdAt": datetime.to_string(),
157 "updatedAt": datetime.to_string(),
158
159 "version": 1
160 },
161 "relationships": []
162 }
163 ],
164 "limit": 1,
165 "offset": 0,
166 "total": 1
167 });
168
169 Mock::given(method("GET"))
170 .and(path("/manga/draft"))
171 .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
172 .expect(1)
173 .mount(&mock_server)
174 .await;
175
176 let res = mangadex_client
177 .manga()
178 .draft()
179 .get()
180 .limit(1u32)
181 .send()
182 .await?;
183
184 assert_eq!(res.response, ResponseType::Collection);
185 let manga = &res.data[0];
186 assert_eq!(manga.id, manga_id);
187 assert_eq!(
188 manga.attributes.title.get(&Language::English).unwrap(),
189 &manga_title
190 );
191 assert!(manga.attributes.alt_titles.is_empty());
192 assert!(manga.attributes.description.is_empty());
193 assert!(!manga.attributes.is_locked);
194 assert_eq!(manga.attributes.links, None);
195 assert_eq!(manga.attributes.original_language, Language::Japanese);
196 assert_eq!(manga.attributes.last_volume, None);
197 assert_eq!(manga.attributes.last_chapter, None);
198 assert_eq!(
199 manga.attributes.publication_demographic.unwrap(),
200 Demographic::Shoujo
201 );
202 assert_eq!(manga.attributes.status, MangaStatus::Ongoing);
203 assert_eq!(manga.attributes.year, None);
204 assert_eq!(
205 manga.attributes.content_rating.unwrap(),
206 ContentRating::Safe
207 );
208 assert!(manga.attributes.tags.is_empty());
209 assert_eq!(
210 manga.attributes.created_at.to_string(),
211 datetime.to_string()
212 );
213 assert_eq!(
214 manga.attributes.updated_at.as_ref().unwrap().to_string(),
215 datetime.to_string()
216 );
217 assert_eq!(manga.attributes.version, 1);
218
219 Ok(())
220 }
221
222 #[tokio::test]
223 async fn list_manga_drafts_handles_400() -> anyhow::Result<()> {
224 let mock_server = MockServer::start().await;
225 let http_client: HttpClient = HttpClient::builder()
226 .base_url(Url::parse(&mock_server.uri())?)
227 .auth_tokens(AuthTokens {
228 session: "sessiontoken".to_string(),
229 refresh: "refreshtoken".to_string(),
230 })
231 .build()?;
232 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
233
234 let error_id = Uuid::new_v4();
235
236 let response_body = json!({
237 "result": "error",
238 "errors": [{
239 "id": error_id.to_string(),
240 "status": 400,
241 "title": "Invalid limit",
242 "detail": "Limit must be between 1 and 100"
243 }]
244 });
245
246 Mock::given(method("GET"))
247 .and(path("/manga/draft"))
248 .and(header("Authorization", "Bearer sessiontoken"))
249 .respond_with(ResponseTemplate::new(400).set_body_json(response_body))
250 .expect(1)
251 .mount(&mock_server)
252 .await;
253
254 let res = mangadex_client
255 .manga()
256 .draft()
257 .get()
258 .limit(0u32)
259 .send()
260 .await
261 .expect_err("expected error");
262
263 if let Error::Api(errors) = res {
264 assert_eq!(errors.errors.len(), 1);
265
266 assert_eq!(errors.errors[0].id, error_id);
267 assert_eq!(errors.errors[0].status, 400);
268 assert_eq!(errors.errors[0].title, Some("Invalid limit".to_string()));
269 assert_eq!(
270 errors.errors[0].detail,
271 Some("Limit must be between 1 and 100".to_string())
272 );
273 }
274
275 Ok(())
276 }
277
278 #[tokio::test]
279 async fn list_manga_drafts_requires_auth() -> anyhow::Result<()> {
280 let mock_server = MockServer::start().await;
281 let http_client: HttpClient = HttpClient::builder()
282 .base_url(Url::parse(&mock_server.uri())?)
283 .build()?;
284 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
285
286 let error_id = Uuid::new_v4();
287
288 let response_body = json!({
289 "result": "error",
290 "errors": [{
291 "id": error_id.to_string(),
292 "status": 403,
293 "title": "Forbidden",
294 "detail": "You must be logged in to continue."
295 }]
296 });
297
298 Mock::given(method("GET"))
299 .and(path("/manga/draft"))
300 .respond_with(ResponseTemplate::new(403).set_body_json(response_body))
301 .expect(0)
302 .mount(&mock_server)
303 .await;
304
305 let res = mangadex_client
306 .manga()
307 .draft()
308 .get()
309 .limit(0u32)
310 .send()
311 .await
312 .expect_err("expected error");
313
314 match res {
315 Error::MissingTokens => {}
316 _ => panic!("unexpected error: {:#?}", res),
317 }
318
319 Ok(())
320 }
321}