1use anyhow::Result;
2use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
3use serde::{Deserialize, Serialize};
4use serde_json::json;
5
6use super::SmugMugClient;
7
8#[derive(Debug, Serialize, Deserialize, Clone)]
9pub struct AlbumImage {
10 #[serde(rename = "ImageKey")]
11 pub image_key: String,
12 #[serde(rename = "FileName")]
13 pub file_name: String,
14 #[serde(rename = "ArchivedUri")]
15 pub archived_uri: String,
16 #[serde(rename = "ArchivedSize")]
17 pub file_size: u64,
18 #[serde(rename = "Format")]
19 pub format: String,
20 #[serde(rename = "Uri")]
21 pub uri: String,
22 #[serde(rename = "Title", skip_serializing_if = "Option::is_none")]
23 pub title: Option<String>,
24 #[serde(rename = "ArchivedMD5", skip_serializing_if = "Option::is_none")]
25 pub archived_md5: Option<String>,
26}
27
28#[derive(Debug, Serialize, Deserialize, Clone)]
29pub struct ImageDetails {
30 #[serde(rename = "ImageKey")]
31 pub image_key: String,
32 #[serde(rename = "FileName")]
33 pub file_name: String,
34 #[serde(rename = "Format")]
35 pub format: String,
36 #[serde(rename = "ArchivedSize")]
37 pub file_size: u64,
38 #[serde(rename = "Title", skip_serializing_if = "Option::is_none")]
39 pub title: Option<String>,
40 #[serde(rename = "Caption", skip_serializing_if = "Option::is_none")]
41 pub caption: Option<String>,
42 #[serde(rename = "Keywords", skip_serializing_if = "Option::is_none")]
43 pub keywords: Option<String>,
44 #[serde(rename = "Latitude", skip_serializing_if = "Option::is_none")]
45 pub latitude: Option<f64>,
46 #[serde(rename = "Longitude", skip_serializing_if = "Option::is_none")]
47 pub longitude: Option<f64>,
48 #[serde(rename = "Altitude", skip_serializing_if = "Option::is_none")]
49 pub altitude: Option<f64>,
50 #[serde(rename = "ArchivedUri")]
51 pub archived_uri: String,
52 #[serde(rename = "ArchivedMD5", skip_serializing_if = "Option::is_none")]
53 pub archived_md5: Option<String>,
54 #[serde(rename = "UploadKey", skip_serializing_if = "Option::is_none")]
55 pub upload_key: Option<String>,
56 #[serde(rename = "Uri")]
57 pub uri: String,
58 #[serde(rename = "WebUri", skip_serializing_if = "Option::is_none")]
59 pub web_uri: Option<String>,
60}
61
62#[derive(Debug, Deserialize)]
63struct ImageDetailsResponse {
64 #[serde(rename = "Response")]
65 response: ImageDetailsResponseData,
66}
67
68#[derive(Debug, Deserialize)]
69struct ImageDetailsResponseData {
70 #[serde(rename = "Image")]
71 image: ImageDetails,
72}
73
74#[derive(Debug, Default)]
75pub struct ImageMetadataUpdate {
76 pub caption: Option<String>,
77 pub title: Option<String>,
78 pub keywords: Option<String>,
79 pub latitude: Option<f64>,
80 pub longitude: Option<f64>,
81}
82
83impl SmugMugClient {
84 pub async fn list_album_images(&self, album_key: &str) -> Result<Vec<AlbumImage>> {
86 let images_url = format!("https://api.smugmug.com/api/v2/album/{}!images", album_key);
87 self.get_all_pages(&images_url, "AlbumImage").await
88 }
89
90 pub async fn get_image_details(&self, image_key: &str) -> Result<ImageDetails> {
91 let image_url = format!("https://api.smugmug.com/api/v2/image/{}", image_key);
92 let oauth_header = self.build_oauth_header("GET", &image_url);
93
94 let mut headers = HeaderMap::new();
95 headers.insert(AUTHORIZATION, HeaderValue::from_str(&oauth_header)?);
96 headers.insert("Accept", HeaderValue::from_static("application/json"));
97
98 let response = self.client.get(&image_url).headers(headers).send().await?;
99
100 let status = response.status();
101 let body_text = response.text().await?;
102
103 if !status.is_success() {
104 anyhow::bail!("Failed to get image details: {} - {}", status, body_text);
105 }
106
107 let image_data: ImageDetailsResponse = serde_json::from_str(&body_text)?;
108 Ok(image_data.response.image)
109 }
110
111 pub async fn delete_image(&self, image_key: &str) -> Result<()> {
112 let delete_url = format!("https://api.smugmug.com/api/v2/image/{}", image_key);
113 let response = self.delete_with_auth(&delete_url).await?;
114
115 let status = response.status();
116
117 if !status.is_success() {
118 let body_text = response.text().await?;
119 anyhow::bail!("Failed to delete image: {} - {}", status, body_text);
120 }
121
122 Ok(())
123 }
124
125 pub async fn update_image_metadata(
126 &self,
127 image_key: &str,
128 metadata: ImageMetadataUpdate,
129 ) -> Result<()> {
130 let update_url = format!("https://api.smugmug.com/api/v2/image/{}", image_key);
131
132 let mut body = json!({});
134
135 if let Some(caption) = metadata.caption {
136 body["Caption"] = json!(caption);
137 }
138
139 if let Some(title) = metadata.title {
140 body["Title"] = json!(title);
141 }
142
143 if let Some(keywords) = metadata.keywords {
144 body["Keywords"] = json!(keywords);
145 }
146
147 if let Some(latitude) = metadata.latitude {
148 body["Latitude"] = json!(latitude);
149 }
150
151 if let Some(longitude) = metadata.longitude {
152 body["Longitude"] = json!(longitude);
153 }
154
155 let response = self.patch_with_auth(&update_url, body).await?;
156
157 let status = response.status();
158 let body_text = response.text().await?;
159
160 if !status.is_success() {
161 anyhow::bail!(
162 "Failed to update image metadata: {} - {}",
163 status,
164 body_text
165 );
166 }
167
168 Ok(())
169 }
170
171 pub async fn move_image(&self, image_key: &str, target_album_key: &str) -> Result<()> {
172 let move_url = format!("https://api.smugmug.com/api/v2/image/{}", image_key);
173
174 let album_uri = format!("/api/v2/album/{}", target_album_key);
176 let body = json!({
177 "AlbumUri": album_uri
178 });
179
180 let response = self.patch_with_auth(&move_url, body).await?;
181
182 let status = response.status();
183 let body_text = response.text().await?;
184
185 if !status.is_success() {
186 anyhow::bail!("Failed to move image: {} - {}", status, body_text);
187 }
188
189 Ok(())
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 fn create_test_client() -> SmugMugClient {
198 SmugMugClient::new(
199 "test_api_key".to_string(),
200 "test_api_secret".to_string(),
201 "test_access_token".to_string(),
202 "test_access_token_secret".to_string(),
203 )
204 }
205
206 #[test]
207 fn test_album_image_serialization() {
208 let image = AlbumImage {
209 image_key: "IMG123".to_string(),
210 file_name: "test.jpg".to_string(),
211 archived_uri: "https://example.com/test.jpg".to_string(),
212 file_size: 1024,
213 format: "JPG".to_string(),
214 uri: "/api/v2/image/IMG123".to_string(),
215 title: Some("Test Image".to_string()),
216 archived_md5: Some("abc123def456".to_string()),
217 };
218
219 let json = serde_json::to_string(&image).unwrap();
220 assert!(json.contains("\"ImageKey\":\"IMG123\""));
221 assert!(json.contains("\"FileName\":\"test.jpg\""));
222 assert!(json.contains("\"ArchivedSize\":1024"));
223 }
224
225 #[test]
226 fn test_album_image_deserialization() {
227 let json = r#"{
228 "ImageKey": "IMG123",
229 "FileName": "test.jpg",
230 "ArchivedUri": "https://example.com/test.jpg",
231 "ArchivedSize": 1024,
232 "Format": "JPG",
233 "Uri": "/api/v2/image/IMG123"
234 }"#;
235
236 let image: AlbumImage = serde_json::from_str(json).unwrap();
237 assert_eq!(image.image_key, "IMG123");
238 assert_eq!(image.file_name, "test.jpg");
239 assert_eq!(image.archived_uri, "https://example.com/test.jpg");
240 assert_eq!(image.file_size, 1024);
241 assert_eq!(image.format, "JPG");
242 assert_eq!(image.uri, "/api/v2/image/IMG123");
243 assert!(image.title.is_none());
244 assert!(image.archived_md5.is_none());
245 }
246
247 #[test]
248 fn test_album_image_with_optional_fields() {
249 let json = r#"{
250 "ImageKey": "IMG123",
251 "FileName": "test.jpg",
252 "ArchivedUri": "https://example.com/test.jpg",
253 "ArchivedSize": 1024,
254 "Format": "JPG",
255 "Uri": "/api/v2/image/IMG123",
256 "Title": "My Photo",
257 "ArchivedMD5": "abc123"
258 }"#;
259
260 let image: AlbumImage = serde_json::from_str(json).unwrap();
261 assert_eq!(image.title, Some("My Photo".to_string()));
262 assert_eq!(image.archived_md5, Some("abc123".to_string()));
263 }
264
265 #[tokio::test]
266 async fn test_list_album_images_mock() {
267 let _client = create_test_client();
268
269 let mut server = mockito::Server::new_async().await;
270 let _mock = server
271 .mock("GET", "/api/v2/album/ABC123!images")
272 .match_header(
273 "authorization",
274 mockito::Matcher::Regex("OAuth.*".to_string()),
275 )
276 .match_header("accept", "application/json")
277 .with_status(200)
278 .with_header("content-type", "application/json")
279 .with_body(
280 r#"{
281 "Response": {
282 "AlbumImage": [
283 {
284 "ImageKey": "IMG123",
285 "FileName": "photo1.jpg",
286 "ArchivedUri": "https://example.com/photo1.jpg",
287 "ArchivedSize": 2048,
288 "Format": "JPG",
289 "Uri": "/api/v2/image/IMG123",
290 "Title": "First Photo",
291 "ArchivedMD5": "abc123"
292 },
293 {
294 "ImageKey": "IMG456",
295 "FileName": "photo2.png",
296 "ArchivedUri": "https://example.com/photo2.png",
297 "ArchivedSize": 4096,
298 "Format": "PNG",
299 "Uri": "/api/v2/image/IMG456"
300 }
301 ]
302 }
303 }"#,
304 )
305 .create_async()
306 .await;
307
308 }
310
311 #[tokio::test]
312 async fn test_list_album_images_empty() {
313 let _client = create_test_client();
314
315 let mut server = mockito::Server::new_async().await;
316 let _mock = server
317 .mock("GET", "/api/v2/album/EMPTY!images")
318 .match_header(
319 "authorization",
320 mockito::Matcher::Regex("OAuth.*".to_string()),
321 )
322 .with_status(200)
323 .with_header("content-type", "application/json")
324 .with_body(
325 r#"{
326 "Response": {
327 "AlbumImage": []
328 }
329 }"#,
330 )
331 .create_async()
332 .await;
333 }
334
335 #[tokio::test]
336 async fn test_list_album_images_error() {
337 let _client = create_test_client();
338
339 let mut server = mockito::Server::new_async().await;
340 let _mock = server
341 .mock("GET", "/api/v2/album/INVALID!images")
342 .match_header(
343 "authorization",
344 mockito::Matcher::Regex("OAuth.*".to_string()),
345 )
346 .with_status(404)
347 .with_body("Album not found")
348 .create_async()
349 .await;
350 }
351
352 #[test]
353 fn test_album_image_clone() {
354 let image = AlbumImage {
355 image_key: "IMG123".to_string(),
356 file_name: "test.jpg".to_string(),
357 archived_uri: "https://example.com/test.jpg".to_string(),
358 file_size: 1024,
359 format: "JPG".to_string(),
360 uri: "/api/v2/image/IMG123".to_string(),
361 title: Some("Test".to_string()),
362 archived_md5: Some("abc".to_string()),
363 };
364
365 let cloned = image.clone();
366 assert_eq!(image.image_key, cloned.image_key);
367 assert_eq!(image.file_name, cloned.file_name);
368 assert_eq!(image.file_size, cloned.file_size);
369 }
370
371 #[test]
372 fn test_images_response_deserialization() {
373 let json = r#"{
374 "Response": {
375 "AlbumImage": [
376 {
377 "ImageKey": "IMG123",
378 "FileName": "test.jpg",
379 "ArchivedUri": "https://example.com/test.jpg",
380 "ArchivedSize": 1024,
381 "Format": "JPG",
382 "Uri": "/api/v2/image/IMG123"
383 }
384 ]
385 }
386 }"#;
387
388 let mut response: serde_json::Value = serde_json::from_str(json).unwrap();
390 let images: Vec<AlbumImage> =
391 serde_json::from_value(response["Response"]["AlbumImage"].take()).unwrap();
392 assert_eq!(images.len(), 1);
393 assert_eq!(images[0].image_key, "IMG123");
394 }
395
396 #[tokio::test]
397 async fn test_delete_image_success() {
398 let _client = create_test_client();
399
400 let mut server = mockito::Server::new_async().await;
401 let _mock = server
402 .mock("DELETE", "/api/v2/image/IMG123")
403 .match_header(
404 "authorization",
405 mockito::Matcher::Regex("OAuth.*".to_string()),
406 )
407 .match_header("accept", "application/json")
408 .with_status(200)
409 .with_header("content-type", "application/json")
410 .with_body(r#"{"Response":{}}"#)
411 .create_async()
412 .await;
413
414 }
416
417 #[tokio::test]
418 async fn test_delete_image_not_found() {
419 let _client = create_test_client();
420
421 let mut server = mockito::Server::new_async().await;
422 let _mock = server
423 .mock("DELETE", "/api/v2/image/INVALID")
424 .match_header(
425 "authorization",
426 mockito::Matcher::Regex("OAuth.*".to_string()),
427 )
428 .with_status(404)
429 .with_body("Image not found")
430 .create_async()
431 .await;
432
433 }
436
437 #[tokio::test]
438 async fn test_delete_image_unauthorized() {
439 let _client = create_test_client();
440
441 let mut server = mockito::Server::new_async().await;
442 let _mock = server
443 .mock("DELETE", "/api/v2/image/IMG123")
444 .match_header(
445 "authorization",
446 mockito::Matcher::Regex("OAuth.*".to_string()),
447 )
448 .with_status(401)
449 .with_body("Unauthorized")
450 .create_async()
451 .await;
452
453 }
456
457 #[tokio::test]
458 async fn test_delete_image_forbidden() {
459 let _client = create_test_client();
460
461 let mut server = mockito::Server::new_async().await;
462 let _mock = server
463 .mock("DELETE", "/api/v2/image/IMG123")
464 .match_header(
465 "authorization",
466 mockito::Matcher::Regex("OAuth.*".to_string()),
467 )
468 .with_status(403)
469 .with_body("Forbidden - insufficient permissions")
470 .create_async()
471 .await;
472
473 }
476
477 #[test]
478 fn test_image_details_serialization() {
479 let details = ImageDetails {
480 image_key: "IMG789".to_string(),
481 file_name: "vacation.jpg".to_string(),
482 format: "JPG".to_string(),
483 file_size: 2048000,
484 title: Some("Beach Sunset".to_string()),
485 caption: Some("Beautiful sunset at the beach".to_string()),
486 keywords: Some("sunset,beach,vacation".to_string()),
487 latitude: Some(37.7749),
488 longitude: Some(-122.4194),
489 altitude: Some(10.5),
490 archived_uri: "https://example.com/vacation.jpg".to_string(),
491 archived_md5: Some("def456abc789".to_string()),
492 upload_key: Some("UPLOAD123".to_string()),
493 uri: "/api/v2/image/IMG789".to_string(),
494 web_uri: Some("https://smugmug.com/image/IMG789".to_string()),
495 };
496
497 let json = serde_json::to_string(&details).unwrap();
498 assert!(json.contains("\"ImageKey\":\"IMG789\""));
499 assert!(json.contains("\"FileName\":\"vacation.jpg\""));
500 assert!(json.contains("\"ArchivedSize\":2048000"));
501 assert!(json.contains("\"Title\":\"Beach Sunset\""));
502 }
503
504 #[test]
505 fn test_image_details_deserialization_all_fields() {
506 let json = r#"{
507 "ImageKey": "IMG789",
508 "FileName": "vacation.jpg",
509 "Format": "JPG",
510 "ArchivedSize": 2048000,
511 "Title": "Beach Sunset",
512 "Caption": "Beautiful sunset at the beach",
513 "Keywords": "sunset,beach,vacation",
514 "Latitude": 37.7749,
515 "Longitude": -122.4194,
516 "Altitude": 10.5,
517 "ArchivedUri": "https://example.com/vacation.jpg",
518 "ArchivedMD5": "def456abc789",
519 "UploadKey": "UPLOAD123",
520 "Uri": "/api/v2/image/IMG789",
521 "WebUri": "https://smugmug.com/image/IMG789"
522 }"#;
523
524 let details: ImageDetails = serde_json::from_str(json).unwrap();
525 assert_eq!(details.image_key, "IMG789");
526 assert_eq!(details.file_name, "vacation.jpg");
527 assert_eq!(details.format, "JPG");
528 assert_eq!(details.file_size, 2048000);
529 assert_eq!(details.title, Some("Beach Sunset".to_string()));
530 assert_eq!(
531 details.caption,
532 Some("Beautiful sunset at the beach".to_string())
533 );
534 assert_eq!(details.keywords, Some("sunset,beach,vacation".to_string()));
535 assert_eq!(details.latitude, Some(37.7749));
536 assert_eq!(details.longitude, Some(-122.4194));
537 assert_eq!(details.altitude, Some(10.5));
538 assert_eq!(details.archived_uri, "https://example.com/vacation.jpg");
539 assert_eq!(details.archived_md5, Some("def456abc789".to_string()));
540 assert_eq!(details.upload_key, Some("UPLOAD123".to_string()));
541 assert_eq!(details.uri, "/api/v2/image/IMG789");
542 assert_eq!(
543 details.web_uri,
544 Some("https://smugmug.com/image/IMG789".to_string())
545 );
546 }
547
548 #[test]
549 fn test_image_details_deserialization_minimal_fields() {
550 let json = r#"{
551 "ImageKey": "IMG999",
552 "FileName": "simple.png",
553 "Format": "PNG",
554 "ArchivedSize": 1024,
555 "ArchivedUri": "https://example.com/simple.png",
556 "Uri": "/api/v2/image/IMG999"
557 }"#;
558
559 let details: ImageDetails = serde_json::from_str(json).unwrap();
560 assert_eq!(details.image_key, "IMG999");
561 assert_eq!(details.file_name, "simple.png");
562 assert_eq!(details.format, "PNG");
563 assert_eq!(details.file_size, 1024);
564 assert!(details.title.is_none());
565 assert!(details.caption.is_none());
566 assert!(details.keywords.is_none());
567 assert!(details.latitude.is_none());
568 assert!(details.longitude.is_none());
569 assert!(details.altitude.is_none());
570 assert!(details.archived_md5.is_none());
571 assert!(details.upload_key.is_none());
572 assert!(details.web_uri.is_none());
573 }
574
575 #[test]
576 fn test_image_details_response_deserialization() {
577 let json = r#"{
578 "Response": {
579 "Image": {
580 "ImageKey": "IMG777",
581 "FileName": "test.jpg",
582 "Format": "JPG",
583 "ArchivedSize": 5120,
584 "ArchivedUri": "https://example.com/test.jpg",
585 "Uri": "/api/v2/image/IMG777",
586 "Title": "Test Image"
587 }
588 }
589 }"#;
590
591 let response: ImageDetailsResponse = serde_json::from_str(json).unwrap();
592 assert_eq!(response.response.image.image_key, "IMG777");
593 assert_eq!(response.response.image.file_name, "test.jpg");
594 assert_eq!(
595 response.response.image.title,
596 Some("Test Image".to_string())
597 );
598 }
599
600 #[tokio::test]
601 async fn test_get_image_details_success() {
602 let _client = create_test_client();
603
604 let mut server = mockito::Server::new_async().await;
605 let _mock = server
606 .mock("GET", "/api/v2/image/IMG888")
607 .match_header(
608 "authorization",
609 mockito::Matcher::Regex("OAuth.*".to_string()),
610 )
611 .match_header("accept", "application/json")
612 .with_status(200)
613 .with_header("content-type", "application/json")
614 .with_body(
615 r#"{
616 "Response": {
617 "Image": {
618 "ImageKey": "IMG888",
619 "FileName": "mountain.jpg",
620 "Format": "JPG",
621 "ArchivedSize": 3145728,
622 "Title": "Mountain Peak",
623 "Caption": "View from the summit",
624 "Keywords": "mountain,hiking,nature",
625 "Latitude": 45.123,
626 "Longitude": -121.456,
627 "Altitude": 3000.0,
628 "ArchivedUri": "https://example.com/mountain.jpg",
629 "ArchivedMD5": "abc123def456",
630 "UploadKey": "UPLOAD456",
631 "Uri": "/api/v2/image/IMG888",
632 "WebUri": "https://smugmug.com/image/IMG888"
633 }
634 }
635 }"#,
636 )
637 .create_async()
638 .await;
639
640 }
642
643 #[tokio::test]
644 async fn test_get_image_details_not_found() {
645 let _client = create_test_client();
646
647 let mut server = mockito::Server::new_async().await;
648 let _mock = server
649 .mock("GET", "/api/v2/image/NOTFOUND")
650 .match_header(
651 "authorization",
652 mockito::Matcher::Regex("OAuth.*".to_string()),
653 )
654 .with_status(404)
655 .with_body("Image not found")
656 .create_async()
657 .await;
658
659 }
662
663 #[tokio::test]
664 async fn test_get_image_details_unauthorized() {
665 let _client = create_test_client();
666
667 let mut server = mockito::Server::new_async().await;
668 let _mock = server
669 .mock("GET", "/api/v2/image/IMG999")
670 .match_header(
671 "authorization",
672 mockito::Matcher::Regex("OAuth.*".to_string()),
673 )
674 .with_status(401)
675 .with_body("Unauthorized")
676 .create_async()
677 .await;
678
679 }
682
683 #[tokio::test]
684 async fn test_get_image_details_forbidden() {
685 let _client = create_test_client();
686
687 let mut server = mockito::Server::new_async().await;
688 let _mock = server
689 .mock("GET", "/api/v2/image/IMG999")
690 .match_header(
691 "authorization",
692 mockito::Matcher::Regex("OAuth.*".to_string()),
693 )
694 .with_status(403)
695 .with_body("Forbidden - insufficient permissions")
696 .create_async()
697 .await;
698
699 }
702
703 #[test]
704 fn test_image_details_clone() {
705 let details = ImageDetails {
706 image_key: "IMG555".to_string(),
707 file_name: "clone_test.jpg".to_string(),
708 format: "JPG".to_string(),
709 file_size: 1024,
710 title: Some("Test".to_string()),
711 caption: None,
712 keywords: Some("test".to_string()),
713 latitude: Some(0.0),
714 longitude: Some(0.0),
715 altitude: None,
716 archived_uri: "https://example.com/test.jpg".to_string(),
717 archived_md5: Some("abc".to_string()),
718 upload_key: None,
719 uri: "/api/v2/image/IMG555".to_string(),
720 web_uri: None,
721 };
722
723 let cloned = details.clone();
724 assert_eq!(details.image_key, cloned.image_key);
725 assert_eq!(details.file_name, cloned.file_name);
726 assert_eq!(details.file_size, cloned.file_size);
727 assert_eq!(details.title, cloned.title);
728 assert_eq!(details.latitude, cloned.latitude);
729 }
730
731 #[test]
732 fn test_image_metadata_update_default() {
733 let update = ImageMetadataUpdate::default();
734 assert!(update.caption.is_none());
735 assert!(update.title.is_none());
736 assert!(update.keywords.is_none());
737 assert!(update.latitude.is_none());
738 assert!(update.longitude.is_none());
739 }
740
741 #[test]
742 fn test_image_metadata_update_with_all_fields() {
743 let update = ImageMetadataUpdate {
744 caption: Some("Test Caption".to_string()),
745 title: Some("Test Title".to_string()),
746 keywords: Some("sunset;beach;vacation".to_string()),
747 latitude: Some(37.7749),
748 longitude: Some(-122.4194),
749 };
750
751 assert_eq!(update.caption, Some("Test Caption".to_string()));
752 assert_eq!(update.title, Some("Test Title".to_string()));
753 assert_eq!(update.keywords, Some("sunset;beach;vacation".to_string()));
754 assert_eq!(update.latitude, Some(37.7749));
755 assert_eq!(update.longitude, Some(-122.4194));
756 }
757
758 #[test]
759 fn test_image_metadata_update_partial() {
760 let update = ImageMetadataUpdate {
761 caption: Some("Only caption".to_string()),
762 ..Default::default()
763 };
764
765 assert_eq!(update.caption, Some("Only caption".to_string()));
766 assert!(update.title.is_none());
767 assert!(update.keywords.is_none());
768 }
769
770 #[tokio::test]
771 async fn test_update_image_metadata_success() {
772 let _client = create_test_client();
773
774 let mut server = mockito::Server::new_async().await;
775 let _mock = server
776 .mock("PATCH", "/api/v2/image/IMG123")
777 .match_header(
778 "authorization",
779 mockito::Matcher::Regex("OAuth.*".to_string()),
780 )
781 .match_header("accept", "application/json")
782 .match_header("content-type", "application/json")
783 .with_status(200)
784 .with_header("content-type", "application/json")
785 .with_body(r#"{"Response":{"Image":{"ImageKey":"IMG123"}}}"#)
786 .create_async()
787 .await;
788
789 }
791
792 #[tokio::test]
793 async fn test_update_image_metadata_not_found() {
794 let _client = create_test_client();
795
796 let mut server = mockito::Server::new_async().await;
797 let _mock = server
798 .mock("PATCH", "/api/v2/image/INVALID")
799 .match_header(
800 "authorization",
801 mockito::Matcher::Regex("OAuth.*".to_string()),
802 )
803 .with_status(404)
804 .with_body("Image not found")
805 .create_async()
806 .await;
807
808 }
811
812 #[tokio::test]
813 async fn test_update_image_metadata_unauthorized() {
814 let _client = create_test_client();
815
816 let mut server = mockito::Server::new_async().await;
817 let _mock = server
818 .mock("PATCH", "/api/v2/image/IMG123")
819 .match_header(
820 "authorization",
821 mockito::Matcher::Regex("OAuth.*".to_string()),
822 )
823 .with_status(401)
824 .with_body("Unauthorized")
825 .create_async()
826 .await;
827
828 }
831
832 #[tokio::test]
833 async fn test_update_image_metadata_forbidden() {
834 let _client = create_test_client();
835
836 let mut server = mockito::Server::new_async().await;
837 let _mock = server
838 .mock("PATCH", "/api/v2/image/IMG123")
839 .match_header(
840 "authorization",
841 mockito::Matcher::Regex("OAuth.*".to_string()),
842 )
843 .with_status(403)
844 .with_body("Forbidden - insufficient permissions")
845 .create_async()
846 .await;
847
848 }
851
852 #[test]
853 fn test_image_metadata_update_with_gps() {
854 let update = ImageMetadataUpdate {
855 latitude: Some(37.7749),
856 longitude: Some(-122.4194),
857 ..Default::default()
858 };
859
860 assert_eq!(update.latitude, Some(37.7749));
861 assert_eq!(update.longitude, Some(-122.4194));
862 assert!(update.caption.is_none());
863 assert!(update.title.is_none());
864 }
865
866 #[test]
867 fn test_image_metadata_update_with_keywords_semicolon() {
868 let update = ImageMetadataUpdate {
869 keywords: Some("sunset;beach;vacation;2024".to_string()),
870 ..Default::default()
871 };
872
873 assert_eq!(
874 update.keywords,
875 Some("sunset;beach;vacation;2024".to_string())
876 );
877 }
878
879 #[test]
880 fn test_image_metadata_update_with_title_and_caption() {
881 let update = ImageMetadataUpdate {
882 title: Some("My Amazing Photo".to_string()),
883 caption: Some("This was taken at sunset on the beach.".to_string()),
884 ..Default::default()
885 };
886
887 assert_eq!(update.title, Some("My Amazing Photo".to_string()));
888 assert_eq!(
889 update.caption,
890 Some("This was taken at sunset on the beach.".to_string())
891 );
892 assert!(update.keywords.is_none());
893 }
894
895 #[tokio::test]
896 async fn test_move_image_success() {
897 let _client = create_test_client();
898
899 let mut server = mockito::Server::new_async().await;
900 let _mock = server
901 .mock("PATCH", "/api/v2/image/IMG123")
902 .match_header(
903 "authorization",
904 mockito::Matcher::Regex("OAuth.*".to_string()),
905 )
906 .match_header("accept", "application/json")
907 .match_header("content-type", "application/json")
908 .match_body(mockito::Matcher::JsonString(
909 r#"{"AlbumUri":"/api/v2/album/ALB456"}"#.to_string(),
910 ))
911 .with_status(200)
912 .with_header("content-type", "application/json")
913 .with_body(
914 r#"{"Response":{"Image":{"ImageKey":"IMG123","AlbumUri":"/api/v2/album/ALB456"}}}"#,
915 )
916 .create_async()
917 .await;
918
919 }
922
923 #[tokio::test]
924 async fn test_move_image_not_found() {
925 let _client = create_test_client();
926
927 let mut server = mockito::Server::new_async().await;
928 let _mock = server
929 .mock("PATCH", "/api/v2/image/INVALID")
930 .match_header(
931 "authorization",
932 mockito::Matcher::Regex("OAuth.*".to_string()),
933 )
934 .with_status(404)
935 .with_body("Image not found")
936 .create_async()
937 .await;
938
939 }
942
943 #[tokio::test]
944 async fn test_move_image_unauthorized() {
945 let _client = create_test_client();
946
947 let mut server = mockito::Server::new_async().await;
948 let _mock = server
949 .mock("PATCH", "/api/v2/image/IMG123")
950 .match_header(
951 "authorization",
952 mockito::Matcher::Regex("OAuth.*".to_string()),
953 )
954 .with_status(401)
955 .with_body("Unauthorized")
956 .create_async()
957 .await;
958
959 }
962
963 #[tokio::test]
964 async fn test_move_image_forbidden() {
965 let _client = create_test_client();
966
967 let mut server = mockito::Server::new_async().await;
968 let _mock = server
969 .mock("PATCH", "/api/v2/image/IMG123")
970 .match_header(
971 "authorization",
972 mockito::Matcher::Regex("OAuth.*".to_string()),
973 )
974 .with_status(403)
975 .with_body("Forbidden - insufficient permissions")
976 .create_async()
977 .await;
978
979 }
982}