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