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::Parse(s) => ArchToolkitError::Parse(s.clone()),
82 ArchToolkitError::RateLimited { retry_after } => ArchToolkitError::RateLimited {
83 retry_after: *retry_after,
84 },
85 ArchToolkitError::PackageNotFound { package } => {
86 ArchToolkitError::PackageNotFound {
87 package: package.clone(),
88 }
89 }
90 ArchToolkitError::InvalidInput(s) => ArchToolkitError::InvalidInput(s.clone()),
91 ArchToolkitError::EmptyInput { field, message } => ArchToolkitError::EmptyInput {
92 field: field.clone(),
93 message: message.clone(),
94 },
95 ArchToolkitError::InvalidPackageName { name, reason } => {
96 ArchToolkitError::InvalidPackageName {
97 name: name.clone(),
98 reason: reason.clone(),
99 }
100 }
101 ArchToolkitError::InvalidSearchQuery { reason } => {
102 ArchToolkitError::InvalidSearchQuery {
103 reason: reason.clone(),
104 }
105 }
106 ArchToolkitError::InputTooLong {
107 field,
108 max_length,
109 actual_length,
110 } => ArchToolkitError::InputTooLong {
111 field: field.clone(),
112 max_length: *max_length,
113 actual_length: *actual_length,
114 },
115 }),
116 }
117 }
118
119 #[must_use]
130 pub fn new() -> Self {
131 Self {
132 search_results: Arc::new(Mutex::new(HashMap::new())),
133 info_results: Arc::new(Mutex::new(HashMap::new())),
134 comments_results: Arc::new(Mutex::new(HashMap::new())),
135 pkgbuild_results: Arc::new(Mutex::new(HashMap::new())),
136 default_search_result: None,
137 default_info_result: None,
138 default_comments_result: None,
139 default_pkgbuild_result: None,
140 }
141 }
142
143 #[must_use]
159 pub fn with_search_result(self, query: &str, result: Result<Vec<AurPackage>>) -> Self {
160 {
161 let mut results = self
162 .search_results
163 .lock()
164 .expect("MockAurApi mutex should not be poisoned");
165 results.insert(query.to_string(), result);
166 }
167 self
168 }
169
170 #[must_use]
182 pub fn with_default_search_result(self, result: Result<Vec<AurPackage>>) -> Self {
183 Self {
184 default_search_result: Some(result),
185 ..self
186 }
187 }
188
189 #[must_use]
205 pub fn with_info_result(self, names: &[&str], result: Result<Vec<AurPackageDetails>>) -> Self {
206 let mut sorted_names = names.to_vec();
207 sorted_names.sort_unstable();
208 let key = sorted_names.join(",");
209 {
210 let mut results = self
211 .info_results
212 .lock()
213 .expect("MockAurApi mutex should not be poisoned");
214 results.insert(key, result);
215 }
216 self
217 }
218
219 #[must_use]
227 pub fn with_default_info_result(self, result: Result<Vec<AurPackageDetails>>) -> Self {
228 Self {
229 default_info_result: Some(result),
230 ..self
231 }
232 }
233
234 #[must_use]
246 pub fn with_comments_result(self, pkgname: &str, result: Result<Vec<AurComment>>) -> Self {
247 {
248 let mut results = self
249 .comments_results
250 .lock()
251 .expect("MockAurApi mutex should not be poisoned");
252 results.insert(pkgname.to_string(), result);
253 }
254 self
255 }
256
257 #[must_use]
265 pub fn with_default_comments_result(self, result: Result<Vec<AurComment>>) -> Self {
266 Self {
267 default_comments_result: Some(result),
268 ..self
269 }
270 }
271
272 #[must_use]
284 pub fn with_pkgbuild_result(self, package: &str, result: Result<String>) -> Self {
285 {
286 let mut results = self
287 .pkgbuild_results
288 .lock()
289 .expect("MockAurApi mutex should not be poisoned");
290 results.insert(package.to_string(), result);
291 }
292 self
293 }
294
295 #[must_use]
303 pub fn with_default_pkgbuild_result(self, result: Result<String>) -> Self {
304 Self {
305 default_pkgbuild_result: Some(result),
306 ..self
307 }
308 }
309}
310
311#[async_trait]
312impl AurApi for MockAurApi {
313 async fn search(&self, query: &str) -> Result<Vec<AurPackage>> {
326 let result = {
327 let results = self
328 .search_results
329 .lock()
330 .expect("MockAurApi mutex should not be poisoned");
331 results.get(query).map(Self::clone_result)
332 };
333
334 if let Some(result) = result {
335 return result;
336 }
337
338 if let Some(ref default) = self.default_search_result {
339 return Self::clone_result(default);
340 }
341
342 Err(ArchToolkitError::Parse(format!(
343 "MockAurApi: No search result configured for query '{query}'"
344 )))
345 }
346
347 async fn info(&self, names: &[&str]) -> Result<Vec<AurPackageDetails>> {
360 let mut sorted_names = names.to_vec();
361 sorted_names.sort_unstable();
362 let key = sorted_names.join(",");
363
364 let result = {
365 let results = self
366 .info_results
367 .lock()
368 .expect("MockAurApi mutex should not be poisoned");
369 results.get(&key).map(Self::clone_result)
370 };
371
372 if let Some(result) = result {
373 return result;
374 }
375
376 if let Some(ref default) = self.default_info_result {
377 return Self::clone_result(default);
378 }
379
380 Err(ArchToolkitError::Parse(format!(
381 "MockAurApi: No info result configured for packages '{key}'"
382 )))
383 }
384
385 async fn comments(&self, pkgname: &str) -> Result<Vec<AurComment>> {
398 let result = {
399 let results = self
400 .comments_results
401 .lock()
402 .expect("MockAurApi mutex should not be poisoned");
403 results.get(pkgname).map(Self::clone_result)
404 };
405
406 if let Some(result) = result {
407 return result;
408 }
409
410 if let Some(ref default) = self.default_comments_result {
411 return Self::clone_result(default);
412 }
413
414 Err(ArchToolkitError::Parse(format!(
415 "MockAurApi: No comments result configured for package '{pkgname}'"
416 )))
417 }
418
419 async fn pkgbuild(&self, package: &str) -> Result<String> {
432 let result = {
433 let results = self
434 .pkgbuild_results
435 .lock()
436 .expect("MockAurApi mutex should not be poisoned");
437 results.get(package).map(Self::clone_result)
438 };
439
440 if let Some(result) = result {
441 return result;
442 }
443
444 if let Some(ref default) = self.default_pkgbuild_result {
445 return Self::clone_result(default);
446 }
447
448 Err(ArchToolkitError::Parse(format!(
449 "MockAurApi: No pkgbuild result configured for package '{package}'"
450 )))
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 #![allow(clippy::unwrap_used)]
458
459 use super::*;
460 use crate::types::{AurComment, AurPackage, AurPackageDetails};
461
462 #[tokio::test]
463 async fn test_mock_search_success() {
464 let mock = MockAurApi::new().with_search_result(
465 "yay",
466 Ok(vec![AurPackage {
467 name: "yay".to_string(),
468 version: "12.0.0".to_string(),
469 description: "AUR helper".to_string(),
470 popularity: Some(100.0),
471 out_of_date: None,
472 orphaned: false,
473 maintainer: Some("user".to_string()),
474 }]),
475 );
476
477 let result = mock.search("yay").await;
478 assert!(result.is_ok());
479 let packages = result.unwrap();
480 assert_eq!(packages.len(), 1);
481 assert_eq!(packages[0].name, "yay");
482 }
483
484 #[tokio::test]
485 async fn test_mock_search_error() {
486 let mock = MockAurApi::new().with_search_result(
487 "error",
488 Err(ArchToolkitError::Parse("test error".to_string())),
489 );
490
491 let result = mock.search("error").await;
492 assert!(result.is_err());
493 }
494
495 #[tokio::test]
496 async fn test_mock_search_not_found() {
497 let mock = MockAurApi::new();
498 let result = mock.search("unknown").await;
499 assert!(result.is_err());
500 assert!(
501 result
502 .unwrap_err()
503 .to_string()
504 .contains("No search result configured")
505 );
506 }
507
508 #[tokio::test]
509 async fn test_mock_search_default() {
510 let mock = MockAurApi::new().with_default_search_result(Ok(vec![AurPackage {
511 name: "default".to_string(),
512 version: "1.0.0".to_string(),
513 description: "Default package".to_string(),
514 popularity: None,
515 out_of_date: None,
516 orphaned: false,
517 maintainer: None,
518 }]));
519
520 let result = mock.search("any-query").await;
521 assert!(result.is_ok());
522 let packages = result.unwrap();
523 assert_eq!(packages.len(), 1);
524 assert_eq!(packages[0].name, "default");
525 }
526
527 #[tokio::test]
528 async fn test_mock_info_success() {
529 let mock = MockAurApi::new().with_info_result(
530 &["yay"],
531 Ok(vec![AurPackageDetails {
532 name: "yay".to_string(),
533 version: "12.0.0".to_string(),
534 description: "AUR helper".to_string(),
535 url: "https://github.com/Jguer/yay".to_string(),
536 licenses: vec!["MIT".to_string()],
537 groups: vec![],
538 provides: vec![],
539 depends: vec![],
540 make_depends: vec![],
541 opt_depends: vec![],
542 conflicts: vec![],
543 replaces: vec![],
544 maintainer: Some("user".to_string()),
545 first_submitted: None,
546 last_modified: None,
547 popularity: Some(100.0),
548 num_votes: Some(1000),
549 out_of_date: None,
550 orphaned: false,
551 }]),
552 );
553
554 let result = mock.info(&["yay"]).await;
555 assert!(result.is_ok());
556 let packages = result.unwrap();
557 assert_eq!(packages.len(), 1);
558 assert_eq!(packages[0].name, "yay");
559 }
560
561 #[tokio::test]
562 async fn test_mock_info_sorted() {
563 let mock = MockAurApi::new().with_info_result(
564 &["yay", "paru"],
565 Ok(vec![AurPackageDetails {
566 name: "yay".to_string(),
567 version: "12.0.0".to_string(),
568 description: "AUR helper".to_string(),
569 url: String::new(),
570 licenses: vec![],
571 groups: vec![],
572 provides: vec![],
573 depends: vec![],
574 make_depends: vec![],
575 opt_depends: vec![],
576 conflicts: vec![],
577 replaces: vec![],
578 maintainer: None,
579 first_submitted: None,
580 last_modified: None,
581 popularity: None,
582 num_votes: None,
583 out_of_date: None,
584 orphaned: false,
585 }]),
586 );
587
588 let result1 = mock.info(&["yay", "paru"]).await;
590 assert!(result1.is_ok());
591
592 let result2 = mock.info(&["paru", "yay"]).await;
593 assert!(result2.is_ok());
594 }
595
596 #[tokio::test]
597 async fn test_mock_comments_success() {
598 let mock = MockAurApi::new().with_comments_result(
599 "yay",
600 Ok(vec![AurComment {
601 id: Some("1".to_string()),
602 author: "user".to_string(),
603 date: "2024-01-01".to_string(),
604 date_timestamp: Some(1_704_067_200),
605 date_url: None,
606 content: "Great package!".to_string(),
607 pinned: false,
608 }]),
609 );
610
611 let result = mock.comments("yay").await;
612 assert!(result.is_ok());
613 let comments = result.unwrap();
614 assert_eq!(comments.len(), 1);
615 assert_eq!(comments[0].author, "user");
616 }
617
618 #[tokio::test]
619 async fn test_mock_pkgbuild_success() {
620 let mock = MockAurApi::new()
621 .with_pkgbuild_result("yay", Ok("pkgname=yay\npkgver=12.0.0".to_string()));
622
623 let result = mock.pkgbuild("yay").await;
624 assert!(result.is_ok());
625 let pkgbuild = result.unwrap();
626 assert!(pkgbuild.contains("yay"));
627 }
628}