1use std::fmt::{self, Display, Formatter};
2use std::fs::File;
3use std::path::{Path, PathBuf};
4
5use crate::api_traits::{
6 Cicd, CicdJob, CicdRunner, CodeGist, CommentMergeRequest, ContainerRegistry, Deploy,
7 DeployAsset, MergeRequest, ProjectMember, RemoteProject, RemoteTag, TrendingProjectURL,
8 UserInfo,
9};
10use crate::cache::{filesystem::FileCache, nocache::NoCache};
11use crate::config::{env_token, ConfigFile, NoConfig};
12use crate::display::Format;
13use crate::error::GRError;
14use crate::github::Github;
15use crate::gitlab::Gitlab;
16use crate::io::{CmdInfo, HttpResponse, HttpRunner, ShellResponse, TaskRunner};
17use crate::time::Milliseconds;
18use crate::{cli, error, get_default_config_path, http, log_debug, log_info};
19use crate::{git, Result};
20use std::sync::Arc;
21
22pub mod query;
23
24#[derive(Builder, Clone)]
26pub struct ListRemoteCliArgs {
27 #[builder(default)]
28 pub from_page: Option<i64>,
29 #[builder(default)]
30 pub to_page: Option<i64>,
31 #[builder(default)]
32 pub num_pages: bool,
33 #[builder(default)]
34 pub num_resources: bool,
35 #[builder(default)]
36 pub page_number: Option<i64>,
37 #[builder(default)]
38 pub created_after: Option<String>,
39 #[builder(default)]
40 pub created_before: Option<String>,
41 #[builder(default)]
42 pub sort: ListSortMode,
43 #[builder(default)]
44 pub flush: bool,
45 #[builder(default)]
46 pub throttle_time: Option<Milliseconds>,
47 #[builder(default)]
48 pub throttle_range: Option<(Milliseconds, Milliseconds)>,
49 #[builder(default)]
50 pub get_args: GetRemoteCliArgs,
51}
52
53impl ListRemoteCliArgs {
54 pub fn builder() -> ListRemoteCliArgsBuilder {
55 ListRemoteCliArgsBuilder::default()
56 }
57}
58
59#[derive(Builder, Clone, Default)]
60pub struct GetRemoteCliArgs {
61 #[builder(default)]
62 pub no_headers: bool,
63 #[builder(default)]
64 pub format: Format,
65 #[builder(default)]
66 pub cache_args: CacheCliArgs,
67 #[builder(default)]
68 pub display_optional: bool,
69 #[builder(default)]
70 pub backoff_max_retries: u32,
71 #[builder(default)]
72 pub backoff_retry_after: u64,
73}
74
75impl GetRemoteCliArgs {
76 pub fn builder() -> GetRemoteCliArgsBuilder {
77 GetRemoteCliArgsBuilder::default()
78 }
79}
80
81#[derive(Builder, Clone, Default)]
82pub struct CacheCliArgs {
83 #[builder(default)]
84 pub refresh: bool,
85 #[builder(default)]
86 pub no_cache: bool,
87}
88
89impl CacheCliArgs {
90 pub fn builder() -> CacheCliArgsBuilder {
91 CacheCliArgsBuilder::default()
92 }
93}
94
95#[derive(Builder, Clone)]
101pub struct ListBodyArgs {
102 #[builder(setter(strip_option), default)]
103 pub page: Option<i64>,
104 #[builder(setter(strip_option), default)]
105 pub max_pages: Option<i64>,
106 #[builder(default)]
107 pub created_after: Option<String>,
108 #[builder(default)]
109 pub created_before: Option<String>,
110 #[builder(default)]
111 pub sort_mode: ListSortMode,
112 #[builder(default)]
113 pub flush: bool,
114 #[builder(default)]
115 pub throttle_time: Option<Milliseconds>,
116 #[builder(default)]
117 pub throttle_range: Option<(Milliseconds, Milliseconds)>,
118 #[builder(default)]
120 pub get_args: GetRemoteCliArgs,
121}
122
123impl ListBodyArgs {
124 pub fn builder() -> ListBodyArgsBuilder {
125 ListBodyArgsBuilder::default()
126 }
127}
128
129pub fn validate_from_to_page(remote_cli_args: &ListRemoteCliArgs) -> Result<Option<ListBodyArgs>> {
130 if remote_cli_args.page_number.is_some() {
131 return Ok(Some(
132 ListBodyArgs::builder()
133 .page(remote_cli_args.page_number.unwrap())
134 .max_pages(1)
135 .sort_mode(remote_cli_args.sort.clone())
136 .created_after(remote_cli_args.created_after.clone())
137 .created_before(remote_cli_args.created_before.clone())
138 .build()
139 .unwrap(),
140 ));
141 }
142 let body_args = match (remote_cli_args.from_page, remote_cli_args.to_page) {
144 (Some(from_page), Some(to_page)) => {
145 if from_page < 0 || to_page < 0 {
146 return Err(GRError::PreconditionNotMet(
147 "from_page and to_page must be a positive number".to_string(),
148 )
149 .into());
150 }
151 if from_page >= to_page {
152 return Err(GRError::PreconditionNotMet(
153 "from_page must be less than to_page".to_string(),
154 )
155 .into());
156 }
157
158 let max_pages = to_page - from_page + 1;
159 Some(
160 ListBodyArgs::builder()
161 .page(from_page)
162 .max_pages(max_pages)
163 .sort_mode(remote_cli_args.sort.clone())
164 .flush(remote_cli_args.flush)
165 .throttle_time(remote_cli_args.throttle_time)
166 .throttle_range(remote_cli_args.throttle_range)
167 .get_args(remote_cli_args.get_args.clone())
168 .build()
169 .unwrap(),
170 )
171 }
172 (Some(_), None) => {
173 return Err(
174 GRError::PreconditionNotMet("from_page requires the to_page".to_string()).into(),
175 );
176 }
177 (None, Some(to_page)) => {
178 if to_page < 0 {
179 return Err(GRError::PreconditionNotMet(
180 "to_page must be a positive number".to_string(),
181 )
182 .into());
183 }
184 Some(
185 ListBodyArgs::builder()
186 .page(1)
187 .max_pages(to_page)
188 .sort_mode(remote_cli_args.sort.clone())
189 .flush(remote_cli_args.flush)
190 .throttle_time(remote_cli_args.throttle_time)
191 .throttle_range(remote_cli_args.throttle_range)
192 .get_args(remote_cli_args.get_args.clone())
193 .build()
194 .unwrap(),
195 )
196 }
197 (None, None) => None,
198 };
199 match (
200 remote_cli_args.created_after.clone(),
201 remote_cli_args.created_before.clone(),
202 ) {
203 (Some(created_after), Some(created_before)) => {
204 if let Some(body_args) = &body_args {
205 return Ok(Some(
206 ListBodyArgs::builder()
207 .page(body_args.page.unwrap())
208 .max_pages(body_args.max_pages.unwrap())
209 .created_after(Some(created_after.to_string()))
210 .created_before(Some(created_before.to_string()))
211 .sort_mode(remote_cli_args.sort.clone())
212 .flush(remote_cli_args.flush)
213 .throttle_time(remote_cli_args.throttle_time)
214 .throttle_range(remote_cli_args.throttle_range)
215 .get_args(remote_cli_args.get_args.clone())
216 .build()
217 .unwrap(),
218 ));
219 }
220 Ok(Some(
221 ListBodyArgs::builder()
222 .created_after(Some(created_after.to_string()))
223 .created_before(Some(created_before.to_string()))
224 .sort_mode(remote_cli_args.sort.clone())
225 .flush(remote_cli_args.flush)
226 .throttle_time(remote_cli_args.throttle_time)
227 .throttle_range(remote_cli_args.throttle_range)
228 .get_args(remote_cli_args.get_args.clone())
229 .build()
230 .unwrap(),
231 ))
232 }
233 (Some(created_after), None) => {
234 if let Some(body_args) = &body_args {
235 return Ok(Some(
236 ListBodyArgs::builder()
237 .page(body_args.page.unwrap())
238 .max_pages(body_args.max_pages.unwrap())
239 .created_after(Some(created_after.to_string()))
240 .sort_mode(remote_cli_args.sort.clone())
241 .flush(remote_cli_args.flush)
242 .throttle_time(remote_cli_args.throttle_time)
243 .throttle_range(remote_cli_args.throttle_range)
244 .get_args(remote_cli_args.get_args.clone())
245 .build()
246 .unwrap(),
247 ));
248 }
249 Ok(Some(
250 ListBodyArgs::builder()
251 .created_after(Some(created_after.to_string()))
252 .sort_mode(remote_cli_args.sort.clone())
253 .flush(remote_cli_args.flush)
254 .throttle_time(remote_cli_args.throttle_time)
255 .throttle_range(remote_cli_args.throttle_range)
256 .get_args(remote_cli_args.get_args.clone())
257 .build()
258 .unwrap(),
259 ))
260 }
261 (None, Some(created_before)) => {
262 if let Some(body_args) = &body_args {
263 return Ok(Some(
264 ListBodyArgs::builder()
265 .page(body_args.page.unwrap())
266 .max_pages(body_args.max_pages.unwrap())
267 .created_before(Some(created_before.to_string()))
268 .sort_mode(remote_cli_args.sort.clone())
269 .flush(remote_cli_args.flush)
270 .throttle_time(remote_cli_args.throttle_time)
271 .throttle_range(remote_cli_args.throttle_range)
272 .get_args(remote_cli_args.get_args.clone())
273 .build()
274 .unwrap(),
275 ));
276 }
277 Ok(Some(
278 ListBodyArgs::builder()
279 .created_before(Some(created_before.to_string()))
280 .sort_mode(remote_cli_args.sort.clone())
281 .flush(remote_cli_args.flush)
282 .throttle_time(remote_cli_args.throttle_time)
283 .throttle_range(remote_cli_args.throttle_range)
284 .get_args(remote_cli_args.get_args.clone())
285 .build()
286 .unwrap(),
287 ))
288 }
289 (None, None) => {
290 if let Some(body_args) = &body_args {
291 return Ok(Some(
292 ListBodyArgs::builder()
293 .page(body_args.page.unwrap())
294 .max_pages(body_args.max_pages.unwrap())
295 .sort_mode(remote_cli_args.sort.clone())
296 .flush(remote_cli_args.flush)
297 .throttle_time(remote_cli_args.throttle_time)
298 .throttle_range(remote_cli_args.throttle_range)
299 .get_args(remote_cli_args.get_args.clone())
300 .build()
301 .unwrap(),
302 ));
303 }
304 Ok(Some(
305 ListBodyArgs::builder()
306 .sort_mode(remote_cli_args.sort.clone())
307 .flush(remote_cli_args.flush)
308 .throttle_time(remote_cli_args.throttle_time)
309 .throttle_range(remote_cli_args.throttle_range)
310 .get_args(remote_cli_args.get_args.clone())
311 .build()
312 .unwrap(),
313 ))
314 }
315 }
316}
317
318pub struct URLQueryParamBuilder {
319 url: String,
320}
321
322impl URLQueryParamBuilder {
323 pub fn new(url: &str) -> Self {
324 URLQueryParamBuilder {
325 url: url.to_string(),
326 }
327 }
328
329 pub fn add_param(&mut self, key: &str, value: &str) -> &mut Self {
330 if self.url.contains('?') {
331 self.url.push_str(&format!("&{}={}", key, value));
332 } else {
333 self.url.push_str(&format!("?{}={}", key, value));
334 }
335 self
336 }
337
338 pub fn build(&self) -> String {
339 self.url.clone()
340 }
341}
342
343#[derive(Clone, Debug, Default, PartialEq)]
344pub enum ListSortMode {
345 #[default]
346 Asc,
347 Desc,
348}
349
350#[derive(Clone, Debug, PartialEq)]
351pub enum CacheType {
352 File,
353 None,
354}
355
356use crate::config::ConfigProperties;
357macro_rules! get {
358 ($func_name:ident, $trait_name:ident) => {
359 paste::paste! {
360 pub fn $func_name(
361 domain: String,
362 path: String,
363 config: Arc<dyn ConfigProperties + Send + Sync + 'static>,
364 cache_args: Option<&CacheCliArgs>,
365 cache_type: CacheType,
366 ) -> Result<Arc<dyn $trait_name + Send + Sync + 'static>> {
367 let refresh_cache = cache_args.map_or(false, |args| args.refresh);
368 let no_cache_args = cache_args.map_or(false, |args| args.no_cache);
369
370 log_debug!("cache_type: {:?}", cache_type);
371 log_debug!("no_cache_args: {:?}", no_cache_args);
372 log_debug!("cache location: {:?}", config.cache_location());
373
374 if cache_type == CacheType::None || no_cache_args || config.cache_location().is_none() {
375 log_info!("No cache used for {}", stringify!($func_name));
376 let runner = Arc::new(http::Client::new(NoCache, config.clone(), refresh_cache));
377 [<create_remote_ $func_name>](domain, path, config, runner)
378 } else {
379 log_info!("File cache used for {}", stringify!($func_name));
380 let file_cache = FileCache::new(config.clone());
381 file_cache.validate_cache_location()?;
382 let runner = Arc::new(http::Client::new(file_cache, config.clone(), refresh_cache));
383 [<create_remote_ $func_name>](domain, path, config, runner)
384 }
385 }
386
387 fn [<create_remote_ $func_name>]<R>(
388 domain: String,
389 path: String,
390 config: Arc<dyn ConfigProperties>,
391 runner: Arc<R>,
392 ) -> Result<Arc<dyn $trait_name + Send + Sync + 'static>>
393 where
394 R: HttpRunner<Response = HttpResponse> + Send + Sync + 'static,
395 {
396 let github_domain_regex = regex::Regex::new(r"^github").unwrap();
397 let gitlab_domain_regex = regex::Regex::new(r"^gitlab").unwrap();
398 let remote: Arc<dyn $trait_name + Send + Sync + 'static> =
399 if github_domain_regex.is_match(&domain) {
400 Arc::new(Github::new(config, &domain, &path, runner))
401 } else if gitlab_domain_regex.is_match(&domain) {
402 Arc::new(Gitlab::new(config, &domain, &path, runner))
403 } else {
404 return Err(error::gen(format!("Unsupported domain: {}", &domain)));
405 };
406 Ok(remote)
407 }
408 }
409 };
410}
411
412get!(get_mr, MergeRequest);
413get!(get_cicd, Cicd);
414get!(get_project, RemoteProject);
415get!(get_tag, RemoteTag);
416get!(get_user, UserInfo);
417get!(get_project_member, ProjectMember);
418get!(get_registry, ContainerRegistry);
419get!(get_deploy, Deploy);
420get!(get_deploy_asset, DeployAsset);
421get!(get_auth_user, UserInfo);
422get!(get_cicd_runner, CicdRunner);
423get!(get_comment_mr, CommentMergeRequest);
424get!(get_trending, TrendingProjectURL);
425get!(get_gist, CodeGist);
426get!(get_cicd_job, CicdJob);
427
428pub fn extract_domain_path(repo_cli: &str) -> (String, String) {
429 let parts: Vec<&str> = repo_cli.split('/').collect();
430 let domain = parts[0].to_string();
431 let path = parts[1..].join("/");
432 (domain, path)
433}
434
435pub enum CliDomainRequirements {
440 CdInLocalRepo,
441 DomainArgs,
442 RepoArgs,
443}
444
445#[derive(Clone, Debug, Default)]
446pub struct RemoteURL {
447 domain: String,
449 path: String,
451 config_encoded_project_path: String,
455 config_encoded_domain: String,
456}
457
458impl RemoteURL {
459 pub fn new(domain: String, path: String) -> Self {
460 let config_encoded_project_path = path.replace("/", "_");
461 let config_encoded_domain = domain.replace(".", "_");
462 RemoteURL {
463 domain,
464 path,
465 config_encoded_project_path,
466 config_encoded_domain,
467 }
468 }
469
470 pub fn domain(&self) -> &str {
471 &self.domain
472 }
473
474 pub fn path(&self) -> &str {
475 &self.path
476 }
477
478 pub fn config_encoded_project_path(&self) -> &str {
479 &self.config_encoded_project_path
480 }
481
482 pub fn config_encoded_domain(&self) -> &str {
483 &self.config_encoded_domain
484 }
485}
486
487impl CliDomainRequirements {
488 pub fn check<R: TaskRunner<Response = ShellResponse>>(
489 &self,
490 cli_args: &cli::CliArgs,
491 runner: &R,
492 mr_target_repo: &Option<&str>,
493 ) -> Result<RemoteURL> {
494 match self {
495 CliDomainRequirements::CdInLocalRepo => match git::remote_url(runner) {
496 Ok(CmdInfo::RemoteUrl(url)) => {
497 if let Some(target_repo) = mr_target_repo {
501 Ok(RemoteURL::new(
502 url.domain().to_string(),
503 target_repo.to_string(),
504 ))
505 } else {
506 Ok(url)
507 }
508 }
509 Err(err) => Err(GRError::GitRemoteUrlNotFound(format!("{}", err)).into()),
510 _ => Err(GRError::ApplicationError(
511 "Could not get remote url during startup. \
512 main::get_config_domain_path - Please open a bug to \
513 https://github.com/jordilin/gitar"
514 .to_string(),
515 )
516 .into()),
517 },
518 CliDomainRequirements::DomainArgs => {
519 if cli_args.domain.is_some() {
520 Ok(RemoteURL::new(
521 cli_args.domain.as_ref().unwrap().to_string(),
522 "".to_string(),
523 ))
524 } else {
525 Err(GRError::DomainExpected("Missing domain information".to_string()).into())
526 }
527 }
528 CliDomainRequirements::RepoArgs => {
529 if cli_args.repo.is_some() {
530 let (domain, path) = extract_domain_path(cli_args.repo.as_ref().unwrap());
531 Ok(RemoteURL::new(domain, path))
532 } else {
533 Err(GRError::RepoExpected("Missing repository information".to_string()).into())
534 }
535 }
536 }
537 }
538}
539
540impl Display for CliDomainRequirements {
541 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
542 match self {
543 CliDomainRequirements::CdInLocalRepo => write!(f, "cd to a git repository"),
544 CliDomainRequirements::DomainArgs => write!(f, "provide --domain option"),
545 CliDomainRequirements::RepoArgs => write!(f, "provide --repo option"),
546 }
547 }
548}
549
550pub fn url<R: TaskRunner<Response = ShellResponse>>(
551 cli_args: &cli::CliArgs,
552 requirements: &[CliDomainRequirements],
553 runner: &R,
554 mr_target_repo: &Option<&str>,
555) -> Result<RemoteURL> {
556 let mut errors = Vec::new();
557 for requirement in requirements {
558 match requirement.check(cli_args, runner, mr_target_repo) {
559 Ok(url) => return Ok(url),
560 Err(err) => {
561 errors.push(err);
562 }
563 }
564 }
565 let trace = errors
566 .iter()
567 .map(|e| format!("{}", e))
568 .collect::<Vec<String>>()
569 .join("\n");
570
571 let expectations_missed_trace = requirements
572 .iter()
573 .map(|r| format!("{}", r))
574 .collect::<Vec<String>>()
575 .join(" OR ");
576
577 Err(GRError::PreconditionNotMet(format!(
578 "\n\nMissed requirements: {}\n\n Errors:\n\n {}",
579 expectations_missed_trace, trace
580 ))
581 .into())
582}
583
584pub fn read_config(
613 config_path: ConfigFilePath,
614 url: &RemoteURL,
615) -> Result<Arc<dyn ConfigProperties>> {
616 let enc_domain = url.config_encoded_domain();
617
618 let domain_config_file = config_path.directory.join(format!("{}.toml", enc_domain));
619 let domain_project_file = config_path.directory.join(format!(
620 "{}_{}.toml",
621 enc_domain,
622 url.config_encoded_project_path()
623 ));
624
625 log_debug!("config_file: {:?}", config_path.file_name);
626 log_debug!("domain_config_file: {:?}", domain_config_file);
627 log_debug!("domain_project_config_file: {:?}", domain_project_file);
628
629 let mut extra_configs = [domain_config_file, domain_project_file]
630 .into_iter()
631 .map(PathBuf::from)
632 .collect::<Vec<PathBuf>>();
633
634 fn open_files(file_paths: &[PathBuf]) -> Vec<File> {
635 file_paths
636 .iter()
637 .filter_map(|path| match File::open(path) {
638 Ok(file) => Some(file),
639 Err(e) => {
640 log_debug!("Could not open file: {:?} - {}", path, e);
641 None
642 }
643 })
644 .collect()
645 }
646
647 extra_configs.push(config_path.file_name);
648 let files = open_files(&extra_configs);
649 if files.is_empty() {
650 let config = NoConfig::new(url.domain(), env_token)?;
651 return Ok(Arc::new(config));
652 }
653 let config = ConfigFile::new(files, url, env_token)?;
654 Ok(Arc::new(config))
655}
656
657pub struct ConfigFilePath {
660 directory: PathBuf,
661 file_name: PathBuf,
662}
663
664impl ConfigFilePath {
665 pub fn new(cli_args: &cli::CliArgs) -> Self {
666 let directory = if let Some(ref config) = cli_args.config {
667 &Path::new(config).to_path_buf()
668 } else {
669 get_default_config_path()
670 };
671 let file_name = directory.join("gitar.toml");
672 ConfigFilePath {
673 directory: directory.clone(),
674 file_name,
675 }
676 }
677
678 pub fn directory(&self) -> &PathBuf {
679 &self.directory
680 }
681
682 pub fn file_name(&self) -> &PathBuf {
683 &self.file_name
684 }
685}
686
687#[cfg(test)]
688mod test {
689 use cli::CliArgs;
690
691 use crate::test::utils::MockRunner;
692
693 use super::*;
694
695 #[test]
696 fn test_cli_from_to_pages_valid_range() {
697 let from_page = Option::Some(1);
698 let to_page = Option::Some(3);
699 let args = ListRemoteCliArgs::builder()
700 .from_page(from_page)
701 .to_page(to_page)
702 .build()
703 .unwrap();
704 let args = validate_from_to_page(&args).unwrap().unwrap();
705 assert_eq!(args.page, Some(1));
706 assert_eq!(args.max_pages, Some(3));
707 }
708
709 #[test]
710 fn test_cli_from_to_pages_invalid_range() {
711 let from_page = Some(5);
712 let to_page = Some(2);
713 let args = ListRemoteCliArgs::builder()
714 .from_page(from_page)
715 .to_page(to_page)
716 .build()
717 .unwrap();
718 let args = validate_from_to_page(&args);
719 match args {
720 Err(err) => match err.downcast_ref::<error::GRError>() {
721 Some(error::GRError::PreconditionNotMet(_)) => (),
722 _ => panic!("Expected error::GRError::PreconditionNotMet"),
723 },
724 _ => panic!("Expected error"),
725 }
726 }
727
728 #[test]
729 fn test_cli_from_page_negative_number_is_error() {
730 let from_page = Some(-5);
731 let to_page = Some(5);
732 let args = ListRemoteCliArgs::builder()
733 .from_page(from_page)
734 .to_page(to_page)
735 .build()
736 .unwrap();
737 let args = validate_from_to_page(&args);
738 match args {
739 Err(err) => match err.downcast_ref::<error::GRError>() {
740 Some(error::GRError::PreconditionNotMet(_)) => (),
741 _ => panic!("Expected error::GRError::PreconditionNotMet"),
742 },
743 _ => panic!("Expected error"),
744 }
745 }
746
747 #[test]
748 fn test_cli_to_page_negative_number_is_error() {
749 let from_page = Some(5);
750 let to_page = Some(-5);
751 let args = ListRemoteCliArgs::builder()
752 .from_page(from_page)
753 .to_page(to_page)
754 .build()
755 .unwrap();
756 let args = validate_from_to_page(&args);
757 match args {
758 Err(err) => match err.downcast_ref::<error::GRError>() {
759 Some(error::GRError::PreconditionNotMet(_)) => (),
760 _ => panic!("Expected error::GRError::PreconditionNotMet"),
761 },
762 _ => panic!("Expected error"),
763 }
764 }
765
766 #[test]
767 fn test_cli_from_page_without_to_page_is_error() {
768 let from_page = Some(5);
769 let to_page = None;
770 let args = ListRemoteCliArgs::builder()
771 .from_page(from_page)
772 .to_page(to_page)
773 .build()
774 .unwrap();
775 let args = validate_from_to_page(&args);
776 match args {
777 Err(err) => match err.downcast_ref::<error::GRError>() {
778 Some(error::GRError::PreconditionNotMet(_)) => (),
779 _ => panic!("Expected error::GRError::PreconditionNotMet"),
780 },
781 _ => panic!("Expected error"),
782 }
783 }
784
785 #[test]
786 fn test_if_from_and_to_provided_must_be_positive() {
787 let from_page = Some(-5);
788 let to_page = Some(-5);
789 let args = ListRemoteCliArgs::builder()
790 .from_page(from_page)
791 .to_page(to_page)
792 .build()
793 .unwrap();
794 let args = validate_from_to_page(&args);
795 match args {
796 Err(err) => match err.downcast_ref::<error::GRError>() {
797 Some(error::GRError::PreconditionNotMet(_)) => (),
798 _ => panic!("Expected error::GRError::PreconditionNotMet"),
799 },
800 _ => panic!("Expected error"),
801 }
802 }
803
804 #[test]
805 fn test_if_page_number_provided_max_pages_is_1() {
806 let page_number = Some(5);
807 let args = ListRemoteCliArgs::builder()
808 .page_number(page_number)
809 .build()
810 .unwrap();
811 let args = validate_from_to_page(&args).unwrap().unwrap();
812 assert_eq!(args.page, Some(5));
813 assert_eq!(args.max_pages, Some(1));
814 }
815
816 #[test]
817 fn test_include_created_after_in_list_body_args() {
818 let created_after = "2021-01-01T00:00:00Z";
819 let args = ListRemoteCliArgs::builder()
820 .created_after(Some(created_after.to_string()))
821 .build()
822 .unwrap();
823 let args = validate_from_to_page(&args).unwrap().unwrap();
824 assert_eq!(args.created_after.unwrap(), created_after);
825 }
826
827 #[test]
828 fn test_includes_from_to_page_and_created_after_in_list_body_args() {
829 let from_page = Some(1);
830 let to_page = Some(3);
831 let created_after = "2021-01-01T00:00:00Z";
832 let args = ListRemoteCliArgs::builder()
833 .from_page(from_page)
834 .to_page(to_page)
835 .created_after(Some(created_after.to_string()))
836 .build()
837 .unwrap();
838 let args = validate_from_to_page(&args).unwrap().unwrap();
839 assert_eq!(args.page, Some(1));
840 assert_eq!(args.max_pages, Some(3));
841 assert_eq!(args.created_after.unwrap(), created_after);
842 }
843
844 #[test]
845 fn test_includes_sort_mode_in_list_body_args_used_with_created_after() {
846 let args = ListRemoteCliArgs::builder()
847 .created_after(Some("2021-01-01T00:00:00Z".to_string()))
848 .sort(ListSortMode::Desc)
849 .build()
850 .unwrap();
851 let args = validate_from_to_page(&args).unwrap().unwrap();
852 assert_eq!(args.sort_mode, ListSortMode::Desc);
853 }
854
855 #[test]
856 fn test_includes_sort_mode_in_list_body_args_used_with_from_to_page() {
857 let from_page = Some(1);
858 let to_page = Some(3);
859 let args = ListRemoteCliArgs::builder()
860 .from_page(from_page)
861 .to_page(to_page)
862 .sort(ListSortMode::Desc)
863 .build()
864 .unwrap();
865 let args = validate_from_to_page(&args).unwrap().unwrap();
866 assert_eq!(args.sort_mode, ListSortMode::Desc);
867 }
868
869 #[test]
870 fn test_includes_sort_mode_in_list_body_args_used_with_page_number() {
871 let page_number = Some(1);
872 let args = ListRemoteCliArgs::builder()
873 .page_number(page_number)
874 .sort(ListSortMode::Desc)
875 .build()
876 .unwrap();
877 let args = validate_from_to_page(&args).unwrap().unwrap();
878 assert_eq!(args.sort_mode, ListSortMode::Desc);
879 }
880
881 #[test]
882 fn test_add_created_after_with_page_number() {
883 let page_number = Some(1);
884 let created_after = "2021-01-01T00:00:00Z";
885 let args = ListRemoteCliArgs::builder()
886 .page_number(page_number)
887 .created_after(Some(created_after.to_string()))
888 .build()
889 .unwrap();
890 let args = validate_from_to_page(&args).unwrap().unwrap();
891 assert_eq!(args.page.unwrap(), 1);
892 assert_eq!(args.max_pages.unwrap(), 1);
893 assert_eq!(args.created_after.unwrap(), created_after);
894 }
895
896 #[test]
897 fn test_add_created_before_with_page_number() {
898 let page_number = Some(1);
899 let created_before = "2021-01-01T00:00:00Z";
900 let args = ListRemoteCliArgs::builder()
901 .page_number(page_number)
902 .created_before(Some(created_before.to_string()))
903 .build()
904 .unwrap();
905 let args = validate_from_to_page(&args).unwrap().unwrap();
906 assert_eq!(args.page.unwrap(), 1);
907 assert_eq!(args.max_pages.unwrap(), 1);
908 assert_eq!(args.created_before.unwrap(), created_before);
909 }
910
911 #[test]
912 fn test_add_created_before_with_from_to_page() {
913 let from_page = Some(1);
914 let to_page = Some(3);
915 let created_before = "2021-01-01T00:00:00Z";
916 let args = ListRemoteCliArgs::builder()
917 .from_page(from_page)
918 .to_page(to_page)
919 .created_before(Some(created_before.to_string()))
920 .sort(ListSortMode::Desc)
921 .build()
922 .unwrap();
923 let args = validate_from_to_page(&args).unwrap().unwrap();
924 assert_eq!(args.page.unwrap(), 1);
925 assert_eq!(args.max_pages.unwrap(), 3);
926 assert_eq!(args.created_before.unwrap(), created_before);
927 assert_eq!(args.sort_mode, ListSortMode::Desc);
928 }
929
930 #[test]
931 fn test_add_crated_before_with_no_created_after_option_and_no_page_number() {
932 let created_before = "2021-01-01T00:00:00Z";
933 let args = ListRemoteCliArgs::builder()
934 .created_before(Some(created_before.to_string()))
935 .sort(ListSortMode::Desc)
936 .build()
937 .unwrap();
938 let args = validate_from_to_page(&args).unwrap().unwrap();
939 assert_eq!(args.created_before.unwrap(), created_before);
940 assert_eq!(args.sort_mode, ListSortMode::Desc);
941 }
942
943 #[test]
944 fn test_adds_created_after_and_created_before_with_from_to_page() {
945 let from_page = Some(1);
946 let to_page = Some(3);
947 let created_after = "2021-01-01T00:00:00Z";
948 let created_before = "2021-01-02T00:00:00Z";
949 let args = ListRemoteCliArgs::builder()
950 .from_page(from_page)
951 .to_page(to_page)
952 .created_after(Some(created_after.to_string()))
953 .created_before(Some(created_before.to_string()))
954 .sort(ListSortMode::Desc)
955 .build()
956 .unwrap();
957 let args = validate_from_to_page(&args).unwrap().unwrap();
958 assert_eq!(args.page.unwrap(), 1);
959 assert_eq!(args.max_pages.unwrap(), 3);
960 assert_eq!(args.created_after.unwrap(), created_after);
961 assert_eq!(args.created_before.unwrap(), created_before);
962 assert_eq!(args.sort_mode, ListSortMode::Desc);
963 }
964
965 #[test]
966 fn test_add_created_after_and_before_no_from_to_page_options() {
967 let created_after = "2021-01-01T00:00:00Z";
968 let created_before = "2021-01-02T00:00:00Z";
969 let args = ListRemoteCliArgs::builder()
970 .created_after(Some(created_after.to_string()))
971 .created_before(Some(created_before.to_string()))
972 .sort(ListSortMode::Desc)
973 .build()
974 .unwrap();
975 let args = validate_from_to_page(&args).unwrap().unwrap();
976 assert_eq!(args.created_after.unwrap(), created_after);
977 assert_eq!(args.created_before.unwrap(), created_before);
978 assert_eq!(args.sort_mode, ListSortMode::Desc);
979 }
980
981 #[test]
982 fn test_if_only_to_page_provided_max_pages_is_to_page() {
983 let to_page = Some(3);
984 let args = ListRemoteCliArgs::builder()
985 .to_page(to_page)
986 .build()
987 .unwrap();
988 let args = validate_from_to_page(&args).unwrap().unwrap();
989 assert_eq!(args.page, Some(1));
990 assert_eq!(args.max_pages, Some(3));
991 }
992
993 #[test]
994 fn test_if_only_to_page_provided_and_negative_number_is_error() {
995 let to_page = Some(-3);
996 let args = ListRemoteCliArgs::builder()
997 .to_page(to_page)
998 .build()
999 .unwrap();
1000 let args = validate_from_to_page(&args);
1001 match args {
1002 Err(err) => match err.downcast_ref::<error::GRError>() {
1003 Some(error::GRError::PreconditionNotMet(_)) => (),
1004 _ => panic!("Expected error::GRError::PreconditionNotMet"),
1005 },
1006 _ => panic!("Expected error"),
1007 }
1008 }
1009
1010 #[test]
1011 fn test_if_sort_provided_use_it() {
1012 let args = ListRemoteCliArgs::builder()
1013 .sort(ListSortMode::Desc)
1014 .build()
1015 .unwrap();
1016 let args = validate_from_to_page(&args).unwrap().unwrap();
1017 assert_eq!(args.sort_mode, ListSortMode::Desc);
1018 }
1019
1020 #[test]
1021 fn test_if_flush_option_provided_use_it() {
1022 let args = ListRemoteCliArgs::builder().flush(true).build().unwrap();
1023 let args = validate_from_to_page(&args).unwrap().unwrap();
1024 assert!(args.flush);
1025 }
1026
1027 #[test]
1028 fn test_query_param_builder_no_params() {
1029 let url = "https://example.com";
1030 let url = URLQueryParamBuilder::new(url).build();
1031 assert_eq!(url, "https://example.com");
1032 }
1033
1034 #[test]
1035 fn test_query_param_builder_with_params() {
1036 let url = "https://example.com";
1037 let url = URLQueryParamBuilder::new(url)
1038 .add_param("key", "value")
1039 .add_param("key2", "value2")
1040 .build();
1041 assert_eq!(url, "https://example.com?key=value&key2=value2");
1042 }
1043
1044 #[test]
1045 fn test_retrieve_domain_path_from_repo_cli_flag() {
1046 let repo_cli = "github.com/jordilin/gitar";
1047 let (domain, path) = extract_domain_path(repo_cli);
1048 assert_eq!("github.com", domain);
1049 assert_eq!("jordilin/gitar", path);
1050 }
1051
1052 #[test]
1053 fn test_cli_requires_cd_local_repo_run_git_remote() {
1054 let cli_args = CliArgs::new(0, None, None, None);
1055 let response = ShellResponse::builder()
1056 .body("git@github.com:jordilin/gitar.git".to_string())
1057 .build()
1058 .unwrap();
1059 let runner = MockRunner::new(vec![response]);
1060 let requirements = vec![CliDomainRequirements::CdInLocalRepo];
1061 let url = url(&cli_args, &requirements, &runner, &None).unwrap();
1062 assert_eq!("github.com", url.domain());
1063 assert_eq!("jordilin/gitar", url.path());
1064 }
1065
1066 #[test]
1067 fn test_cli_requires_cd_local_repo_run_git_remote_error() {
1068 let cli_args = CliArgs::new(0, None, None, None);
1069 let response = ShellResponse::builder()
1070 .body("".to_string())
1071 .build()
1072 .unwrap();
1073 let runner = MockRunner::new(vec![response]);
1074 let requirements = vec![CliDomainRequirements::CdInLocalRepo];
1075 let result = url(&cli_args, &requirements, &runner, &None);
1076 match result {
1077 Err(err) => match err.downcast_ref::<error::GRError>() {
1078 Some(error::GRError::PreconditionNotMet(_)) => (),
1079 _ => panic!("Expected error::GRError::GitRemoteUrlNotFound"),
1080 },
1081 _ => panic!("Expected error"),
1082 }
1083 }
1084
1085 #[test]
1086 fn test_cli_requires_repo_args_or_cd_repo_fails_on_cd_repo() {
1087 let cli_args = CliArgs::new(0, Some("github.com/jordilin/gitar".to_string()), None, None);
1088 let requirements = vec![
1089 CliDomainRequirements::CdInLocalRepo,
1090 CliDomainRequirements::RepoArgs,
1091 ];
1092 let response = ShellResponse::builder()
1093 .body("".to_string())
1094 .build()
1095 .unwrap();
1096 let url = url(
1097 &cli_args,
1098 &requirements,
1099 &MockRunner::new(vec![response]),
1100 &None,
1101 )
1102 .unwrap();
1103 assert_eq!("github.com", url.domain());
1104 assert_eq!("jordilin/gitar", url.path());
1105 assert_eq!("jordilin_gitar", url.config_encoded_project_path());
1106 }
1107
1108 #[test]
1109 fn test_cli_requires_domain_args_or_cd_repo_fails_on_cd_repo() {
1110 let cli_args = CliArgs::new(0, None, Some("github.com".to_string()), None);
1111 let requirements = vec![
1112 CliDomainRequirements::CdInLocalRepo,
1113 CliDomainRequirements::DomainArgs,
1114 ];
1115 let response = ShellResponse::builder()
1116 .body("".to_string())
1117 .build()
1118 .unwrap();
1119 let url = url(
1120 &cli_args,
1121 &requirements,
1122 &MockRunner::new(vec![response]),
1123 &None,
1124 )
1125 .unwrap();
1126 assert_eq!("github.com", url.domain());
1127 assert_eq!("", url.path());
1128 }
1129
1130 #[test]
1131 fn test_remote_url() {
1132 let remote_url = RemoteURL::new("github.com".to_string(), "jordilin/gitar".to_string());
1133 assert_eq!("github.com", remote_url.domain());
1134 assert_eq!("jordilin/gitar", remote_url.path());
1135 }
1136
1137 #[test]
1138 fn test_get_config_encoded_project_path() {
1139 let remote_url = RemoteURL::new("github.com".to_string(), "jordilin/gitar".to_string());
1140 assert_eq!("jordilin_gitar", remote_url.config_encoded_project_path());
1141 }
1142
1143 #[test]
1144 fn test_get_config_encoded_project_path_multiple_groups() {
1145 let remote_url = RemoteURL::new(
1146 "gitlab.com".to_string(),
1147 "team/subgroup/project".to_string(),
1148 );
1149 assert_eq!(
1150 "team_subgroup_project",
1151 remote_url.config_encoded_project_path()
1152 );
1153 }
1154
1155 #[test]
1156 fn test_get_config_encoded_domain() {
1157 let remote_url = RemoteURL::new("github.com".to_string(), "jordilin/gitar".to_string());
1158 assert_eq!("github_com", remote_url.config_encoded_domain());
1159 }
1160
1161 #[test]
1162 fn test_remote_url_from_optional_target_repo() {
1163 let target_repo = Some("jordilin/gitar");
1164 let cli_args = CliArgs::default();
1165 let response = ShellResponse::builder()
1168 .body("git@github.com:hfinn/gitar.git".to_string())
1169 .build()
1170 .unwrap();
1171 let runner = MockRunner::new(vec![response]);
1172 let requirements = vec![CliDomainRequirements::CdInLocalRepo];
1173 let url = url(&cli_args, &requirements, &runner, &target_repo).unwrap();
1174 assert_eq!("github.com", url.domain());
1175 assert_eq!("jordilin/gitar", url.path());
1176 }
1177}