mangadex_api/v5/chapter/id/
put.rs1use derive_builder::Builder;
43use serde::Serialize;
44use uuid::Uuid;
45
46use crate::HttpClientRef;
47use mangadex_api_schema::v5::ChapterData;
48use mangadex_api_types::Language;
49
50#[cfg_attr(
51 feature = "deserializable-endpoint",
52 derive(serde::Deserialize, getset::Getters, getset::Setters)
53)]
54#[derive(Debug, Serialize, Clone, Builder)]
55#[serde(rename_all = "camelCase")]
56#[builder(
57 setter(into, strip_option),
58 build_fn(error = "mangadex_api_types::error::BuilderError")
59)]
60#[cfg_attr(feature = "non_exhaustive", non_exhaustive)]
61pub struct UpdateChapter {
62 #[doc(hidden)]
64 #[serde(skip)]
65 #[builder(pattern = "immutable")]
66 #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
67 pub http_client: HttpClientRef,
68
69 #[serde(skip_serializing)]
70 pub chapter_id: Uuid,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
76 #[builder(default)]
77 pub title: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
82 #[builder(default)]
83 pub volume: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
90 #[builder(default)]
91 pub chapter: Option<Option<String>>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 #[builder(default)]
94 pub translated_language: Option<Language>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 #[builder(default)]
97 pub groups: Option<Vec<Uuid>>,
98 pub version: u32,
100}
101
102endpoint! {
103 PUT ("/chapter/{}", chapter_id),
104 #[body auth] UpdateChapter,
105 #[rate_limited] ChapterData,
106 UpdateChapterBuilder
107}
108
109#[cfg(test)]
110mod tests {
111 use fake::faker::name::en::Name;
112 use fake::Fake;
113 use serde_json::json;
114 use time::OffsetDateTime;
115 use url::Url;
116 use uuid::Uuid;
117 use wiremock::matchers::{body_json, header, method, path_regex};
118 use wiremock::{Mock, MockServer, ResponseTemplate};
119
120 use crate::v5::AuthTokens;
121 use crate::{HttpClient, MangaDexClient};
122 use mangadex_api_types::MangaDexDateTime;
123
124 #[tokio::test]
125 async fn update_chapter_fires_a_request_to_base_url() -> anyhow::Result<()> {
126 let mock_server = MockServer::start().await;
127 let http_client = HttpClient::builder()
128 .base_url(Url::parse(&mock_server.uri())?)
129 .auth_tokens(AuthTokens {
130 session: "sessiontoken".to_string(),
131 refresh: "refreshtoken".to_string(),
132 })
133 .build()?;
134 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
135
136 let chapter_id = Uuid::new_v4();
137 let uploader_id = Uuid::new_v4();
138 let chapter_title: String = Name().fake();
139
140 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
141
142 let expected_body = json!({
143 "version": 2
144 });
145 let response_body = json!({
146 "result": "ok",
147 "response": "entity",
148 "data": {
149 "id": chapter_id,
150 "type": "chapter",
151 "attributes": {
152 "title": chapter_title,
153 "volume": "1",
154 "chapter": "1.5",
155 "pages": 4,
156 "translatedLanguage": "en",
157 "uploader": uploader_id,
158 "version": 1,
159 "createdAt": datetime.to_string(),
160 "updatedAt": datetime.to_string(),
161 "publishAt": datetime.to_string(),
162 "readableAt": datetime.to_string(),
163 },
164 "relationships": []
165 }
166 });
167
168 Mock::given(method("PUT"))
169 .and(path_regex(r"/chapter/[0-9a-fA-F-]+"))
170 .and(header("Authorization", "Bearer sessiontoken"))
171 .and(header("Content-Type", "application/json"))
172 .and(body_json(expected_body))
173 .respond_with(
174 ResponseTemplate::new(200)
175 .insert_header("x-ratelimit-retry-after", "1698723860")
176 .insert_header("x-ratelimit-limit", "40")
177 .insert_header("x-ratelimit-remaining", "39")
178 .set_body_json(response_body),
179 )
180 .expect(1)
181 .mount(&mock_server)
182 .await;
183
184 let _ = mangadex_client
185 .chapter()
186 .id(chapter_id)
187 .put()
188 .version(2_u32)
189 .send()
190 .await?;
191
192 Ok(())
193 }
194
195 #[tokio::test]
196 async fn update_chapter_does_not_include_title_when_not_used() -> anyhow::Result<()> {
197 let mock_server = MockServer::start().await;
198 let http_client = HttpClient::builder()
199 .base_url(Url::parse(&mock_server.uri())?)
200 .auth_tokens(AuthTokens {
201 session: "sessiontoken".to_string(),
202 refresh: "refreshtoken".to_string(),
203 })
204 .build()?;
205 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
206
207 let chapter_id = Uuid::new_v4();
208 let uploader_id = Uuid::new_v4();
209 let chapter_title: String = Name().fake();
210
211 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
212
213 let expected_body = json!({
214 "version": 2
215 });
216 let response_body = json!({
217 "result": "ok",
218 "response": "entity",
219 "data": {
220 "id": chapter_id,
221 "type": "chapter",
222 "attributes": {
223 "title": chapter_title,
224 "volume": "1",
225 "chapter": "1.5",
226 "pages": 4,
227 "translatedLanguage": "en",
228 "uploader": uploader_id,
229 "version": 1,
230 "createdAt": datetime.to_string(),
231 "updatedAt": datetime.to_string(),
232 "publishAt": datetime.to_string(),
233 "readableAt": datetime.to_string(),
234 },
235 "relationships": []
236 }
237 });
238
239 Mock::given(method("PUT"))
240 .and(path_regex(r"/chapter/[0-9a-fA-F-]+"))
241 .and(header("Authorization", "Bearer sessiontoken"))
242 .and(header("Content-Type", "application/json"))
243 .and(body_json(expected_body))
244 .respond_with(
245 ResponseTemplate::new(200)
246 .insert_header("x-ratelimit-retry-after", "1698723860")
247 .insert_header("x-ratelimit-limit", "40")
248 .insert_header("x-ratelimit-remaining", "39")
249 .set_body_json(response_body),
250 )
251 .expect(1)
252 .mount(&mock_server)
253 .await;
254
255 let _ = mangadex_client
256 .chapter()
257 .id(chapter_id)
258 .put()
259 .version(2_u32)
260 .send()
261 .await?;
262
263 Ok(())
264 }
265
266 #[tokio::test]
267 async fn update_chapter_sends_null_title() -> anyhow::Result<()> {
268 let mock_server = MockServer::start().await;
269 let http_client = HttpClient::builder()
270 .base_url(Url::parse(&mock_server.uri())?)
271 .auth_tokens(AuthTokens {
272 session: "sessiontoken".to_string(),
273 refresh: "refreshtoken".to_string(),
274 })
275 .build()?;
276 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
277
278 let chapter_id = Uuid::new_v4();
279 let uploader_id = Uuid::new_v4();
280
281 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
282
283 let expected_body = json!({
284 "version": 2
286 });
287 let response_body = json!({
288 "result": "ok",
289 "response": "entity",
290 "data": {
291 "id": chapter_id,
292 "type": "chapter",
293 "attributes": {
294 "title": null,
295 "volume": "1",
296 "chapter": "1.5",
297 "pages": 4,
298 "translatedLanguage": "en",
299 "uploader": uploader_id,
300 "version": 1,
301 "createdAt": datetime.to_string(),
302 "updatedAt": datetime.to_string(),
303 "publishAt": datetime.to_string(),
304 "readableAt": datetime.to_string(),
305 },
306 "relationships": []
307 }
308 });
309
310 Mock::given(method("PUT"))
311 .and(path_regex(r"/chapter/[0-9a-fA-F-]+"))
312 .and(header("Authorization", "Bearer sessiontoken"))
313 .and(header("Content-Type", "application/json"))
314 .and(body_json(expected_body))
315 .respond_with(
316 ResponseTemplate::new(200)
317 .insert_header("x-ratelimit-retry-after", "1698723860")
318 .insert_header("x-ratelimit-limit", "40")
319 .insert_header("x-ratelimit-remaining", "39")
320 .set_body_json(response_body),
321 )
322 .expect(1)
323 .mount(&mock_server)
324 .await;
325
326 let _ = mangadex_client
327 .chapter()
328 .id(chapter_id)
329 .put()
330 .version(2_u32)
331 .send()
332 .await?;
333
334 Ok(())
335 }
336
337 #[tokio::test]
338 async fn update_chapter_sends_title_with_value() -> anyhow::Result<()> {
339 let mock_server = MockServer::start().await;
340 let http_client = HttpClient::builder()
341 .base_url(Url::parse(&mock_server.uri())?)
342 .auth_tokens(AuthTokens {
343 session: "sessiontoken".to_string(),
344 refresh: "refreshtoken".to_string(),
345 })
346 .build()?;
347 let mangadex_client = MangaDexClient::new_with_http_client(http_client);
348
349 let chapter_id = Uuid::new_v4();
350 let uploader_id = Uuid::new_v4();
351 let chapter_title: String = Name().fake();
352
353 let datetime = MangaDexDateTime::new(&OffsetDateTime::now_utc());
354
355 let expected_body = json!({
356 "title": chapter_title,
357 "version": 2
358 });
359 let response_body = json!({
360 "result": "ok",
361 "response": "entity",
362 "data": {
363 "id": chapter_id,
364 "type": "chapter",
365 "attributes": {
366 "title": chapter_title,
367 "volume": "1",
368 "chapter": "1.5",
369 "pages": 4,
370 "translatedLanguage": "en",
371 "uploader": uploader_id,
372 "version": 1,
373 "createdAt": datetime.to_string(),
374 "updatedAt": datetime.to_string(),
375 "publishAt": datetime.to_string(),
376 "readableAt": datetime.to_string(),
377 },
378 "relationships": []
379 }
380 });
381
382 Mock::given(method("PUT"))
383 .and(path_regex(r"/chapter/[0-9a-fA-F-]+"))
384 .and(header("Authorization", "Bearer sessiontoken"))
385 .and(header("Content-Type", "application/json"))
386 .and(body_json(expected_body))
387 .respond_with(
388 ResponseTemplate::new(200)
389 .insert_header("x-ratelimit-retry-after", "1698723860")
390 .insert_header("x-ratelimit-limit", "40")
391 .insert_header("x-ratelimit-remaining", "39")
392 .set_body_json(response_body),
393 )
394 .expect(1)
395 .mount(&mock_server)
396 .await;
397
398 let _ = mangadex_client
399 .chapter()
400 .id(chapter_id)
401 .put()
402 .title(chapter_title.as_str())
403 .version(2_u32)
404 .send()
405 .await?;
406
407 Ok(())
408 }
409}