1use super::traits::AurApi;
4use crate::error::{ArchToolkitError, Result};
5use crate::types::{AurComment, AurPackage, AurPackageDetails};
6use async_trait::async_trait;
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9
10#[derive(Debug)]
24pub struct MockAurApi {
25 search_results: Arc<Mutex<HashMap<String, Result<Vec<AurPackage>>>>>,
27 info_results: Arc<Mutex<HashMap<String, Result<Vec<AurPackageDetails>>>>>,
29 comments_results: Arc<Mutex<HashMap<String, Result<Vec<AurComment>>>>>,
31 pkgbuild_results: Arc<Mutex<HashMap<String, Result<String>>>>,
33 default_search_result: Option<Result<Vec<AurPackage>>>,
35 default_info_result: Option<Result<Vec<AurPackageDetails>>>,
37 default_comments_result: Option<Result<Vec<AurComment>>>,
39 default_pkgbuild_result: Option<Result<String>>,
41}
42
43impl Default for MockAurApi {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl MockAurApi {
50 fn clone_result<T: Clone>(result: &Result<T>) -> Result<T> {
62 match result {
63 Ok(ok) => Ok(ok.clone()),
64 Err(e) => Err(match e {
65 ArchToolkitError::Network(_) => {
66 ArchToolkitError::Parse("Mock network error".to_string())
67 }
68 ArchToolkitError::SearchFailed { query, .. } => {
69 ArchToolkitError::Parse(format!("Mock search error for query: {query}"))
70 }
71 ArchToolkitError::InfoFailed { packages, .. } => {
72 ArchToolkitError::Parse(format!("Mock info error for packages: {packages}"))
73 }
74 ArchToolkitError::CommentsFailed { package, .. } => {
75 ArchToolkitError::Parse(format!("Mock comments error for package: {package}"))
76 }
77 ArchToolkitError::PkgbuildFailed { package, .. } => {
78 ArchToolkitError::Parse(format!("Mock pkgbuild error for package: {package}"))
79 }
80 ArchToolkitError::Json(_) => ArchToolkitError::Parse("Mock JSON error".to_string()),
81 ArchToolkitError::Io { path, .. } => {
82 ArchToolkitError::Parse(format!("Mock I/O error at path: {path}"))
83 }
84 ArchToolkitError::Parse(s) => ArchToolkitError::Parse(s.clone()),
85 ArchToolkitError::RateLimited { retry_after } => ArchToolkitError::RateLimited {
86 retry_after: *retry_after,
87 },
88 ArchToolkitError::PackageNotFound { package } => {
89 ArchToolkitError::PackageNotFound {
90 package: package.clone(),
91 }
92 }
93 ArchToolkitError::InvalidInput(s) => ArchToolkitError::InvalidInput(s.clone()),
94 ArchToolkitError::EmptyInput { field, message } => ArchToolkitError::EmptyInput {
95 field: field.clone(),
96 message: message.clone(),
97 },
98 ArchToolkitError::InvalidPackageName { name, reason } => {
99 ArchToolkitError::InvalidPackageName {
100 name: name.clone(),
101 reason: reason.clone(),
102 }
103 }
104 ArchToolkitError::InvalidSearchQuery { reason } => {
105 ArchToolkitError::InvalidSearchQuery {
106 reason: reason.clone(),
107 }
108 }
109 ArchToolkitError::InputTooLong {
110 field,
111 max_length,
112 actual_length,
113 } => ArchToolkitError::InputTooLong {
114 field: field.clone(),
115 max_length: *max_length,
116 actual_length: *actual_length,
117 },
118 }),
119 }
120 }
121
122 #[must_use]
133 pub fn new() -> Self {
134 Self {
135 search_results: Arc::new(Mutex::new(HashMap::new())),
136 info_results: Arc::new(Mutex::new(HashMap::new())),
137 comments_results: Arc::new(Mutex::new(HashMap::new())),
138 pkgbuild_results: Arc::new(Mutex::new(HashMap::new())),
139 default_search_result: None,
140 default_info_result: None,
141 default_comments_result: None,
142 default_pkgbuild_result: None,
143 }
144 }
145
146 #[must_use]
162 pub fn with_search_result(self, query: &str, result: Result<Vec<AurPackage>>) -> Self {
163 {
164 let mut results = self
165 .search_results
166 .lock()
167 .expect("MockAurApi mutex should not be poisoned");
168 results.insert(query.to_string(), result);
169 }
170 self
171 }
172
173 #[must_use]
185 pub fn with_default_search_result(self, result: Result<Vec<AurPackage>>) -> Self {
186 Self {
187 default_search_result: Some(result),
188 ..self
189 }
190 }
191
192 #[must_use]
208 pub fn with_info_result(self, names: &[&str], result: Result<Vec<AurPackageDetails>>) -> Self {
209 let mut sorted_names = names.to_vec();
210 sorted_names.sort_unstable();
211 let key = sorted_names.join(",");
212 {
213 let mut results = self
214 .info_results
215 .lock()
216 .expect("MockAurApi mutex should not be poisoned");
217 results.insert(key, result);
218 }
219 self
220 }
221
222 #[must_use]
230 pub fn with_default_info_result(self, result: Result<Vec<AurPackageDetails>>) -> Self {
231 Self {
232 default_info_result: Some(result),
233 ..self
234 }
235 }
236
237 #[must_use]
249 pub fn with_comments_result(self, pkgname: &str, result: Result<Vec<AurComment>>) -> Self {
250 {
251 let mut results = self
252 .comments_results
253 .lock()
254 .expect("MockAurApi mutex should not be poisoned");
255 results.insert(pkgname.to_string(), result);
256 }
257 self
258 }
259
260 #[must_use]
268 pub fn with_default_comments_result(self, result: Result<Vec<AurComment>>) -> Self {
269 Self {
270 default_comments_result: Some(result),
271 ..self
272 }
273 }
274
275 #[must_use]
287 pub fn with_pkgbuild_result(self, package: &str, result: Result<String>) -> Self {
288 {
289 let mut results = self
290 .pkgbuild_results
291 .lock()
292 .expect("MockAurApi mutex should not be poisoned");
293 results.insert(package.to_string(), result);
294 }
295 self
296 }
297
298 #[must_use]
306 pub fn with_default_pkgbuild_result(self, result: Result<String>) -> Self {
307 Self {
308 default_pkgbuild_result: Some(result),
309 ..self
310 }
311 }
312}
313
314#[async_trait]
315impl AurApi for MockAurApi {
316 async fn search(&self, query: &str) -> Result<Vec<AurPackage>> {
329 let result = {
330 let results = self
331 .search_results
332 .lock()
333 .expect("MockAurApi mutex should not be poisoned");
334 results.get(query).map(Self::clone_result)
335 };
336
337 if let Some(result) = result {
338 return result;
339 }
340
341 if let Some(ref default) = self.default_search_result {
342 return Self::clone_result(default);
343 }
344
345 Err(ArchToolkitError::Parse(format!(
346 "MockAurApi: No search result configured for query '{query}'"
347 )))
348 }
349
350 async fn info(&self, names: &[&str]) -> Result<Vec<AurPackageDetails>> {
363 let mut sorted_names = names.to_vec();
364 sorted_names.sort_unstable();
365 let key = sorted_names.join(",");
366
367 let result = {
368 let results = self
369 .info_results
370 .lock()
371 .expect("MockAurApi mutex should not be poisoned");
372 results.get(&key).map(Self::clone_result)
373 };
374
375 if let Some(result) = result {
376 return result;
377 }
378
379 if let Some(ref default) = self.default_info_result {
380 return Self::clone_result(default);
381 }
382
383 Err(ArchToolkitError::Parse(format!(
384 "MockAurApi: No info result configured for packages '{key}'"
385 )))
386 }
387
388 async fn comments(&self, pkgname: &str) -> Result<Vec<AurComment>> {
401 let result = {
402 let results = self
403 .comments_results
404 .lock()
405 .expect("MockAurApi mutex should not be poisoned");
406 results.get(pkgname).map(Self::clone_result)
407 };
408
409 if let Some(result) = result {
410 return result;
411 }
412
413 if let Some(ref default) = self.default_comments_result {
414 return Self::clone_result(default);
415 }
416
417 Err(ArchToolkitError::Parse(format!(
418 "MockAurApi: No comments result configured for package '{pkgname}'"
419 )))
420 }
421
422 async fn pkgbuild(&self, package: &str) -> Result<String> {
435 let result = {
436 let results = self
437 .pkgbuild_results
438 .lock()
439 .expect("MockAurApi mutex should not be poisoned");
440 results.get(package).map(Self::clone_result)
441 };
442
443 if let Some(result) = result {
444 return result;
445 }
446
447 if let Some(ref default) = self.default_pkgbuild_result {
448 return Self::clone_result(default);
449 }
450
451 Err(ArchToolkitError::Parse(format!(
452 "MockAurApi: No pkgbuild result configured for package '{package}'"
453 )))
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 #![allow(clippy::unwrap_used)]
461
462 use super::*;
463 use crate::types::{AurComment, AurPackage, AurPackageDetails};
464
465 #[tokio::test]
466 async fn test_mock_search_success() {
467 let mock = MockAurApi::new().with_search_result(
468 "yay",
469 Ok(vec![AurPackage {
470 name: "yay".to_string(),
471 version: "12.0.0".to_string(),
472 description: "AUR helper".to_string(),
473 popularity: Some(100.0),
474 out_of_date: None,
475 orphaned: false,
476 maintainer: Some("user".to_string()),
477 }]),
478 );
479
480 let result = mock.search("yay").await;
481 assert!(result.is_ok());
482 let packages = result.unwrap();
483 assert_eq!(packages.len(), 1);
484 assert_eq!(packages[0].name, "yay");
485 }
486
487 #[tokio::test]
488 async fn test_mock_search_error() {
489 let mock = MockAurApi::new().with_search_result(
490 "error",
491 Err(ArchToolkitError::Parse("test error".to_string())),
492 );
493
494 let result = mock.search("error").await;
495 assert!(result.is_err());
496 }
497
498 #[tokio::test]
499 async fn test_mock_search_not_found() {
500 let mock = MockAurApi::new();
501 let result = mock.search("unknown").await;
502 assert!(result.is_err());
503 assert!(
504 result
505 .unwrap_err()
506 .to_string()
507 .contains("No search result configured")
508 );
509 }
510
511 #[tokio::test]
512 async fn test_mock_search_default() {
513 let mock = MockAurApi::new().with_default_search_result(Ok(vec![AurPackage {
514 name: "default".to_string(),
515 version: "1.0.0".to_string(),
516 description: "Default package".to_string(),
517 popularity: None,
518 out_of_date: None,
519 orphaned: false,
520 maintainer: None,
521 }]));
522
523 let result = mock.search("any-query").await;
524 assert!(result.is_ok());
525 let packages = result.unwrap();
526 assert_eq!(packages.len(), 1);
527 assert_eq!(packages[0].name, "default");
528 }
529
530 #[tokio::test]
531 async fn test_mock_info_success() {
532 let mock = MockAurApi::new().with_info_result(
533 &["yay"],
534 Ok(vec![AurPackageDetails {
535 name: "yay".to_string(),
536 version: "12.0.0".to_string(),
537 description: "AUR helper".to_string(),
538 url: "https://github.com/Jguer/yay".to_string(),
539 licenses: vec!["MIT".to_string()],
540 groups: vec![],
541 provides: vec![],
542 depends: vec![],
543 make_depends: vec![],
544 opt_depends: vec![],
545 conflicts: vec![],
546 replaces: vec![],
547 maintainer: Some("user".to_string()),
548 first_submitted: None,
549 last_modified: None,
550 popularity: Some(100.0),
551 num_votes: Some(1000),
552 out_of_date: None,
553 orphaned: false,
554 }]),
555 );
556
557 let result = mock.info(&["yay"]).await;
558 assert!(result.is_ok());
559 let packages = result.unwrap();
560 assert_eq!(packages.len(), 1);
561 assert_eq!(packages[0].name, "yay");
562 }
563
564 #[tokio::test]
565 async fn test_mock_info_sorted() {
566 let mock = MockAurApi::new().with_info_result(
567 &["yay", "paru"],
568 Ok(vec![AurPackageDetails {
569 name: "yay".to_string(),
570 version: "12.0.0".to_string(),
571 description: "AUR helper".to_string(),
572 url: String::new(),
573 licenses: vec![],
574 groups: vec![],
575 provides: vec![],
576 depends: vec![],
577 make_depends: vec![],
578 opt_depends: vec![],
579 conflicts: vec![],
580 replaces: vec![],
581 maintainer: None,
582 first_submitted: None,
583 last_modified: None,
584 popularity: None,
585 num_votes: None,
586 out_of_date: None,
587 orphaned: false,
588 }]),
589 );
590
591 let result1 = mock.info(&["yay", "paru"]).await;
593 assert!(result1.is_ok());
594
595 let result2 = mock.info(&["paru", "yay"]).await;
596 assert!(result2.is_ok());
597 }
598
599 #[tokio::test]
600 async fn test_mock_comments_success() {
601 let mock = MockAurApi::new().with_comments_result(
602 "yay",
603 Ok(vec![AurComment {
604 id: Some("1".to_string()),
605 author: "user".to_string(),
606 date: "2024-01-01".to_string(),
607 date_timestamp: Some(1_704_067_200),
608 date_url: None,
609 content: "Great package!".to_string(),
610 pinned: false,
611 }]),
612 );
613
614 let result = mock.comments("yay").await;
615 assert!(result.is_ok());
616 let comments = result.unwrap();
617 assert_eq!(comments.len(), 1);
618 assert_eq!(comments[0].author, "user");
619 }
620
621 #[tokio::test]
622 async fn test_mock_pkgbuild_success() {
623 let mock = MockAurApi::new()
624 .with_pkgbuild_result("yay", Ok("pkgname=yay\npkgver=12.0.0".to_string()));
625
626 let result = mock.pkgbuild("yay").await;
627 assert!(result.is_ok());
628 let pkgbuild = result.unwrap();
629 assert!(pkgbuild.contains("yay"));
630 }
631}