1use crate::api_defaults::{EXPIRE_IMMEDIATELY, RATE_LIMIT_REMAINING_THRESHOLD, REST_API_MAX_PAGES};
4use crate::api_traits::ApiOperation;
5use crate::cmds::project::{Member, MrMemberType};
6use crate::error::{self, GRError};
7use crate::remote::RemoteURL;
8use crate::Result;
9use serde::Deserialize;
10use std::sync::Arc;
11use std::{collections::HashMap, io::Read};
12
13pub trait ConfigProperties: Send + Sync {
14 fn api_token(&self) -> &str;
15 fn cache_location(&self) -> Option<&str>;
16 fn preferred_assignee_username(&self) -> Option<Member> {
17 None
18 }
19
20 fn merge_request_members(&self) -> Vec<Member> {
21 vec![]
22 }
23
24 fn merge_request_description_signature(&self) -> &str {
25 ""
26 }
27
28 fn get_cache_expiration(&self, _api_operation: &ApiOperation) -> &str {
29 "0s"
31 }
32 fn get_max_pages(&self, _api_operation: &ApiOperation) -> u32 {
33 REST_API_MAX_PAGES
34 }
35
36 fn rate_limit_remaining_threshold(&self) -> u32 {
37 RATE_LIMIT_REMAINING_THRESHOLD
38 }
39}
40
41pub struct NoConfig {
45 api_token: String,
46}
47
48impl NoConfig {
49 pub fn new<FE: Fn(&str) -> Result<String>>(domain: &str, env: FE) -> Result<Self> {
50 let api_token_res = env(domain);
51 let api_token = api_token_res.map_err(|_| {
52 GRError::PreconditionNotMet(format!(
53 "Configuration not found, so it is expected environment variable {}_API_TOKEN to be set.",
54 env_var(domain)
55 ))
56 })?;
57 Ok(NoConfig { api_token })
58 }
59}
60
61impl ConfigProperties for NoConfig {
62 fn api_token(&self) -> &str {
63 &self.api_token
64 }
65
66 fn cache_location(&self) -> Option<&str> {
67 None
68 }
69}
70
71#[derive(Deserialize, Clone, Debug)]
72struct ApiSettings {
73 #[serde(flatten)]
74 settings: HashMap<ApiOperation, String>,
75}
76
77#[derive(Deserialize, Clone, Debug)]
78struct MaxPagesApi {
79 #[serde(flatten)]
80 settings: HashMap<ApiOperation, u32>,
81}
82
83#[derive(Deserialize, Clone, Debug)]
84#[serde(untagged)]
85enum UserInfo {
86 UsernameOnly(String),
89 UsernameID {
93 username: String,
94 id: u64,
95 },
96 UsernameIDString {
97 username: String,
98 id: String,
99 },
100}
101
102#[derive(Deserialize, Clone, Debug, Default)]
103struct MergeRequestConfig {
104 preferred_assignee_username: Option<UserInfo>,
105 members: Option<Vec<UserInfo>>,
106 description_signature: Option<String>,
107}
108
109#[derive(Deserialize, Clone, Debug)]
110struct ProjectConfig {
111 merge_requests: Option<MergeRequestConfig>,
112}
113
114#[derive(Deserialize, Clone, Debug, Default)]
115pub struct DomainConfig {
116 api_token: Option<String>,
117 cache_location: Option<String>,
118 merge_requests: Option<MergeRequestConfig>,
119 rate_limit_remaining_threshold: Option<u32>,
120 cache_expirations: Option<ApiSettings>,
121 max_pages_api: Option<MaxPagesApi>,
122 #[serde(flatten)]
123 projects: HashMap<String, ProjectConfig>,
124}
125
126#[derive(Deserialize, Clone, Debug, Default)]
127pub struct ConfigFileInner {
128 #[serde(flatten)]
129 domains: HashMap<String, DomainConfig>,
130}
131
132#[derive(Clone, Debug, Default)]
133pub struct ConfigFile {
134 inner: ConfigFileInner,
135 domain_key: String,
136 project_path_key: String,
137}
138
139pub fn env_token(domain: &str) -> Result<String> {
140 let env_domain = env_var(domain);
141 Ok(std::env::var(format!("{env_domain}_API_TOKEN"))?)
142}
143
144fn env_var(domain: &str) -> String {
145 let domain_fields = domain.split('.').collect::<Vec<&str>>();
146 let env_domain = if domain_fields.len() == 1 {
147 domain
149 } else {
150 &domain_fields[0..domain_fields.len() - 1].join("_")
151 };
152 env_domain.to_ascii_uppercase()
153}
154
155impl ConfigFile {
156 pub fn new<T: Read, FE: Fn(&str) -> Result<String>>(
166 readers: Vec<T>,
167 url: &RemoteURL,
168 env: FE,
169 ) -> Result<ConfigFile> {
170 let mut config_data = String::new();
171 for mut reader in readers.into_iter() {
172 reader.read_to_string(&mut config_data)?;
173 }
174 let mut config: ConfigFileInner = toml::from_str(&config_data)?;
175 let project_path_key = url.config_encoded_project_path();
176 let domain = url.domain();
177 let domain_key = url.config_encoded_domain();
184 if let Some(domain_config) = config.domains.get_mut(domain_key) {
185 if domain_config.api_token.is_none() {
186 domain_config.api_token = Some(env(domain).map_err(|_| {
187 GRError::PreconditionNotMet(format!(
188 "No api_token found for domain {domain} in config or environment variable"
189 ))
190 })?);
191 }
192 Ok(ConfigFile {
193 inner: config,
194 domain_key: domain_key.to_string(),
195 project_path_key: project_path_key.to_string(),
196 })
197 } else {
198 Err(error::gen(format!(
199 "No config data found for domain {domain}"
200 )))
201 }
202 }
203
204 fn get_members_from_config(&self) -> Vec<Member> {
205 if let Some(domain_config) = &self.inner.domains.get(&self.domain_key) {
206 let members = domain_config
207 .projects
208 .get(&self.project_path_key)
209 .and_then(|project_config| {
210 project_config
211 .merge_requests
212 .as_ref()
213 .and_then(|merge_request_config| self.get_members(merge_request_config))
214 })
215 .or_else(|| {
216 domain_config
217 .merge_requests
218 .as_ref()
219 .and_then(|merge_request_config| self.get_members(merge_request_config))
220 });
221 members.unwrap_or_default()
222 } else {
223 vec![]
224 }
225 }
226
227 fn get_members(&self, merge_request_config: &MergeRequestConfig) -> Option<Vec<Member>> {
228 merge_request_config.members.as_ref().map(|users| {
229 users
230 .iter()
231 .map(|user_info| match user_info {
232 UserInfo::UsernameOnly(username) => Member::builder()
233 .username(username.clone())
234 .mr_member_type(MrMemberType::Filled)
235 .build()
236 .unwrap(),
237 UserInfo::UsernameID { username, id } => Member::builder()
238 .username(username.clone())
239 .id(*id as i64)
240 .mr_member_type(MrMemberType::Filled)
241 .build()
242 .unwrap(),
243 UserInfo::UsernameIDString { username, id } => Member::builder()
244 .username(username.clone())
245 .id(id.parse::<i64>().expect("User ID must be a number"))
246 .mr_member_type(MrMemberType::Filled)
247 .build()
248 .unwrap(),
249 })
250 .collect()
251 })
252 }
253}
254
255impl ConfigProperties for ConfigFile {
256 fn api_token(&self) -> &str {
257 if let Some(domain) = self.inner.domains.get(&self.domain_key) {
258 domain.api_token.as_deref().unwrap_or_default()
259 } else {
260 ""
261 }
262 }
263
264 fn cache_location(&self) -> Option<&str> {
265 if let Some(domain) = self.inner.domains.get(&self.domain_key) {
266 domain.cache_location.as_deref()
267 } else {
268 None
269 }
270 }
271
272 fn preferred_assignee_username(&self) -> Option<Member> {
273 if let Some(domain_config) = &self.inner.domains.get(&self.domain_key) {
274 domain_config
275 .projects
276 .get(&self.project_path_key)
277 .and_then(|project_config| {
278 project_config
279 .merge_requests
280 .as_ref()
281 .and_then(|merge_request_config| {
282 merge_request_config
283 .preferred_assignee_username
284 .as_ref()
285 .map(|user_info| match user_info {
286 UserInfo::UsernameOnly(username) => Member::builder()
287 .username(username.clone())
288 .mr_member_type(MrMemberType::Filled)
289 .build()
290 .unwrap(),
291 UserInfo::UsernameID { username, id } => Member::builder()
292 .username(username.clone())
293 .mr_member_type(MrMemberType::Filled)
294 .id(*id as i64)
295 .build()
296 .unwrap(),
297 UserInfo::UsernameIDString { username, id } => {
298 Member::builder()
301 .username(username.clone())
302 .mr_member_type(MrMemberType::Filled)
303 .id(id
304 .parse::<i64>()
305 .expect("User ID must be a number"))
306 .build()
307 .unwrap()
308 }
309 })
310 })
311 })
312 .or_else(|| {
313 domain_config
314 .merge_requests
315 .as_ref()
316 .and_then(|merge_request_config| {
317 merge_request_config
318 .preferred_assignee_username
319 .as_ref()
320 .map(|user_info| match user_info {
321 UserInfo::UsernameOnly(username) => Member::builder()
322 .username(username.clone())
323 .mr_member_type(MrMemberType::Filled)
324 .build()
325 .unwrap(),
326 UserInfo::UsernameID { username, id } => Member::builder()
327 .username(username.clone())
328 .mr_member_type(MrMemberType::Filled)
329 .id(*id as i64)
330 .build()
331 .unwrap(),
332 UserInfo::UsernameIDString { username, id } => {
333 Member::builder()
334 .username(username.clone())
335 .mr_member_type(MrMemberType::Filled)
336 .id(id
337 .parse::<i64>()
338 .expect("User ID must be a number"))
339 .build()
340 .unwrap()
341 }
342 })
343 })
344 })
345 } else {
346 None
347 }
348 }
349
350 fn merge_request_members(&self) -> Vec<Member> {
351 self.get_members_from_config()
352 }
353
354 fn merge_request_description_signature(&self) -> &str {
355 if let Some(domain_config) = &self.inner.domains.get(&self.domain_key) {
356 domain_config
357 .projects
358 .get(&self.project_path_key)
359 .and_then(|project_config| {
360 project_config
361 .merge_requests
362 .as_ref()
363 .and_then(|merge_request_config| {
364 merge_request_config.description_signature.as_deref()
365 })
366 })
367 .unwrap_or_else(|| {
368 domain_config
369 .merge_requests
370 .as_ref()
371 .and_then(|merge_request_config| {
372 merge_request_config.description_signature.as_deref()
373 })
374 .unwrap_or_default()
375 })
376 } else {
377 ""
378 }
379 }
380
381 fn get_cache_expiration(&self, api_operation: &ApiOperation) -> &str {
382 self.inner
383 .domains
384 .get(&self.domain_key)
385 .and_then(|domain_config| {
386 domain_config
387 .cache_expirations
388 .as_ref()
389 .and_then(|cache_expirations| cache_expirations.settings.get(api_operation))
390 })
391 .map(|s| s.as_str())
392 .unwrap_or_else(|| EXPIRE_IMMEDIATELY)
393 }
394
395 fn get_max_pages(&self, api_operation: &ApiOperation) -> u32 {
396 self.inner
397 .domains
398 .get(&self.domain_key)
399 .and_then(|domain_config| {
400 domain_config
401 .max_pages_api
402 .as_ref()
403 .and_then(|max_pages| max_pages.settings.get(api_operation))
404 })
405 .copied()
406 .unwrap_or(REST_API_MAX_PAGES)
407 }
408
409 fn rate_limit_remaining_threshold(&self) -> u32 {
410 self.inner
411 .domains
412 .get(&self.domain_key)
413 .and_then(|domain_config| domain_config.rate_limit_remaining_threshold)
414 .unwrap_or(RATE_LIMIT_REMAINING_THRESHOLD)
415 }
416}
417
418impl ConfigProperties for Arc<ConfigFile> {
419 fn api_token(&self) -> &str {
420 self.as_ref().api_token()
421 }
422
423 fn cache_location(&self) -> Option<&str> {
424 self.as_ref().cache_location()
425 }
426
427 fn preferred_assignee_username(&self) -> Option<Member> {
428 self.as_ref().preferred_assignee_username()
429 }
430
431 fn merge_request_description_signature(&self) -> &str {
432 self.as_ref().merge_request_description_signature()
433 }
434
435 fn get_cache_expiration(&self, api_operation: &ApiOperation) -> &str {
436 self.as_ref().get_cache_expiration(api_operation)
437 }
438
439 fn get_max_pages(&self, api_operation: &ApiOperation) -> u32 {
440 self.as_ref().get_max_pages(api_operation)
441 }
442
443 fn rate_limit_remaining_threshold(&self) -> u32 {
444 self.as_ref().rate_limit_remaining_threshold()
445 }
446
447 fn merge_request_members(&self) -> Vec<Member> {
448 self.as_ref().merge_request_members()
449 }
450}
451
452#[cfg(test)]
453mod test {
454 use crate::cmds::project::MrMemberType;
455
456 use super::*;
457
458 fn no_env(_: &str) -> Result<String> {
459 Err(error::gen("No env var"))
460 }
461
462 #[test]
463 fn test_config_ok() {
464 let config_data = r#"
465 [gitlab_com]
466 api_token = '1234'
467 cache_location = "/home/user/.config/mr_cache"
468 rate_limit_remaining_threshold=15
469
470 [gitlab_com.merge_requests]
471 preferred_assignee_username = "jordilin"
472 description_signature = "- devops team :-)"
473 members = [
474 { username = 'jdoe', id = 1231 },
475 { username = 'jane', id = 1232 }
476 ]
477
478 [gitlab_com.max_pages_api]
479 merge_request = 2
480 pipeline = 3
481 project = 4
482 container_registry = 5
483 single_page = 6
484 release = 7
485 gist = 8
486 repository_tag = 9
487
488 [gitlab_com.cache_expirations]
489 merge_request = "30m"
490 pipeline = "0s"
491 project = "90d"
492 container_registry = "0s"
493 single_page = "0s"
494 release = "4h"
495 gist = "1w"
496 repository_tag = "0s"
497 "#;
498 let domain = "gitlab.com";
499 let reader = vec![std::io::Cursor::new(config_data)];
500 let project_path = "/jordilin/gitar";
501 let url = RemoteURL::new(domain.to_string(), project_path.to_string());
502 let config = Arc::new(ConfigFile::new(reader, &url, no_env).unwrap());
503 assert_eq!("1234", config.api_token());
504 assert_eq!(
505 "/home/user/.config/mr_cache",
506 config.cache_location().unwrap()
507 );
508 assert_eq!(15, config.rate_limit_remaining_threshold());
509 assert_eq!(
510 "- devops team :-)",
511 config.merge_request_description_signature()
512 );
513 let preferred_assignee_user = config.preferred_assignee_username().unwrap();
514 assert_eq!("jordilin", preferred_assignee_user.username);
515 assert_eq!(MrMemberType::Filled, preferred_assignee_user.mr_member_type);
516 assert_eq!(2, config.get_max_pages(&ApiOperation::MergeRequest));
517 assert_eq!(3, config.get_max_pages(&ApiOperation::Pipeline));
518 assert_eq!(4, config.get_max_pages(&ApiOperation::Project));
519 assert_eq!(5, config.get_max_pages(&ApiOperation::ContainerRegistry));
520 assert_eq!(6, config.get_max_pages(&ApiOperation::SinglePage));
521 assert_eq!(7, config.get_max_pages(&ApiOperation::Release));
522 assert_eq!(8, config.get_max_pages(&ApiOperation::Gist));
523 assert_eq!(9, config.get_max_pages(&ApiOperation::RepositoryTag));
524
525 assert_eq!(
526 "30m",
527 config.get_cache_expiration(&ApiOperation::MergeRequest)
528 );
529 assert_eq!("0s", config.get_cache_expiration(&ApiOperation::Pipeline));
530 assert_eq!("90d", config.get_cache_expiration(&ApiOperation::Project));
531 assert_eq!(
532 "0s",
533 config.get_cache_expiration(&ApiOperation::ContainerRegistry)
534 );
535 assert_eq!("0s", config.get_cache_expiration(&ApiOperation::SinglePage));
536 assert_eq!("4h", config.get_cache_expiration(&ApiOperation::Release));
537 assert_eq!("1w", config.get_cache_expiration(&ApiOperation::Gist));
538 assert_eq!(
539 "0s",
540 config.get_cache_expiration(&ApiOperation::RepositoryTag)
541 );
542 let members = config.merge_request_members();
543 assert_eq!(2, members.len());
544 assert_eq!("jdoe", members[0].username);
545 assert_eq!(1231, members[0].id);
546 assert_eq!(MrMemberType::Filled, members[0].mr_member_type);
547 assert_eq!("jane", members[1].username);
548 assert_eq!(1232, members[1].id);
549 }
550
551 #[test]
552 fn test_config_defaults() {
553 let config_data = r#"
554 [github_com]
555 api_token = '1234'
556 "#;
557 let domain = "github.com";
558 let reader = vec![std::io::Cursor::new(config_data)];
559 let project_path = "/jordilin/gitar";
560 let url = RemoteURL::new(domain.to_string(), project_path.to_string());
561 let config = Arc::new(ConfigFile::new(reader, &url, no_env).unwrap());
562 for api_operation in ApiOperation::iter() {
563 assert_eq!(REST_API_MAX_PAGES, config.get_max_pages(&api_operation));
564 assert_eq!(
565 EXPIRE_IMMEDIATELY,
566 config.get_cache_expiration(&api_operation)
567 );
568 }
569 assert_eq!(
570 RATE_LIMIT_REMAINING_THRESHOLD,
571 config.rate_limit_remaining_threshold()
572 );
573 assert_eq!(None, config.cache_location());
574 assert_eq!(None, config.preferred_assignee_username());
575 assert_eq!("", config.merge_request_description_signature());
576 }
577
578 #[test]
579 fn test_config_with_overridden_project_specific_settings() {
580 let config_data = r#"
581 [gitlab_com]
582 api_token = '1234'
583 cache_location = "/home/user/.config/mr_cache"
584 rate_limit_remaining_threshold=15
585
586 [gitlab_com.merge_requests]
587 preferred_assignee_username = "jordilin"
588 description_signature = "- devops team :-)"
589 members = [
590 { username = 'jdoe', id = 1231 }
591 ]
592
593 # Project specific settings for /datateam/projecta
594 [gitlab_com.datateam_projecta.merge_requests]
595 preferred_assignee_username = 'jdoe'
596 description_signature = '- data team projecta :-)'
597 members = [ { username = 'jane', id = 1234 } ]"#;
598
599 let domain = "gitlab.com";
600 let reader = vec![std::io::Cursor::new(config_data)];
601 let project_path = "datateam/projecta";
602 let url = RemoteURL::new(domain.to_string(), project_path.to_string());
603 let config = Arc::new(ConfigFile::new(reader, &url, no_env).unwrap());
604 let preferred_assignee_user = config.preferred_assignee_username().unwrap();
605 assert_eq!("jdoe", preferred_assignee_user.username);
606 assert_eq!(
607 "- data team projecta :-)",
608 config.merge_request_description_signature()
609 );
610 let members = config.merge_request_members();
611 assert_eq!(1, members.len());
612 assert_eq!("jane", members[0].username);
613 assert_eq!(1234, members[0].id);
614 }
615
616 #[test]
617 fn test_config_with_overridden_project_specific_settings_multiple_readers() {
618 let config_data = r#"
619 [gitlab_com]
620 api_token = '1234'
621 cache_location = "/home/user/.config/mr_cache"
622 rate_limit_remaining_threshold=15
623
624 [gitlab_com.merge_requests]
625 preferred_assignee_username = "jordilin"
626 description_signature = "- devops team :-)"
627 members = [
628 { username = 'jdoe', id = 1231 }
629 ]"#;
630
631 let config_data_2 = r#"
632 # Project specific settings for /datateam/projecta
633 [gitlab_com.datateam_projecta.merge_requests]
634 preferred_assignee_username = 'jdoe'
635 description_signature = '- data team projecta :-)'
636 members = [ { username = 'jane', id = 1234 } ]"#;
637
638 let domain = "gitlab.com";
639 let reader = vec![
640 std::io::Cursor::new(config_data),
641 std::io::Cursor::new(config_data_2),
642 ];
643 let project_path = "datateam/projecta";
644 let url = RemoteURL::new(domain.to_string(), project_path.to_string());
645 let config = Arc::new(ConfigFile::new(reader, &url, no_env).unwrap());
646 let preferred_assignee_user = config.preferred_assignee_username().unwrap();
647 assert_eq!("jdoe", preferred_assignee_user.username);
648 assert_eq!(
649 "- data team projecta :-)",
650 config.merge_request_description_signature()
651 );
652 let members = config.merge_request_members();
653 assert_eq!(1, members.len());
654 assert_eq!("jane", members[0].username);
655 assert_eq!(1234, members[0].id);
656 }
657
658 #[test]
659 fn test_config_multiple_readers_same_headers_is_error() {
660 let config_data = r#"
661 [gitlab_com]
662 api_token = '1234'
663 cache_location = "/home/user/.config/mr_cache"
664 rate_limit_remaining_threshold=15
665
666 [gitlab_com.merge_requests]
667 preferred_assignee_username = "jordilin"
668 description_signature = "- devops team :-)"
669 members = [
670 { username = 'jdoe', id = 1231 }
671 ]"#;
672
673 let config_data_2 = r#"
674 [gitlab_com]
675 api_token = '1234'
676 cache_location = "/home/user/.config/mr_cache"
677 rate_limit_remaining_threshold=15"#;
678
679 let domain = "gitlab.com";
680 let reader = vec![
681 std::io::Cursor::new(config_data),
682 std::io::Cursor::new(config_data_2),
683 ];
684 let project_path = "datateam/projecta";
685 let url = RemoteURL::new(domain.to_string(), project_path.to_string());
686 assert!(ConfigFile::new(reader, &url, no_env).is_err());
687 }
688
689 #[test]
690 fn test_config_preferred_assignee_username_with_id() {
691 let config_data = r#"
692 [gitlab_com]
693 api_token = '1234'
694 cache_location = "/home/user/.config/mr_cache"
695 rate_limit_remaining_threshold=15
696
697 [gitlab_com.merge_requests]
698 preferred_assignee_username = { username = 'jdoe', id = 1231 }
699
700 # Project specific settings for /datateam/projecta
701 [gitlab_com.datateam_projecta.merge_requests]
702 preferred_assignee_username = { username = 'jordilin', id = 1234 }
703 "#;
704
705 let domain = "gitlab.com";
706 let reader = vec![std::io::Cursor::new(config_data)];
707 let project_path = "datateam_projecta";
708 let url = RemoteURL::new(domain.to_string(), project_path.to_string());
709 let config = Arc::new(ConfigFile::new(reader, &url, no_env).unwrap());
710 let preferred_assignee_user = config.preferred_assignee_username().unwrap();
711 assert_eq!("jordilin", preferred_assignee_user.username);
712 }
713
714 #[test]
715 fn test_no_api_token_is_err() {
716 let config_data = r#"
717 [gitlab_com]
718 api_token_typo=1234"#;
719 let domain = "gitlab.com";
720 let reader = vec![std::io::Cursor::new(config_data)];
721 let project_path = "/jordilin/gitar";
722 let url = RemoteURL::new(domain.to_string(), project_path.to_string());
723 assert!(ConfigFile::new(reader, &url, no_env).is_err());
724 }
725
726 #[test]
727 fn test_config_no_data() {
728 let config_data = "";
729 let domain = "gitlab.com";
730 let reader = vec![std::io::Cursor::new(config_data)];
731 let project_path = "/jordilin/gitar";
732 let url = RemoteURL::new(domain.to_string(), project_path.to_string());
733 assert!(ConfigFile::new(reader, &url, no_env).is_err());
734 }
735
736 fn env(_: &str) -> Result<String> {
737 Ok("1234".to_string())
738 }
739
740 #[test]
741 fn test_use_gitlab_com_api_token_envvar() {
742 let config_data = r#"
743 [gitlab_com]
744 "#;
745 let domain = "gitlab.com";
746 let reader = vec![std::io::Cursor::new(config_data)];
747 let project_path = "/jordilin/gitar";
748 let url = RemoteURL::new(domain.to_string(), project_path.to_string());
749 let config = Arc::new(ConfigFile::new(reader, &url, env).unwrap());
750 assert_eq!("1234", config.api_token());
751 }
752
753 #[test]
754 fn test_use_sub_domain_gitlab_token_env_var() {
755 let config_data = r#"
756 [gitlab_company_com]
757 "#;
758 let domain = "gitlab.company.com";
759 let reader = vec![std::io::Cursor::new(config_data)];
760 let project_path = "/jordilin/gitar";
761 let url = RemoteURL::new(domain.to_string(), project_path.to_string());
762 let config = Arc::new(ConfigFile::new(reader, &url, env).unwrap());
763 assert_eq!("1234", config.api_token());
764 }
765
766 #[test]
767 fn test_domain_without_top_level_domain_token_envvar() {
768 let config_data = r#"
769 [gitlabweb]
770 "#;
771 let domain = "gitlabweb";
772 let reader = vec![std::io::Cursor::new(config_data)];
773 let project_path = "/jordilin/gitar";
774 let url = RemoteURL::new(domain.to_string(), project_path.to_string());
775 let config = Arc::new(ConfigFile::new(reader, &url, env).unwrap());
776 assert_eq!("1234", config.api_token());
777 }
778
779 #[test]
780 fn test_no_config_requires_auth_env_token_and_no_cache() {
781 let domain = "gitlabwebnoconfig";
782 let config = NoConfig::new(domain, env).unwrap();
783 assert_eq!("1234", config.api_token());
784 assert_eq!(None, config.cache_location());
785 }
786
787 #[test]
788 fn test_no_config_no_env_token_is_error() {
789 let domain = "gitlabwebnoenv.com";
790 let config_res = NoConfig::new(domain, no_env);
791 match config_res {
792 Err(err) => match err.downcast_ref::<error::GRError>() {
793 Some(error::GRError::PreconditionNotMet(val)) => {
794 assert_eq!("Configuration not found, so it is expected environment variable GITLABWEBNOENV_API_TOKEN to be set.", val)
795 }
796 _ => panic!("Expected error::GRError::PreconditionNotMet"),
797 },
798 _ => panic!("Expected error"),
799 }
800 }
801
802 #[test]
803 fn test_default_config_file() {
804 let config = ConfigFile::default();
806 assert_eq!("", config.api_token());
807 assert_eq!(None, config.cache_location());
808 assert_eq!(
809 RATE_LIMIT_REMAINING_THRESHOLD,
810 config.rate_limit_remaining_threshold()
811 );
812 assert_eq!(None, config.preferred_assignee_username());
813 assert_eq!("", config.merge_request_description_signature());
814 }
815
816 #[test]
817 fn test_config_with_member_ids_strings() {
818 let config_data = r#"
819 [gitlab_com]
820 api_token = '1234'
821 cache_location = "/home/user/.config/mr_cache"
822 rate_limit_remaining_threshold=15
823
824 [gitlab_com.merge_requests]
825 preferred_assignee_username = { username = "jordilin", id = "1234" }
826 description_signature = "- devops team :-)"
827 members = [
828 { username = 'jdoe', id = '1231' }
829 ]"#;
830
831 let domain = "gitlab.com";
832 let reader = vec![std::io::Cursor::new(config_data)];
833 let project_path = "datateam/projecta";
834 let url = RemoteURL::new(domain.to_string(), project_path.to_string());
835 let config = Arc::new(ConfigFile::new(reader, &url, no_env).unwrap());
836 let preferred_assignee_user = config.preferred_assignee_username().unwrap();
837 assert_eq!("jordilin", preferred_assignee_user.username);
838 assert_eq!(
839 "- devops team :-)",
840 config.merge_request_description_signature()
841 );
842 let members = config.merge_request_members();
843 assert_eq!(1, members.len());
844 assert_eq!("jdoe", members[0].username);
845 assert_eq!(1231, members[0].id);
846 }
847
848 #[test]
849 fn test_config_with_overridden_project_specific_settings_member_id_strings() {
850 let config_data = r#"
851 [gitlab_com]
852 api_token = '1234'
853 cache_location = "/home/user/.config/mr_cache"
854 rate_limit_remaining_threshold=15
855
856 [gitlab_com.merge_requests]
857 preferred_assignee_username = "jordilin"
858 description_signature = "- devops team :-)"
859 members = [
860 { username = 'jdoe', id = "1234" }
861 ]
862
863 # Project specific settings for /datateam/projecta
864 [gitlab_com.datateam_projecta.merge_requests]
865 preferred_assignee_username = { username = 'jdoe', id = '1234' }
866 description_signature = '- data team projecta :-)'
867 members = [ { username = 'jane', id = "1235" } ]"#;
868
869 let domain = "gitlab.com";
870 let reader = vec![std::io::Cursor::new(config_data)];
871 let project_path = "datateam/projecta";
872 let url = RemoteURL::new(domain.to_string(), project_path.to_string());
873 let config = Arc::new(ConfigFile::new(reader, &url, no_env).unwrap());
874 let preferred_assignee_user = config.preferred_assignee_username().unwrap();
875 assert_eq!("jdoe", preferred_assignee_user.username);
876 assert_eq!(
877 "- data team projecta :-)",
878 config.merge_request_description_signature()
879 );
880 let members = config.merge_request_members();
881 assert_eq!(1, members.len());
882 assert_eq!("jane", members[0].username);
883 assert_eq!(1235, members[0].id);
884 }
885}