1#![allow(clippy::uninlined_format_args)]
3#![allow(dead_code)]
11
12use anyhow::{Context, Result};
13use serde::{Deserialize, Serialize};
14use std::path::Path;
15use tokio::fs::File;
16use tokio::io::AsyncReadExt;
17
18use raps_kernel::auth::AuthClient;
19use raps_kernel::config::Config;
20use raps_kernel::http::{self, HttpClientConfig};
21
22#[derive(Debug, Clone, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct Photoscene {
26 #[serde(rename = "photosceneid")]
27 pub photoscene_id: String,
28 pub name: Option<String>,
29 #[serde(rename = "scenetype")]
30 pub scene_type: Option<String>,
31 #[serde(rename = "convertformat")]
32 pub convert_format: Option<String>,
33 pub status: Option<String>,
34 pub progress: Option<String>,
35 #[serde(rename = "progressmsg")]
36 pub progress_msg: Option<String>,
37}
38
39#[derive(Debug, Deserialize)]
41#[serde(rename_all = "PascalCase")]
42pub struct ListPhotoscenesResponse {
43 #[serde(default, alias = "photoscenes")]
44 pub photoscenes: PhotoscenesList,
45}
46
47#[derive(Debug, Default, Deserialize)]
48pub struct PhotoscenesList {
49 #[serde(default)]
50 pub photoscene: Vec<Photoscene>,
51}
52
53#[derive(Debug, Deserialize)]
55#[serde(rename_all = "PascalCase")]
56pub struct CreatePhotosceneResponse {
57 #[serde(alias = "photoscene")]
58 pub photoscene: Photoscene,
59}
60
61#[derive(Debug, Deserialize)]
63#[serde(rename_all = "PascalCase")]
64pub struct UploadResponse {
65 pub files: Option<UploadFiles>,
66 pub usage: Option<String>,
67 pub resource: Option<String>,
68}
69
70#[derive(Debug, Deserialize)]
71pub struct UploadFiles {
72 pub file: Option<Vec<UploadedFile>>,
73}
74
75#[derive(Debug, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct UploadedFile {
78 pub filename: String,
79 pub fileid: String,
80 pub filesize: Option<String>,
82 pub msg: Option<String>,
83}
84
85impl UploadedFile {
86 pub fn filesize_bytes(&self) -> Option<u64> {
88 self.filesize.as_deref().and_then(|s| s.parse().ok())
89 }
90}
91
92#[derive(Debug, Deserialize)]
94pub struct RcApiError {
95 pub code: Option<String>,
96 #[serde(alias = "message")]
97 pub msg: Option<String>,
98}
99
100#[derive(Debug, Deserialize)]
102#[serde(rename_all = "PascalCase")]
103pub struct ProgressResponse {
104 #[serde(alias = "photoscene")]
105 pub photoscene: Option<PhotosceneProgress>,
106 #[serde(alias = "error")]
107 pub error: Option<RcApiError>,
108}
109
110#[derive(Debug, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct PhotosceneProgress {
113 #[serde(rename = "photosceneid")]
114 pub photoscene_id: String,
115 pub progress: String,
116 #[serde(rename = "progressmsg")]
117 pub progress_msg: Option<String>,
118 pub status: Option<String>,
119}
120
121#[derive(Debug, Deserialize)]
123#[serde(rename_all = "PascalCase")]
124pub struct ResultResponse {
125 #[serde(alias = "photoscene")]
126 pub photoscene: Option<PhotosceneResult>,
127 #[serde(alias = "error")]
128 pub error: Option<RcApiError>,
129}
130
131#[derive(Debug, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct PhotosceneResult {
134 #[serde(rename = "photosceneid")]
135 pub photoscene_id: String,
136 pub progress: String,
137 #[serde(rename = "progressmsg")]
138 pub progress_msg: Option<String>,
139 #[serde(rename = "scenelink")]
140 pub scene_link: Option<String>,
141 #[serde(rename = "filesize")]
143 pub file_size: Option<String>,
144}
145
146impl PhotosceneResult {
147 pub fn filesize_bytes(&self) -> Option<u64> {
149 self.file_size.as_deref().and_then(|s| s.parse().ok())
150 }
151}
152
153#[derive(Debug, Clone, Copy)]
155pub enum OutputFormat {
156 Rcm, Rcs, Obj, Fbx, Ortho, }
162
163impl std::fmt::Display for OutputFormat {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 match self {
166 OutputFormat::Rcm => write!(f, "rcm"),
167 OutputFormat::Rcs => write!(f, "rcs"),
168 OutputFormat::Obj => write!(f, "obj"),
169 OutputFormat::Fbx => write!(f, "fbx"),
170 OutputFormat::Ortho => write!(f, "ortho"),
171 }
172 }
173}
174
175impl OutputFormat {
176 pub fn all() -> Vec<Self> {
177 vec![Self::Rcm, Self::Rcs, Self::Obj, Self::Fbx, Self::Ortho]
178 }
179
180 pub fn description(&self) -> &str {
181 match self {
182 OutputFormat::Rcm => "Autodesk ReCap format (point cloud)",
183 OutputFormat::Rcs => "ReCap scan format",
184 OutputFormat::Obj => "Wavefront OBJ (mesh)",
185 OutputFormat::Fbx => "Autodesk FBX (mesh)",
186 OutputFormat::Ortho => "Orthophoto (2D image)",
187 }
188 }
189}
190
191#[derive(Debug, Clone, Copy, Serialize)]
193#[serde(rename_all = "lowercase")]
194pub enum SceneType {
195 Aerial,
196 Object,
197}
198
199impl std::fmt::Display for SceneType {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 match self {
202 SceneType::Aerial => write!(f, "aerial"),
203 SceneType::Object => write!(f, "object"),
204 }
205 }
206}
207
208#[derive(Clone)]
210pub struct RealityCaptureClient {
211 config: Config,
212 auth: AuthClient,
213 http_client: reqwest::Client,
214}
215
216impl RealityCaptureClient {
217 pub fn new(config: Config, auth: AuthClient) -> Self {
219 Self::new_with_http_config(config, auth, HttpClientConfig::default())
220 }
221
222 pub fn new_with_http_config(
224 config: Config,
225 auth: AuthClient,
226 http_config: HttpClientConfig,
227 ) -> Self {
228 let http_client = http_config
230 .create_client()
231 .unwrap_or_else(|_| reqwest::Client::new()); Self {
234 config,
235 auth,
236 http_client,
237 }
238 }
239
240 pub async fn create_photoscene(
242 &self,
243 name: &str,
244 scene_type: SceneType,
245 format: OutputFormat,
246 ) -> Result<Photoscene> {
247 let token = self.auth.get_token().await?;
248 let url = format!("{}/photoscene", self.config.reality_capture_url());
249
250 let params = [
251 ("scenename", name),
252 ("scenetype", &scene_type.to_string()),
253 ("format", &format.to_string()),
254 ];
255
256 let response = http::send_with_retry(&self.config.http_config, || {
257 self.http_client
258 .post(&url)
259 .bearer_auth(&token)
260 .form(¶ms)
261 })
262 .await?;
263
264 if !response.status().is_success() {
265 let status = response.status();
266 let error_text = response.text().await.unwrap_or_default();
267 anyhow::bail!("Failed to create photoscene ({status}): {error_text}");
268 }
269
270 let create_response: CreatePhotosceneResponse = response
271 .json()
272 .await
273 .context("Failed to parse photoscene response")?;
274
275 Ok(create_response.photoscene)
276 }
277
278 pub async fn upload_photos(
280 &self,
281 photoscene_id: &str,
282 photo_paths: &[&Path],
283 ) -> Result<Vec<UploadedFile>> {
284 let token = self.auth.get_token().await?;
285 let url = format!("{}/file", self.config.reality_capture_url());
286
287 let mut file_parts: Vec<(String, Vec<u8>)> = Vec::new();
289 for path in photo_paths {
290 let mut file = File::open(path)
291 .await
292 .context(format!("Failed to open file: {}", path.display()))?;
293
294 let mut buffer = Vec::new();
295 file.read_to_end(&mut buffer)
296 .await
297 .context("Failed to read file")?;
298
299 let filename = path
300 .file_name()
301 .and_then(|n| n.to_str())
302 .unwrap_or("photo.jpg")
303 .to_string();
304
305 file_parts.push((filename, buffer));
306 }
307
308 let photoscene_id_owned = photoscene_id.to_string();
309 let response = http::send_with_retry(&self.config.http_config, || {
310 let mut form = reqwest::multipart::Form::new()
311 .text("photosceneid", photoscene_id_owned.clone())
312 .text("type", "image");
313
314 for (i, (filename, buffer)) in file_parts.iter().enumerate() {
315 let part = reqwest::multipart::Part::bytes(buffer.clone())
316 .file_name(filename.clone())
317 .mime_str(mime_type_from_extension(filename))
318 .expect("valid MIME type");
319
320 form = form.part(format!("file[{}]", i), part);
321 }
322
323 self.http_client
324 .post(&url)
325 .bearer_auth(&token)
326 .multipart(form)
327 })
328 .await?;
329
330 if !response.status().is_success() {
331 let status = response.status();
332 let error_text = response.text().await.unwrap_or_default();
333 anyhow::bail!("Failed to upload photos ({status}): {error_text}");
334 }
335
336 let upload_response: UploadResponse = response
337 .json()
338 .await
339 .context("Failed to parse upload response")?;
340
341 let files = upload_response
342 .files
343 .and_then(|f| f.file)
344 .unwrap_or_default();
345
346 Ok(files)
347 }
348
349 pub async fn start_processing(&self, photoscene_id: &str) -> Result<()> {
351 let token = self.auth.get_token().await?;
352 let url = format!(
353 "{}/photoscene/{}",
354 self.config.reality_capture_url(),
355 photoscene_id
356 );
357
358 let response = http::send_with_retry(&self.config.http_config, || {
359 self.http_client.post(&url).bearer_auth(&token)
360 })
361 .await?;
362
363 if !response.status().is_success() {
364 let status = response.status();
365 let error_text = response.text().await.unwrap_or_default();
366 anyhow::bail!("Failed to start processing ({status}): {error_text}");
367 }
368
369 Ok(())
370 }
371
372 pub async fn get_progress(&self, photoscene_id: &str) -> Result<PhotosceneProgress> {
374 let token = self.auth.get_token().await?;
375 let url = format!(
376 "{}/photoscene/{}/progress",
377 self.config.reality_capture_url(),
378 photoscene_id
379 );
380
381 let response = http::send_with_retry(&self.config.http_config, || {
382 self.http_client.get(&url).bearer_auth(&token)
383 })
384 .await?;
385
386 if !response.status().is_success() {
387 let status = response.status();
388 let error_text = response.text().await.unwrap_or_default();
389 anyhow::bail!("Failed to get progress ({status}): {error_text}");
390 }
391
392 let progress_response: ProgressResponse = response
393 .json()
394 .await
395 .context("Failed to parse progress response")?;
396
397 if let Some(err) = progress_response.error {
398 let code = err.code.unwrap_or_default();
399 let msg = err.msg.unwrap_or_default();
400 anyhow::bail!("Reality Capture API error ({code}): {msg}");
401 }
402
403 progress_response
404 .photoscene
405 .ok_or_else(|| anyhow::anyhow!("Progress response missing Photoscene data"))
406 }
407
408 pub async fn get_result(
410 &self,
411 photoscene_id: &str,
412 format: OutputFormat,
413 ) -> Result<PhotosceneResult> {
414 let token = self.auth.get_token().await?;
415 let url = format!(
416 "{}/photoscene/{}?format={}",
417 self.config.reality_capture_url(),
418 photoscene_id,
419 format
420 );
421
422 let response = http::send_with_retry(&self.config.http_config, || {
423 self.http_client.get(&url).bearer_auth(&token)
424 })
425 .await?;
426
427 if !response.status().is_success() {
428 let status = response.status();
429 let error_text = response.text().await.unwrap_or_default();
430 anyhow::bail!("Failed to get result ({status}): {error_text}");
431 }
432
433 let result_response: ResultResponse = response
434 .json()
435 .await
436 .context("Failed to parse result response")?;
437
438 if let Some(err) = result_response.error {
439 let code = err.code.unwrap_or_default();
440 let msg = err.msg.unwrap_or_default();
441 anyhow::bail!("Reality Capture API error ({code}): {msg}");
442 }
443
444 result_response
445 .photoscene
446 .ok_or_else(|| anyhow::anyhow!("Result response missing Photoscene data"))
447 }
448
449 pub async fn delete_photoscene(&self, photoscene_id: &str) -> Result<()> {
451 let token = self.auth.get_token().await?;
452 let url = format!(
453 "{}/photoscene/{}",
454 self.config.reality_capture_url(),
455 photoscene_id
456 );
457
458 let response = http::send_with_retry(&self.config.http_config, || {
459 self.http_client.delete(&url).bearer_auth(&token)
460 })
461 .await?;
462
463 if !response.status().is_success() {
464 let status = response.status();
465 let error_text = response.text().await.unwrap_or_default();
466 anyhow::bail!("Failed to delete photoscene ({status}): {error_text}");
467 }
468
469 Ok(())
470 }
471
472 pub async fn list_photoscenes(&self) -> Result<Vec<Photoscene>> {
474 let token = self.auth.get_token().await?;
475 let url = format!("{}/photoscene", self.config.reality_capture_url());
476
477 let response = http::send_with_retry(&self.config.http_config, || {
478 self.http_client.get(&url).bearer_auth(&token)
479 })
480 .await?;
481
482 if !response.status().is_success() {
483 let status = response.status();
484 let error_text = response.text().await.unwrap_or_default();
485 anyhow::bail!("Failed to list photoscenes ({status}): {error_text}");
486 }
487
488 let list_response: ListPhotoscenesResponse = response
489 .json()
490 .await
491 .context("Failed to parse photoscenes response")?;
492
493 Ok(list_response.photoscenes.photoscene)
494 }
495
496 pub fn available_formats(&self) -> Vec<OutputFormat> {
498 OutputFormat::all()
499 }
500}
501
502fn mime_type_from_extension(filename: &str) -> &'static str {
504 let ext = filename
505 .rsplit('.')
506 .next()
507 .unwrap_or("")
508 .to_ascii_lowercase();
509 match ext.as_str() {
510 "jpg" | "jpeg" => "image/jpeg",
511 "png" => "image/png",
512 "tiff" | "tif" => "image/tiff",
513 "bmp" => "image/bmp",
514 "webp" => "image/webp",
515 "gif" => "image/gif",
516 _ => "application/octet-stream",
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523
524 #[test]
525 fn test_output_format_all() {
526 let formats = OutputFormat::all();
527 assert_eq!(formats.len(), 5);
528 }
529
530 #[test]
531 fn test_output_format_display() {
532 assert_eq!(OutputFormat::Rcm.to_string(), "rcm");
533 assert_eq!(OutputFormat::Rcs.to_string(), "rcs");
534 assert_eq!(OutputFormat::Obj.to_string(), "obj");
535 assert_eq!(OutputFormat::Fbx.to_string(), "fbx");
536 assert_eq!(OutputFormat::Ortho.to_string(), "ortho");
537 }
538
539 #[test]
540 fn test_output_format_description() {
541 assert!(!OutputFormat::Rcm.description().is_empty());
542 assert!(OutputFormat::Rcm.description().contains("ReCap"));
543 assert!(OutputFormat::Obj.description().contains("OBJ"));
544 }
545
546 #[test]
547 fn test_scene_type_display() {
548 assert_eq!(SceneType::Aerial.to_string(), "aerial");
549 assert_eq!(SceneType::Object.to_string(), "object");
550 }
551
552 #[test]
553 fn test_scene_type_serialization() {
554 assert_eq!(
555 serde_json::to_string(&SceneType::Aerial).unwrap(),
556 "\"aerial\""
557 );
558 assert_eq!(
559 serde_json::to_string(&SceneType::Object).unwrap(),
560 "\"object\""
561 );
562 }
563
564 #[test]
565 fn test_photoscene_deserialization() {
566 let json = r#"{
567 "photosceneid": "scene-123",
568 "name": "Test Scene",
569 "scenetype": "object",
570 "convertformat": "rcm",
571 "status": "Created",
572 "progress": "0"
573 }"#;
574
575 let scene: Photoscene = serde_json::from_str(json).unwrap();
576 assert_eq!(scene.photoscene_id, "scene-123");
577 assert_eq!(scene.name, Some("Test Scene".to_string()));
578 }
579
580 #[test]
581 fn test_photoscene_progress_deserialization() {
582 let json = r#"{
583 "photosceneid": "scene-123",
584 "progress": "50",
585 "progressmsg": "Processing images"
586 }"#;
587
588 let progress: PhotosceneProgress = serde_json::from_str(json).unwrap();
589 assert_eq!(progress.photoscene_id, "scene-123");
590 assert_eq!(progress.progress, "50");
591 }
592
593 #[test]
594 fn test_photoscene_result_deserialization() {
595 let json = r#"{
596 "photosceneid": "scene-123",
597 "progress": "100",
598 "progressmsg": "Complete",
599 "filesize": "5242880",
600 "scenelink": "https://example.com/download/scene.rcm"
601 }"#;
602
603 let result: PhotosceneResult = serde_json::from_str(json).unwrap();
604 assert_eq!(result.photoscene_id, "scene-123");
605 assert!(result.scene_link.is_some());
606 }
607
608 #[test]
609 fn test_create_photoscene_response_deserialization() {
610 let json = r#"{
611 "Photoscene": {
612 "photosceneid": "new-scene-456",
613 "name": "New Scene"
614 }
615 }"#;
616
617 let response: CreatePhotosceneResponse = serde_json::from_str(json).unwrap();
618 assert_eq!(response.photoscene.photoscene_id, "new-scene-456");
619 }
620
621 #[test]
622 fn test_list_photoscenes_response_deserialization() {
623 let json = r#"{
624 "Photoscenes": {
625 "photoscene": [
626 {
627 "photosceneid": "scene-1",
628 "name": "Scene One",
629 "scenetype": "aerial",
630 "status": "Complete",
631 "progress": "100"
632 },
633 {
634 "photosceneid": "scene-2",
635 "name": "Scene Two",
636 "scenetype": "object",
637 "status": "Created",
638 "progress": "0"
639 }
640 ]
641 }
642 }"#;
643
644 let response: ListPhotoscenesResponse = serde_json::from_str(json).unwrap();
645 assert_eq!(response.photoscenes.photoscene.len(), 2);
646 assert_eq!(response.photoscenes.photoscene[0].photoscene_id, "scene-1");
647 assert_eq!(response.photoscenes.photoscene[1].photoscene_id, "scene-2");
648 assert_eq!(
649 response.photoscenes.photoscene[0].name,
650 Some("Scene One".to_string())
651 );
652 }
653
654 #[test]
655 fn test_list_photoscenes_response_empty() {
656 let json = r#"{
657 "Photoscenes": {
658 "photoscene": []
659 }
660 }"#;
661
662 let response: ListPhotoscenesResponse = serde_json::from_str(json).unwrap();
663 assert!(response.photoscenes.photoscene.is_empty());
664 }
665
666 #[test]
667 fn test_list_photoscenes_response_missing_photoscenes() {
668 let json = r#"{}"#;
669
670 let response: ListPhotoscenesResponse = serde_json::from_str(json).unwrap();
671 assert!(response.photoscenes.photoscene.is_empty());
672 }
673
674 #[test]
675 fn test_upload_response_deserialization() {
676 let json = r#"{
677 "Files": {
678 "file": [
679 {
680 "filename": "photo1.jpg",
681 "fileid": "file-001",
682 "filesize": "2048000",
683 "msg": "File uploaded"
684 }
685 ]
686 },
687 "Usage": "1",
688 "Resource": "/file"
689 }"#;
690
691 let response: UploadResponse = serde_json::from_str(json).unwrap();
692 assert!(response.files.is_some());
693 let files = response.files.unwrap().file.unwrap();
694 assert_eq!(files.len(), 1);
695 assert_eq!(files[0].filename, "photo1.jpg");
696 assert_eq!(files[0].fileid, "file-001");
697 }
698
699 #[test]
700 fn test_progress_response_deserialization() {
701 let json = r#"{
702 "Photoscene": {
703 "photosceneid": "scene-789",
704 "progress": "75",
705 "progressmsg": "Processing images",
706 "status": "InProgress"
707 }
708 }"#;
709
710 let response: ProgressResponse = serde_json::from_str(json).unwrap();
711 let ps = response.photoscene.unwrap();
712 assert_eq!(ps.photoscene_id, "scene-789");
713 assert_eq!(ps.progress, "75");
714 assert_eq!(ps.progress_msg, Some("Processing images".to_string()));
715 }
716
717 #[test]
718 fn test_progress_response_with_error() {
719 let json = r#"{
720 "Usage": "0.51",
721 "Resource": "/photoscene/xyz/progress",
722 "Error": {
723 "code": "ERR-001",
724 "msg": "Scene not found"
725 }
726 }"#;
727
728 let response: ProgressResponse = serde_json::from_str(json).unwrap();
729 assert!(response.photoscene.is_none());
730 assert!(response.error.is_some());
731 let err = response.error.unwrap();
732 assert_eq!(err.code.unwrap(), "ERR-001");
733 assert_eq!(err.msg.unwrap(), "Scene not found");
734 }
735
736 #[test]
737 fn test_mime_type_from_extension() {
738 assert_eq!(mime_type_from_extension("photo.jpg"), "image/jpeg");
739 assert_eq!(mime_type_from_extension("photo.jpeg"), "image/jpeg");
740 assert_eq!(mime_type_from_extension("photo.png"), "image/png");
741 assert_eq!(mime_type_from_extension("photo.tiff"), "image/tiff");
742 assert_eq!(mime_type_from_extension("photo.tif"), "image/tiff");
743 assert_eq!(mime_type_from_extension("photo.bmp"), "image/bmp");
744 assert_eq!(mime_type_from_extension("photo.webp"), "image/webp");
745 assert_eq!(mime_type_from_extension("photo.gif"), "image/gif");
746 }
747
748 #[test]
749 fn test_mime_fallback() {
750 assert_eq!(
751 mime_type_from_extension("photo.raw"),
752 "application/octet-stream"
753 );
754 assert_eq!(
755 mime_type_from_extension("photo.xyz"),
756 "application/octet-stream"
757 );
758 assert_eq!(
759 mime_type_from_extension("photo.RAW"),
760 "application/octet-stream"
761 );
762 }
763
764 #[test]
765 fn test_mime_case_insensitive() {
766 assert_eq!(mime_type_from_extension("photo.PNG"), "image/png");
767 assert_eq!(mime_type_from_extension("photo.JPEG"), "image/jpeg");
768 assert_eq!(mime_type_from_extension("photo.Tiff"), "image/tiff");
769 }
770
771 #[test]
772 fn test_photoscene_result_filesize_bytes() {
773 let result = PhotosceneResult {
774 photoscene_id: "scene-1".to_string(),
775 progress: "100".to_string(),
776 progress_msg: None,
777 scene_link: None,
778 file_size: Some("5242880".to_string()),
779 };
780 assert_eq!(result.filesize_bytes(), Some(5_242_880));
781 }
782
783 #[test]
784 fn test_photoscene_result_filesize_bytes_none() {
785 let result = PhotosceneResult {
786 photoscene_id: "scene-1".to_string(),
787 progress: "100".to_string(),
788 progress_msg: None,
789 scene_link: None,
790 file_size: None,
791 };
792 assert_eq!(result.filesize_bytes(), None);
793 }
794
795 #[test]
796 fn test_photoscene_result_filesize_bytes_unparsable() {
797 let result = PhotosceneResult {
798 photoscene_id: "scene-1".to_string(),
799 progress: "100".to_string(),
800 progress_msg: None,
801 scene_link: None,
802 file_size: Some("not-a-number".to_string()),
803 };
804 assert_eq!(result.filesize_bytes(), None);
805 }
806
807 #[test]
808 fn test_uploaded_file_filesize_bytes() {
809 let file = UploadedFile {
810 filename: "photo.jpg".to_string(),
811 fileid: "file-1".to_string(),
812 filesize: Some("2048000".to_string()),
813 msg: None,
814 };
815 assert_eq!(file.filesize_bytes(), Some(2_048_000));
816 }
817}
818
819#[cfg(test)]
821mod integration_tests {
822 use super::*;
823 use raps_kernel::auth::AuthClient;
824 use raps_kernel::config::Config;
825
826 fn create_mock_reality_client(mock_url: &str) -> RealityCaptureClient {
827 let config = Config {
828 client_id: "test-client-id".to_string(),
829 client_secret: "test-client-secret".to_string(),
830 base_url: mock_url.to_string(),
831 callback_url: "http://localhost:8080/callback".to_string(),
832 da_nickname: None,
833 http_config: HttpClientConfig::default(),
834 };
835 let auth = AuthClient::new(config.clone());
836 RealityCaptureClient::new(config, auth)
837 }
838
839 #[tokio::test]
840 async fn test_client_creation() {
841 let server = raps_mock::TestServer::start_default().await.unwrap();
842 let client = create_mock_reality_client(&server.url);
843 assert!(client.auth.config().base_url.starts_with("http://"));
844 }
845
846 #[tokio::test]
847 async fn test_list_photoscenes() {
848 let server = raps_mock::TestServer::start_default().await.unwrap();
849 let client = create_mock_reality_client(&server.url);
850 let result = client.list_photoscenes().await;
851 let _ = result;
852 }
853}