1use crate::config::advanced::FormattingStyle;
5use crate::config::advanced::auth::{AuthConfig, AuthorizationRestrictions};
6use crate::config::data_server::{DataServerConfig, DataServerEnabled};
7use crate::config::location::{Location, Locations};
8use crate::config::parser::from_path;
9use crate::config::service_info::{PackageInfo, ServiceInfo};
10use crate::config::ticket_server::TicketServerConfig;
11use crate::error::Error::{ArgParseError, ParseError, TracingError};
12use crate::error::Result;
13use crate::http::KeyPairScheme;
14use crate::storage::Backend;
15use clap::{Args as ClapArgs, Command, FromArgMatches, Parser};
16use http::header::AUTHORIZATION;
17use http::uri::Authority;
18use schemars::schema_for;
19use serde::de::Error;
20use serde::ser::SerializeSeq;
21use serde::{Deserialize, Deserializer, Serialize, Serializer};
22use std::collections::HashSet;
23use std::fmt::{Debug, Display};
24use std::io;
25use std::path::{Path, PathBuf};
26use std::str::FromStr;
27use tracing::subscriber::set_global_default;
28use tracing_subscriber::fmt::{format, layer};
29use tracing_subscriber::layer::SubscriberExt;
30use tracing_subscriber::{EnvFilter, Registry};
31
32pub mod advanced;
33pub mod data_server;
34pub mod location;
35pub mod parser;
36pub mod service_info;
37pub mod ticket_server;
38
39pub const USAGE: &str = "To configure htsget-rs use a config file or environment variables. \
41See the documentation of the htsget-config crate for more information.";
42
43#[derive(Parser, Debug)]
45#[command(author, version, about, long_about = USAGE)]
46struct Args {
47 #[arg(
48 short,
49 long,
50 env = "HTSGET_CONFIG",
51 help = "Set the location of the config file"
52 )]
53 config: Option<PathBuf>,
54 #[arg(short, long, exclusive = true, help = "Print a default config file")]
55 print_default_config: bool,
56 #[arg(
57 short = 's',
58 long,
59 exclusive = true,
60 help = "Print the response JSON schema used in the htsget auth process"
61 )]
62 print_response_schema: bool,
63}
64
65#[derive(Serialize, Deserialize, Debug, Clone)]
67#[serde(default, deny_unknown_fields)]
68pub struct Config {
69 ticket_server: TicketServerConfig,
70 data_server: DataServerEnabled,
71 service_info: ServiceInfo,
72 #[serde(alias = "location")]
73 locations: Locations,
74 formatting_style: FormattingStyle,
75 #[serde(skip_serializing)]
76 auth: Option<AuthConfig>,
77 #[serde(skip)]
78 package_info: PackageInfo,
79}
80
81impl Config {
82 pub fn new(
84 formatting_style: FormattingStyle,
85 ticket_server: TicketServerConfig,
86 data_server: DataServerEnabled,
87 service_info: ServiceInfo,
88 locations: Locations,
89 auth: Option<AuthConfig>,
90 package_info: PackageInfo,
91 ) -> Self {
92 Self {
93 formatting_style,
94 ticket_server,
95 data_server,
96 service_info,
97 locations,
98 auth,
99 package_info,
100 }
101 }
102
103 pub fn formatting_style(&self) -> FormattingStyle {
105 self.formatting_style
106 }
107
108 pub fn ticket_server(&self) -> &TicketServerConfig {
110 &self.ticket_server
111 }
112
113 pub fn ticket_server_mut(&mut self) -> &mut TicketServerConfig {
115 &mut self.ticket_server
116 }
117
118 pub fn data_server(&self) -> &DataServerEnabled {
120 &self.data_server
121 }
122
123 pub fn data_server_mut(&mut self) -> Option<&mut DataServerConfig> {
125 match &mut self.data_server {
126 DataServerEnabled::None(_) => None,
127 DataServerEnabled::Some(data_server) => Some(data_server),
128 }
129 }
130
131 pub fn package_info(&self) -> &PackageInfo {
133 &self.package_info
134 }
135
136 pub fn service_info(&self) -> &ServiceInfo {
138 &self.service_info
139 }
140
141 pub fn set_package_info(&mut self, package_info: PackageInfo) -> Result<()> {
143 self.package_info = package_info;
144
145 self
146 .service_info
147 .set_from_package_info(&self.package_info)?;
148
149 let data_server_auth = match &mut self.data_server {
150 DataServerEnabled::Some(config) => config.auth.as_mut(),
151 DataServerEnabled::None(_) => None,
152 };
153 for auth in [
154 self.auth.as_mut(),
155 self.ticket_server.auth.as_mut(),
156 data_server_auth,
157 ]
158 .into_iter()
159 .flatten()
160 {
161 auth.set_from_package_info(&self.package_info)?;
162 }
163
164 self.locations.set_from_package_info(&self.package_info)?;
165
166 Ok(())
167 }
168
169 pub fn service_info_mut(&mut self) -> &mut ServiceInfo {
171 &mut self.service_info
172 }
173
174 pub fn locations(&self) -> &[Location] {
176 self.locations.as_slice()
177 }
178
179 pub fn into_locations(self) -> Locations {
180 self.locations
181 }
182
183 pub fn parse_args_with_command(augment_args: Command) -> Result<Option<PathBuf>> {
186 let args = Args::from_arg_matches(&Args::augment_args(augment_args).get_matches())
187 .map_err(|err| ArgParseError(err.to_string()))?;
188
189 if args.config.as_ref().is_some_and(|path| !path.exists()) {
190 return Err(ParseError("config file not found".to_string()));
191 }
192
193 Ok(Self::parse_with_args(args))
194 }
195
196 pub fn parse_args() -> Option<PathBuf> {
198 Self::parse_with_args(Args::parse())
199 }
200
201 fn parse_with_args(args: Args) -> Option<PathBuf> {
202 if args.print_default_config {
203 println!(
204 "{}",
205 toml::ser::to_string_pretty(&Config::default()).unwrap()
206 );
207 None
208 } else if args.print_response_schema {
209 println!(
210 "{}",
211 serde_json::to_string_pretty(&schema_for!(AuthorizationRestrictions)).unwrap()
212 );
213 None
214 } else {
215 Some(args.config.unwrap_or_else(|| "".into()))
216 }
217 }
218
219 pub fn from_path(path: &Path) -> io::Result<Self> {
221 let mut config: Self = from_path(path)?;
222
223 if let DataServerEnabled::Some(ref mut data_server_config) = config.data_server
225 && data_server_config.auth().is_none()
226 {
227 data_server_config.set_auth(config.auth.clone());
228 }
229 if config.ticket_server().auth().is_none() {
230 config.ticket_server.set_auth(config.auth.clone());
231 }
232
233 Ok(config.validate_file_locations()?)
234 }
235
236 pub fn setup_tracing(&self) -> Result<()> {
238 let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
239
240 let subscriber = Registry::default().with(env_filter);
241
242 match self.formatting_style() {
243 FormattingStyle::Full => set_global_default(subscriber.with(layer())),
244 FormattingStyle::Compact => {
245 set_global_default(subscriber.with(layer().event_format(format().compact())))
246 }
247 FormattingStyle::Pretty => {
248 set_global_default(subscriber.with(layer().event_format(format().pretty())))
249 }
250 FormattingStyle::Json => {
251 set_global_default(subscriber.with(layer().event_format(format().json())))
252 }
253 }
254 .map_err(|err| TracingError(err.to_string()))?;
255
256 Ok(())
257 }
258
259 pub fn validate_file_locations(mut self) -> Result<Self> {
261 if !self
262 .locations()
263 .iter()
264 .any(|location| location.backend().as_file().is_ok())
265 {
266 return Ok(self);
267 }
268
269 let DataServerEnabled::Some(ref mut config) = self.data_server else {
270 return Err(ParseError(
271 "must enable data server if using file locations".to_string(),
272 ));
273 };
274
275 let mut possible_paths: HashSet<_> =
276 HashSet::from_iter(self.locations.as_slice().iter().map(|location| {
277 location
278 .backend()
279 .as_file()
280 .ok()
281 .map(|file| file.local_path())
282 }));
283 possible_paths.remove(&None);
284
285 if possible_paths.len() > 1 {
286 return Err(ParseError(
287 "cannot have multiple file paths for file storage".to_string(),
288 ));
289 }
290 let local_path = possible_paths
291 .into_iter()
292 .next()
293 .flatten()
294 .ok_or_else(|| ParseError("failed to find local path from locations".to_string()))?
295 .to_string();
296
297 if config
298 .local_path()
299 .is_some_and(|path| path.to_string_lossy() != local_path)
300 {
301 return Err(ParseError(
302 "the data server local path and file storage directories must be the same".to_string(),
303 ));
304 }
305
306 config.set_local_path(Some(PathBuf::from(local_path)));
307
308 let scheme = config.tls().get_scheme();
309 let authority =
310 Authority::from_str(&config.addr().to_string()).map_err(|err| ParseError(err.to_string()))?;
311 let ticket_origin = config.ticket_origin();
312
313 self
314 .locations
315 .as_mut_slice()
316 .iter_mut()
317 .map(|location| {
318 match location.backend_mut() {
321 Backend::File(file) => {
322 file.set_ticket_origin(ticket_origin.clone());
323 if file.is_defaulted {
324 file.set_scheme(scheme);
325 file.set_authority(authority.clone());
326 }
327 }
328 #[cfg(feature = "aws")]
329 Backend::S3(_) => {}
330 #[cfg(feature = "url")]
331 Backend::Url(_) => {}
332 }
333
334 if self
336 .data_server
337 .as_data_server_config()
338 .is_ok_and(|config| config.auth().is_some())
339 {
340 location
341 .backend_mut()
342 .add_ticket_header(AUTHORIZATION.to_string());
343 }
344
345 Ok(())
346 })
347 .collect::<Result<Vec<()>>>()?;
348
349 Ok(self)
350 }
351}
352
353impl Default for Config {
354 fn default() -> Self {
355 Self {
356 formatting_style: FormattingStyle::Full,
357 ticket_server: Default::default(),
358 data_server: DataServerEnabled::Some(Default::default()),
359 service_info: Default::default(),
360 locations: Default::default(),
361 auth: Default::default(),
362 package_info: Default::default(),
363 }
364 }
365}
366
367pub(crate) fn serialize_array_display<S, T>(
368 names: &[T],
369 serializer: S,
370) -> std::result::Result<S::Ok, S::Error>
371where
372 T: Display,
373 S: Serializer,
374{
375 let mut sequence = serializer.serialize_seq(Some(names.len()))?;
376 for element in names.iter().map(|name| format!("{name}")) {
377 sequence.serialize_element(&element)?;
378 }
379 sequence.end()
380}
381
382pub(crate) fn deserialize_vec_from_str<'de, D, T>(
383 deserializer: D,
384) -> std::result::Result<Vec<T>, D::Error>
385where
386 T: FromStr,
387 T::Err: Display,
388 D: Deserializer<'de>,
389{
390 let names: Vec<String> = Deserialize::deserialize(deserializer)?;
391 names
392 .into_iter()
393 .map(|name| T::from_str(&name).map_err(Error::custom))
394 .collect()
395}
396
397#[cfg(test)]
398pub(crate) mod tests {
399 use std::fmt::Display;
400
401 use super::*;
402 use crate::config::location::SimpleLocation;
403 use crate::config::parser::from_str;
404 use crate::http::tests::with_test_certificates;
405 use crate::storage::Backend;
406 use crate::types::Scheme;
407 use figment::Jail;
408 #[cfg(feature = "url")]
409 use http::Uri;
410 use http::uri::Authority;
411 use serde::de::DeserializeOwned;
412 use serde_json::json;
413
414 fn test_config<K, V, F>(contents: Option<&str>, env_variables: Vec<(K, V)>, test_fn: F)
415 where
416 K: AsRef<str>,
417 V: Display,
418 F: Fn(Config),
419 {
420 #[allow(clippy::result_large_err)]
421 Jail::expect_with(|jail| {
422 let file = "test.toml";
423
424 if let Some(contents) = contents {
425 jail.create_file(file, contents)?;
426 }
427
428 for (key, value) in env_variables {
429 jail.set_env(key, value);
430 }
431
432 let path = Path::new(file);
433 test_fn(Config::from_path(path).map_err(|err| err.to_string())?);
434
435 test_fn(
436 from_path::<Config>(path)
437 .map_err(|err| err.to_string())?
438 .validate_file_locations()
439 .map_err(|err| err.to_string())?,
440 );
441 test_fn(
442 from_str::<Config>(contents.unwrap_or(""))
443 .map_err(|err| err.to_string())?
444 .validate_file_locations()
445 .map_err(|err| err.to_string())?,
446 );
447
448 Ok(())
449 });
450 }
451
452 pub(crate) fn test_config_from_env<K, V, F>(env_variables: Vec<(K, V)>, test_fn: F)
453 where
454 K: AsRef<str>,
455 V: Display,
456 F: Fn(Config),
457 {
458 test_config(None, env_variables, test_fn);
459 }
460
461 pub(crate) fn test_config_from_file<F>(contents: &str, test_fn: F)
462 where
463 F: Fn(Config),
464 {
465 test_config(Some(contents), Vec::<(&str, &str)>::new(), test_fn);
466 }
467
468 pub(crate) fn test_serialize_and_deserialize<T, D, F>(input: &str, expected: T, get_result: F)
469 where
470 T: Debug + PartialEq,
471 F: Fn(D) -> T,
472 D: DeserializeOwned + Serialize + Clone,
473 {
474 let config: D = toml::from_str(input).unwrap();
475 assert_eq!(expected, get_result(config.clone()));
476
477 let serialized = toml::to_string(&config).unwrap();
478 let deserialized = toml::from_str(&serialized).unwrap();
479 assert_eq!(expected, get_result(deserialized));
480 }
481
482 #[test]
483 fn config_ticket_server_addr_env() {
484 test_config_from_env(
485 vec![("HTSGET_TICKET_SERVER_ADDR", "127.0.0.1:8082")],
486 |config| {
487 assert_eq!(
488 config.ticket_server().addr(),
489 "127.0.0.1:8082".parse().unwrap()
490 );
491 },
492 );
493 }
494
495 #[test]
496 fn config_ticket_server_cors_allow_origin_env() {
497 test_config_from_env(
498 vec![("HTSGET_TICKET_SERVER_CORS_ALLOW_CREDENTIALS", true)],
499 |config| {
500 assert!(config.ticket_server().cors().allow_credentials());
501 },
502 );
503 }
504
505 #[test]
506 fn config_service_info_id_env() {
507 test_config_from_env(vec![("HTSGET_SERVICE_INFO", "{ id=id }")], |config| {
508 assert_eq!(config.service_info().as_ref().get("id"), Some(&json!("id")));
509 });
510 }
511
512 #[test]
513 fn config_data_server_addr_env() {
514 test_config_from_env(
515 vec![("HTSGET_DATA_SERVER_ADDR", "127.0.0.1:8082")],
516 |config| {
517 assert_eq!(
518 config
519 .data_server()
520 .clone()
521 .as_data_server_config()
522 .unwrap()
523 .addr(),
524 "127.0.0.1:8082".parse().unwrap()
525 );
526 },
527 );
528 }
529
530 #[test]
531 fn config_ticket_server_addr_file() {
532 test_config_from_file(r#"ticket_server.addr = "127.0.0.1:8082""#, |config| {
533 assert_eq!(
534 config.ticket_server().addr(),
535 "127.0.0.1:8082".parse().unwrap()
536 );
537 });
538 }
539
540 #[test]
541 fn config_ticket_server_cors_allow_origin_file() {
542 test_config_from_file(r#"ticket_server.cors.allow_credentials = true"#, |config| {
543 assert!(config.ticket_server().cors().allow_credentials());
544 });
545 }
546
547 #[test]
548 fn config_service_info_id_file() {
549 test_config_from_file(r#"service_info.id = "id""#, |config| {
550 assert_eq!(config.service_info().as_ref().get("id"), Some(&json!("id")));
551 });
552 }
553
554 #[test]
555 fn config_data_server_addr_file() {
556 test_config_from_file(r#"data_server.addr = "127.0.0.1:8082""#, |config| {
557 assert_eq!(
558 config
559 .data_server()
560 .clone()
561 .as_data_server_config()
562 .unwrap()
563 .addr(),
564 "127.0.0.1:8082".parse().unwrap()
565 );
566 });
567 }
568
569 #[test]
570 #[should_panic]
571 fn config_data_server_tls_no_cert() {
572 with_test_certificates(|path, _, _| {
573 let key_path = path.join("key.pem");
574
575 test_config_from_file(
576 &format!(
577 r#"
578 data_server.tls.key = "{}"
579 "#,
580 key_path.to_string_lossy().escape_default()
581 ),
582 |config| {
583 assert!(
584 config
585 .data_server()
586 .clone()
587 .as_data_server_config()
588 .unwrap()
589 .tls()
590 .is_none()
591 );
592 },
593 );
594 });
595 }
596
597 #[test]
598 fn config_data_server_tls() {
599 with_test_certificates(|path, _, _| {
600 let key_path = path.join("key.pem");
601 let cert_path = path.join("cert.pem");
602
603 test_config_from_file(
604 &format!(
605 r#"
606 data_server.tls.key = "{}"
607 data_server.tls.cert = "{}"
608 "#,
609 key_path.to_string_lossy().escape_default(),
610 cert_path.to_string_lossy().escape_default()
611 ),
612 |config| {
613 assert!(
614 config
615 .data_server()
616 .clone()
617 .as_data_server_config()
618 .unwrap()
619 .tls()
620 .is_some()
621 );
622 },
623 );
624 });
625 }
626
627 #[test]
628 fn config_data_server_tls_env() {
629 with_test_certificates(|path, _, _| {
630 let key_path = path.join("key.pem");
631 let cert_path = path.join("cert.pem");
632
633 test_config_from_env(
634 vec![
635 ("HTSGET_DATA_SERVER_TLS_KEY", key_path.to_string_lossy()),
636 ("HTSGET_DATA_SERVER_TLS_CERT", cert_path.to_string_lossy()),
637 ],
638 |config| {
639 assert!(
640 config
641 .data_server()
642 .clone()
643 .as_data_server_config()
644 .unwrap()
645 .tls()
646 .is_some()
647 );
648 },
649 );
650 });
651 }
652
653 #[test]
654 #[should_panic]
655 fn config_ticket_server_tls_no_cert() {
656 with_test_certificates(|path, _, _| {
657 let key_path = path.join("key.pem");
658
659 test_config_from_file(
660 &format!(
661 r#"
662 ticket_server.tls.key = "{}"
663 "#,
664 key_path.to_string_lossy().escape_default()
665 ),
666 |config| {
667 assert!(config.ticket_server().tls().is_none());
668 },
669 );
670 });
671 }
672
673 #[test]
674 fn config_ticket_server_tls() {
675 with_test_certificates(|path, _, _| {
676 let key_path = path.join("key.pem");
677 let cert_path = path.join("cert.pem");
678
679 test_config_from_file(
680 &format!(
681 r#"
682 ticket_server.tls.key = "{}"
683 ticket_server.tls.cert = "{}"
684 "#,
685 key_path.to_string_lossy().escape_default(),
686 cert_path.to_string_lossy().escape_default()
687 ),
688 |config| {
689 assert!(config.ticket_server().tls().is_some());
690 },
691 );
692 });
693 }
694
695 #[test]
696 fn config_ticket_server_tls_env() {
697 with_test_certificates(|path, _, _| {
698 let key_path = path.join("key.pem");
699 let cert_path = path.join("cert.pem");
700
701 test_config_from_env(
702 vec![
703 ("HTSGET_TICKET_SERVER_TLS_KEY", key_path.to_string_lossy()),
704 ("HTSGET_TICKET_SERVER_TLS_CERT", cert_path.to_string_lossy()),
705 ],
706 |config| {
707 assert!(config.ticket_server().tls().is_some());
708 },
709 );
710 });
711 }
712
713 #[test]
714 fn locations_from_data_server_config() {
715 test_config_from_file(
716 r#"
717 data_server.addr = "127.0.0.1:8080"
718 data_server.local_path = "path"
719
720 [[locations]]
721 regex = "123"
722 backend.kind = "File"
723 backend.local_path = "path"
724 "#,
725 |config| {
726 assert_eq!(config.locations().len(), 1);
727 let config = config.locations.into_inner();
728 let regex = config[0].as_regex().unwrap();
729 assert!(matches!(regex.backend(),
730 Backend::File(file) if file.local_path() == "path" && file.scheme() == Scheme::Http && file.authority() == &Authority::from_static("127.0.0.1:8080")));
731 },
732 );
733 }
734
735 #[test]
736 fn parse_example_auth_config() {
737 let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("docs/examples/auth.toml");
738 let config = Config::from_path(&path).unwrap();
739
740 let auth = config.ticket_server().auth().unwrap();
741 assert!(auth.jwt().and_then(|jwt| jwt.jwks()).is_some());
742 assert!(
743 auth
744 .authorization()
745 .and_then(|source| source.callout())
746 .is_some()
747 );
748 }
749
750 #[test]
751 fn data_server_ticket_origin_propagates_to_file_backend() {
752 test_config_from_file(
753 r#"
754 data_server.addr = "127.0.0.1:8080"
755 data_server.local_path = "data"
756 data_server.ticket_origin = "https://example.com/"
757
758 locations = "file://data"
759 "#,
760 |config| {
761 let locations = config.locations.into_inner();
762 let backend = locations[0].as_simple().unwrap().backend();
763 assert!(matches!(backend,
764 Backend::File(file) if file.ticket_origin() == Some("https://example.com/")));
765 },
766 );
767 }
768
769 #[test]
770 fn simple_locations_env() {
771 test_config_from_env(
772 vec![
773 ("HTSGET_DATA_SERVER_ADDR", "127.0.0.1:8080"),
774 (
775 "HTSGET_LOCATIONS",
776 "[ { location=file://data, prefix=bam }, { location=file://data, prefix=cram }]",
777 ),
778 ],
779 |config| {
780 assert_multiple(config);
781 },
782 );
783 }
784
785 #[test]
786 fn simple_locations() {
787 test_config_from_file(
788 r#"
789 data_server.addr = "127.0.0.1:8080"
790 data_server.local_path = "data"
791
792 locations = "file://data"
793 "#,
794 |config| {
795 assert_eq!(config.locations().len(), 1);
796 let config = config.locations.into_inner();
797 let location = config[0].as_simple().unwrap();
798 assert_eq!(location.prefix_or_id(), None);
799 assert_file_location(location, "data");
800 },
801 );
802 }
803
804 #[cfg(feature = "aws")]
805 #[test]
806 fn simple_locations_s3() {
807 test_config_from_file(
808 r#"
809 locations = "s3://bucket"
810 "#,
811 |config| {
812 assert_eq!(config.locations().len(), 1);
813 let config = config.locations.into_inner();
814 let location = config[0].as_simple().unwrap();
815 assert_eq!(location.prefix_or_id(), None);
816 assert!(matches!(location.backend(),
817 Backend::S3(s3) if s3.bucket() == "bucket"));
818 },
819 );
820 }
821
822 #[cfg(feature = "aws")]
823 #[test]
824 fn simple_locations_s3_env() {
825 test_config_from_env(
826 vec![("HTSGET_LOCATIONS", "[{location=s3://bucket}]")],
827 |config| {
828 assert_eq!(config.locations().len(), 1);
829 let config = config.locations.into_inner();
830 let location = config[0].as_simple().unwrap();
831 assert_eq!(location.prefix_or_id(), None);
832 assert!(matches!(location.backend(),
833 Backend::S3(s3) if s3.bucket() == "bucket"));
834 },
835 );
836 }
837
838 #[cfg(feature = "url")]
839 #[test]
840 fn simple_locations_url() {
841 test_config_from_file(
842 r#"
843 locations = "https://example.com"
844 "#,
845 |config| {
846 assert_eq!(config.locations().len(), 1);
847 let config = config.locations.into_inner();
848 let location = config[0].as_simple().unwrap();
849 assert_eq!(location.prefix_or_id(), None);
850 assert!(matches!(location.backend(),
851 Backend::Url(url) if url.url() == &"https://example.com".parse::<Uri>().unwrap()));
852 },
853 );
854 }
855
856 #[test]
857 fn simple_locations_multiple() {
858 test_config_from_file(
859 r#"
860 data_server.addr = "127.0.0.1:8080"
861 locations = [ { location = "file://data", prefix = "bam" }, { location = "file://data", prefix = "cram" }]
862 "#,
863 |config| {
864 assert_multiple(config);
865 },
866 );
867 }
868
869 #[cfg(feature = "aws")]
870 #[test]
871 fn simple_locations_multiple_mixed() {
872 test_config_from_file(
873 r#"
874 data_server.addr = "127.0.0.1:8080"
875 data_server.local_path = "data"
876 locations = [ { location = "file://data", prefix = "bam" }, { location = "file://data", prefix = "cram" }, { location = "s3://bucket", prefix = "vcf" } ]
877 "#,
878 |config| {
879 assert_eq!(config.locations().len(), 3);
880 let config = config.locations.into_inner();
881
882 let location = config[0].as_simple().unwrap();
883 assert_eq!(location.prefix_or_id().unwrap().as_prefix().unwrap(), "bam");
884 assert_file_location(location, "data");
885
886 let location = config[1].as_simple().unwrap();
887 assert_eq!(
888 location.prefix_or_id().unwrap().as_prefix().unwrap(),
889 "cram"
890 );
891 assert_file_location(location, "data");
892
893 let location = config[2].as_simple().unwrap();
894 assert_eq!(location.prefix_or_id().unwrap().as_prefix().unwrap(), "vcf");
895 assert!(matches!(location.backend(),
896 Backend::S3(s3) if s3.bucket() == "bucket"));
897 },
898 );
899 }
900
901 #[test]
902 fn config_server_auth() {
903 test_config_from_file(
904 r#"
905 ticket_server.auth.jwt = { kind = "jwks", url = "https://www.example.com/" }
906 ticket_server.auth.validate_issuer = ["iss1"]
907 ticket_server.auth.authorization = { kind = "callout", url = "https://www.example.com" }
908 data_server.auth.jwt = { kind = "jwks", url = "https://www.example.com/" }
909 data_server.auth.validate_audience = ["aud1"]
910 data_server.auth.authorization = { kind = "callout", url = "https://www.example.com" }
911 "#,
912 |config| {
913 let auth = config.ticket_server().auth().unwrap();
914 assert_eq!(
915 auth.jwt().unwrap().jwks().unwrap().url().to_string(),
916 "https://www.example.com/"
917 );
918 assert_eq!(
919 auth.validate_issuer(),
920 Some(vec!["iss1".to_string()].as_slice())
921 );
922 assert_eq!(
923 auth
924 .authorization()
925 .unwrap()
926 .callout()
927 .unwrap()
928 .url()
929 .to_string(),
930 "https://www.example.com/"
931 );
932 let auth = config
933 .data_server()
934 .as_data_server_config()
935 .unwrap()
936 .auth()
937 .unwrap();
938 assert_eq!(
939 auth.jwt().unwrap().jwks().unwrap().url().to_string(),
940 "https://www.example.com/"
941 );
942 assert_eq!(
943 auth.validate_audience(),
944 Some(vec!["aud1".to_string()].as_slice())
945 );
946 assert_eq!(
947 auth
948 .authorization()
949 .unwrap()
950 .callout()
951 .unwrap()
952 .url()
953 .to_string(),
954 "https://www.example.com/"
955 );
956 },
957 );
958 }
959
960 #[test]
961 fn config_server_auth_global() {
962 test_config_from_file(
963 r#"
964 auth.jwt = { kind = "jwks", url = "https://www.example.com/" }
965 auth.validate_audience = ["aud1"]
966 auth.authorization = { kind = "callout", url = "https://www.example.com" }
967 "#,
968 |config| {
969 let auth = config.auth.unwrap();
970 assert_eq!(
971 auth.jwt().unwrap().jwks().unwrap().url().to_string(),
972 "https://www.example.com/"
973 );
974 assert_eq!(
975 auth.validate_audience(),
976 Some(vec!["aud1".to_string()].as_slice())
977 );
978 assert_eq!(
979 auth
980 .authorization()
981 .unwrap()
982 .callout()
983 .unwrap()
984 .url()
985 .to_string(),
986 "https://www.example.com/"
987 );
988 },
989 );
990 }
991
992 #[cfg(feature = "aws")]
993 #[test]
994 fn no_data_server() {
995 test_config_from_file(
996 r#"
997 data_server = "None"
998 locations = "s3://bucket"
999 "#,
1000 |config| {
1001 assert!(config.data_server().as_data_server_config().is_err());
1002 },
1003 );
1004 }
1005
1006 fn assert_multiple(config: Config) {
1007 assert_eq!(config.locations().len(), 2);
1008 let config = config.locations.into_inner();
1009
1010 println!("{config:#?}");
1011
1012 let location = config[0].as_simple().unwrap();
1013 assert_eq!(location.prefix_or_id().unwrap().as_prefix().unwrap(), "bam");
1014 assert_file_location(location, "data");
1015
1016 let location = config[1].as_simple().unwrap();
1017 assert_eq!(
1018 location.prefix_or_id().unwrap().as_prefix().unwrap(),
1019 "cram"
1020 );
1021 assert_file_location(location, "data");
1022 }
1023
1024 fn assert_file_location(location: &SimpleLocation, local_path: &str) {
1025 assert!(matches!(location.backend(),
1026 Backend::File(file) if file.local_path() == local_path && file.scheme() == Scheme::Http && file.authority() == &Authority::from_static("127.0.0.1:8080")));
1027 }
1028}