htsget_config/config/
mod.rs

1//! Structs to serialize and deserialize the htsget-rs config options.
2//!
3
4use 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
39/// The usage string for htsget-rs.
40pub 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/// The command line arguments allowed for the htsget-rs executables.
44#[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/// Simplified config.
66#[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  /// Create a config.
83  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  /// Get the ticket server config.
104  pub fn formatting_style(&self) -> FormattingStyle {
105    self.formatting_style
106  }
107
108  /// Get the ticket server config.
109  pub fn ticket_server(&self) -> &TicketServerConfig {
110    &self.ticket_server
111  }
112
113  /// Get the mutable ticket server config.
114  pub fn ticket_server_mut(&mut self) -> &mut TicketServerConfig {
115    &mut self.ticket_server
116  }
117
118  /// Get the data server config.
119  pub fn data_server(&self) -> &DataServerEnabled {
120    &self.data_server
121  }
122
123  /// Get the mutable data server config.
124  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  /// Get the package info.
132  pub fn package_info(&self) -> &PackageInfo {
133    &self.package_info
134  }
135
136  /// Get the service info config.
137  pub fn service_info(&self) -> &ServiceInfo {
138    &self.service_info
139  }
140
141  /// Set the package info for the config from a dependent package.
142  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    if let Some(ref mut auth) = self.auth {
150      auth.set_from_package_info(&self.package_info)?;
151    };
152    if let Some(ref mut auth) = self.ticket_server.auth {
153      auth.set_from_package_info(&self.package_info)?;
154    }
155    if let DataServerEnabled::Some(ref mut data_server_config) = self.data_server
156      && let Some(ref mut auth) = data_server_config.auth
157    {
158      auth.set_from_package_info(&self.package_info)?;
159    }
160
161    self.locations.set_from_package_info(&self.package_info)?;
162
163    Ok(())
164  }
165
166  /// Get a mutable instance of the service info config.
167  pub fn service_info_mut(&mut self) -> &mut ServiceInfo {
168    &mut self.service_info
169  }
170
171  /// Get the location.
172  pub fn locations(&self) -> &[Location] {
173    self.locations.as_slice()
174  }
175
176  pub fn into_locations(self) -> Locations {
177    self.locations
178  }
179
180  /// Parse the command line arguments. Returns the config path, or prints the default config.
181  /// Augment the `Command` args from the `clap` parser. Returns an error if the
182  pub fn parse_args_with_command(augment_args: Command) -> Result<Option<PathBuf>> {
183    let args = Args::from_arg_matches(&Args::augment_args(augment_args).get_matches())
184      .map_err(|err| ArgParseError(err.to_string()))?;
185
186    if args.config.as_ref().is_some_and(|path| !path.exists()) {
187      return Err(ParseError("config file not found".to_string()));
188    }
189
190    Ok(Self::parse_with_args(args))
191  }
192
193  /// Parse the command line arguments. Returns the config path, or prints the default config.
194  pub fn parse_args() -> Option<PathBuf> {
195    Self::parse_with_args(Args::parse())
196  }
197
198  fn parse_with_args(args: Args) -> Option<PathBuf> {
199    if args.print_default_config {
200      println!(
201        "{}",
202        toml::ser::to_string_pretty(&Config::default()).unwrap()
203      );
204      None
205    } else if args.print_response_schema {
206      println!(
207        "{}",
208        serde_json::to_string_pretty(&schema_for!(AuthorizationRestrictions)).unwrap()
209      );
210      None
211    } else {
212      Some(args.config.unwrap_or_else(|| "".into()))
213    }
214  }
215
216  /// Read a config struct from a TOML file.
217  pub fn from_path(path: &Path) -> io::Result<Self> {
218    let mut config: Self = from_path(path)?;
219
220    // Propagate global config to individual ticket and data servers.
221    if let DataServerEnabled::Some(ref mut data_server_config) = config.data_server
222      && data_server_config.auth().is_none()
223    {
224      data_server_config.set_auth(config.auth.clone());
225    }
226    if config.ticket_server().auth().is_none() {
227      config.ticket_server.set_auth(config.auth.clone());
228    }
229
230    Ok(config.validate_file_locations()?)
231  }
232
233  /// Setup tracing, using a global subscriber.
234  pub fn setup_tracing(&self) -> Result<()> {
235    let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
236
237    let subscriber = Registry::default().with(env_filter);
238
239    match self.formatting_style() {
240      FormattingStyle::Full => set_global_default(subscriber.with(layer())),
241      FormattingStyle::Compact => {
242        set_global_default(subscriber.with(layer().event_format(format().compact())))
243      }
244      FormattingStyle::Pretty => {
245        set_global_default(subscriber.with(layer().event_format(format().pretty())))
246      }
247      FormattingStyle::Json => {
248        set_global_default(subscriber.with(layer().event_format(format().json())))
249      }
250    }
251    .map_err(|err| TracingError(err.to_string()))?;
252
253    Ok(())
254  }
255
256  /// Set the local resolvers from the data server config.
257  pub fn validate_file_locations(mut self) -> Result<Self> {
258    if !self
259      .locations()
260      .iter()
261      .any(|location| location.backend().as_file().is_ok())
262    {
263      return Ok(self);
264    }
265
266    let DataServerEnabled::Some(ref mut config) = self.data_server else {
267      return Err(ParseError(
268        "must enable data server if using file locations".to_string(),
269      ));
270    };
271
272    let mut possible_paths: HashSet<_> =
273      HashSet::from_iter(self.locations.as_slice().iter().map(|location| {
274        location
275          .backend()
276          .as_file()
277          .ok()
278          .map(|file| file.local_path())
279      }));
280    possible_paths.remove(&None);
281
282    if possible_paths.len() > 1 {
283      return Err(ParseError(
284        "cannot have multiple file paths for file storage".to_string(),
285      ));
286    }
287    let local_path = possible_paths
288      .into_iter()
289      .next()
290      .flatten()
291      .ok_or_else(|| ParseError("failed to find local path from locations".to_string()))?
292      .to_string();
293
294    if config
295      .local_path()
296      .is_some_and(|path| path.to_string_lossy() != local_path)
297    {
298      return Err(ParseError(
299        "the data server local path and file storage directories must be the same".to_string(),
300      ));
301    }
302
303    config.set_local_path(Some(PathBuf::from(local_path)));
304
305    let scheme = config.tls().get_scheme();
306    let authority =
307      Authority::from_str(&config.addr().to_string()).map_err(|err| ParseError(err.to_string()))?;
308    let ticket_origin = config.ticket_origin();
309
310    self
311      .locations
312      .as_mut_slice()
313      .iter_mut()
314      .map(|location| {
315        // Configure the scheme and authority for file locations that haven't been
316        // explicitly set.
317        match location.backend_mut() {
318          Backend::File(file) => {
319            if file.is_defaulted {
320              file.set_scheme(scheme);
321              file.set_authority(authority.clone());
322              file.set_ticket_origin(ticket_origin.clone())
323            }
324          }
325          #[cfg(feature = "aws")]
326          Backend::S3(_) => {}
327          #[cfg(feature = "url")]
328          Backend::Url(_) => {}
329        }
330
331        // Ensure authorization header gets forwarded if the data server has authorization set.
332        if self
333          .data_server
334          .as_data_server_config()
335          .is_ok_and(|config| config.auth().is_some())
336        {
337          location
338            .backend_mut()
339            .add_ticket_header(AUTHORIZATION.to_string());
340        }
341
342        Ok(())
343      })
344      .collect::<Result<Vec<()>>>()?;
345
346    Ok(self)
347  }
348}
349
350impl Default for Config {
351  fn default() -> Self {
352    Self {
353      formatting_style: FormattingStyle::Full,
354      ticket_server: Default::default(),
355      data_server: DataServerEnabled::Some(Default::default()),
356      service_info: Default::default(),
357      locations: Default::default(),
358      auth: Default::default(),
359      package_info: Default::default(),
360    }
361  }
362}
363
364pub(crate) fn serialize_array_display<S, T>(
365  names: &[T],
366  serializer: S,
367) -> std::result::Result<S::Ok, S::Error>
368where
369  T: Display,
370  S: Serializer,
371{
372  let mut sequence = serializer.serialize_seq(Some(names.len()))?;
373  for element in names.iter().map(|name| format!("{name}")) {
374    sequence.serialize_element(&element)?;
375  }
376  sequence.end()
377}
378
379pub(crate) fn deserialize_vec_from_str<'de, D, T>(
380  deserializer: D,
381) -> std::result::Result<Vec<T>, D::Error>
382where
383  T: FromStr,
384  T::Err: Display,
385  D: Deserializer<'de>,
386{
387  let names: Vec<String> = Deserialize::deserialize(deserializer)?;
388  names
389    .into_iter()
390    .map(|name| T::from_str(&name).map_err(Error::custom))
391    .collect()
392}
393
394#[cfg(test)]
395pub(crate) mod tests {
396  use std::fmt::Display;
397
398  use super::*;
399  use crate::config::advanced::auth::authorization::UrlOrStatic;
400  use crate::config::advanced::auth::jwt::AuthMode;
401  use crate::config::location::SimpleLocation;
402  use crate::config::parser::from_str;
403  use crate::http::tests::with_test_certificates;
404  use crate::storage::Backend;
405  use crate::types::Scheme;
406  use figment::Jail;
407  use http::Uri;
408  use http::uri::Authority;
409  use serde::de::DeserializeOwned;
410  use serde_json::json;
411
412  fn test_config<K, V, F>(contents: Option<&str>, env_variables: Vec<(K, V)>, test_fn: F)
413  where
414    K: AsRef<str>,
415    V: Display,
416    F: Fn(Config),
417  {
418    Jail::expect_with(|jail| {
419      let file = "test.toml";
420
421      if let Some(contents) = contents {
422        jail.create_file(file, contents)?;
423      }
424
425      for (key, value) in env_variables {
426        jail.set_env(key, value);
427      }
428
429      let path = Path::new(file);
430      test_fn(Config::from_path(path).map_err(|err| err.to_string())?);
431
432      test_fn(
433        from_path::<Config>(path)
434          .map_err(|err| err.to_string())?
435          .validate_file_locations()
436          .map_err(|err| err.to_string())?,
437      );
438      test_fn(
439        from_str::<Config>(contents.unwrap_or(""))
440          .map_err(|err| err.to_string())?
441          .validate_file_locations()
442          .map_err(|err| err.to_string())?,
443      );
444
445      Ok(())
446    });
447  }
448
449  pub(crate) fn test_config_from_env<K, V, F>(env_variables: Vec<(K, V)>, test_fn: F)
450  where
451    K: AsRef<str>,
452    V: Display,
453    F: Fn(Config),
454  {
455    test_config(None, env_variables, test_fn);
456  }
457
458  pub(crate) fn test_config_from_file<F>(contents: &str, test_fn: F)
459  where
460    F: Fn(Config),
461  {
462    test_config(Some(contents), Vec::<(&str, &str)>::new(), test_fn);
463  }
464
465  pub(crate) fn test_serialize_and_deserialize<T, D, F>(input: &str, expected: T, get_result: F)
466  where
467    T: Debug + PartialEq,
468    F: Fn(D) -> T,
469    D: DeserializeOwned + Serialize + Clone,
470  {
471    let config: D = toml::from_str(input).unwrap();
472    assert_eq!(expected, get_result(config.clone()));
473
474    let serialized = toml::to_string(&config).unwrap();
475    let deserialized = toml::from_str(&serialized).unwrap();
476    assert_eq!(expected, get_result(deserialized));
477  }
478
479  #[test]
480  fn config_ticket_server_addr_env() {
481    test_config_from_env(
482      vec![("HTSGET_TICKET_SERVER_ADDR", "127.0.0.1:8082")],
483      |config| {
484        assert_eq!(
485          config.ticket_server().addr(),
486          "127.0.0.1:8082".parse().unwrap()
487        );
488      },
489    );
490  }
491
492  #[test]
493  fn config_ticket_server_cors_allow_origin_env() {
494    test_config_from_env(
495      vec![("HTSGET_TICKET_SERVER_CORS_ALLOW_CREDENTIALS", true)],
496      |config| {
497        assert!(config.ticket_server().cors().allow_credentials());
498      },
499    );
500  }
501
502  #[test]
503  fn config_service_info_id_env() {
504    test_config_from_env(vec![("HTSGET_SERVICE_INFO", "{ id=id }")], |config| {
505      assert_eq!(config.service_info().as_ref().get("id"), Some(&json!("id")));
506    });
507  }
508
509  #[test]
510  fn config_data_server_addr_env() {
511    test_config_from_env(
512      vec![("HTSGET_DATA_SERVER_ADDR", "127.0.0.1:8082")],
513      |config| {
514        assert_eq!(
515          config
516            .data_server()
517            .clone()
518            .as_data_server_config()
519            .unwrap()
520            .addr(),
521          "127.0.0.1:8082".parse().unwrap()
522        );
523      },
524    );
525  }
526
527  #[test]
528  fn config_ticket_server_addr_file() {
529    test_config_from_file(r#"ticket_server.addr = "127.0.0.1:8082""#, |config| {
530      assert_eq!(
531        config.ticket_server().addr(),
532        "127.0.0.1:8082".parse().unwrap()
533      );
534    });
535  }
536
537  #[test]
538  fn config_ticket_server_cors_allow_origin_file() {
539    test_config_from_file(r#"ticket_server.cors.allow_credentials = true"#, |config| {
540      assert!(config.ticket_server().cors().allow_credentials());
541    });
542  }
543
544  #[test]
545  fn config_service_info_id_file() {
546    test_config_from_file(r#"service_info.id = "id""#, |config| {
547      assert_eq!(config.service_info().as_ref().get("id"), Some(&json!("id")));
548    });
549  }
550
551  #[test]
552  fn config_data_server_addr_file() {
553    test_config_from_file(r#"data_server.addr = "127.0.0.1:8082""#, |config| {
554      assert_eq!(
555        config
556          .data_server()
557          .clone()
558          .as_data_server_config()
559          .unwrap()
560          .addr(),
561        "127.0.0.1:8082".parse().unwrap()
562      );
563    });
564  }
565
566  #[test]
567  #[should_panic]
568  fn config_data_server_tls_no_cert() {
569    with_test_certificates(|path, _, _| {
570      let key_path = path.join("key.pem");
571
572      test_config_from_file(
573        &format!(
574          r#"
575        data_server.tls.key = "{}"
576        "#,
577          key_path.to_string_lossy().escape_default()
578        ),
579        |config| {
580          assert!(
581            config
582              .data_server()
583              .clone()
584              .as_data_server_config()
585              .unwrap()
586              .tls()
587              .is_none()
588          );
589        },
590      );
591    });
592  }
593
594  #[test]
595  fn config_data_server_tls() {
596    with_test_certificates(|path, _, _| {
597      let key_path = path.join("key.pem");
598      let cert_path = path.join("cert.pem");
599
600      test_config_from_file(
601        &format!(
602          r#"
603          data_server.tls.key = "{}"
604          data_server.tls.cert = "{}"
605          "#,
606          key_path.to_string_lossy().escape_default(),
607          cert_path.to_string_lossy().escape_default()
608        ),
609        |config| {
610          assert!(
611            config
612              .data_server()
613              .clone()
614              .as_data_server_config()
615              .unwrap()
616              .tls()
617              .is_some()
618          );
619        },
620      );
621    });
622  }
623
624  #[test]
625  fn config_data_server_tls_env() {
626    with_test_certificates(|path, _, _| {
627      let key_path = path.join("key.pem");
628      let cert_path = path.join("cert.pem");
629
630      test_config_from_env(
631        vec![
632          ("HTSGET_DATA_SERVER_TLS_KEY", key_path.to_string_lossy()),
633          ("HTSGET_DATA_SERVER_TLS_CERT", cert_path.to_string_lossy()),
634        ],
635        |config| {
636          assert!(
637            config
638              .data_server()
639              .clone()
640              .as_data_server_config()
641              .unwrap()
642              .tls()
643              .is_some()
644          );
645        },
646      );
647    });
648  }
649
650  #[test]
651  #[should_panic]
652  fn config_ticket_server_tls_no_cert() {
653    with_test_certificates(|path, _, _| {
654      let key_path = path.join("key.pem");
655
656      test_config_from_file(
657        &format!(
658          r#"
659        ticket_server.tls.key = "{}"
660        "#,
661          key_path.to_string_lossy().escape_default()
662        ),
663        |config| {
664          assert!(config.ticket_server().tls().is_none());
665        },
666      );
667    });
668  }
669
670  #[test]
671  fn config_ticket_server_tls() {
672    with_test_certificates(|path, _, _| {
673      let key_path = path.join("key.pem");
674      let cert_path = path.join("cert.pem");
675
676      test_config_from_file(
677        &format!(
678          r#"
679        ticket_server.tls.key = "{}"
680        ticket_server.tls.cert = "{}"
681        "#,
682          key_path.to_string_lossy().escape_default(),
683          cert_path.to_string_lossy().escape_default()
684        ),
685        |config| {
686          assert!(config.ticket_server().tls().is_some());
687        },
688      );
689    });
690  }
691
692  #[test]
693  fn config_ticket_server_tls_env() {
694    with_test_certificates(|path, _, _| {
695      let key_path = path.join("key.pem");
696      let cert_path = path.join("cert.pem");
697
698      test_config_from_env(
699        vec![
700          ("HTSGET_TICKET_SERVER_TLS_KEY", key_path.to_string_lossy()),
701          ("HTSGET_TICKET_SERVER_TLS_CERT", cert_path.to_string_lossy()),
702        ],
703        |config| {
704          assert!(config.ticket_server().tls().is_some());
705        },
706      );
707    });
708  }
709
710  #[test]
711  fn locations_from_data_server_config() {
712    test_config_from_file(
713      r#"
714    data_server.addr = "127.0.0.1:8080"
715    data_server.local_path = "path"
716
717    [[locations]]
718    regex = "123"
719    backend.kind = "File"
720    backend.local_path = "path"
721    "#,
722      |config| {
723        assert_eq!(config.locations().len(), 1);
724        let config = config.locations.into_inner();
725        let regex = config[0].as_regex().unwrap();
726        assert!(matches!(regex.backend(),
727            Backend::File(file) if file.local_path() == "path" && file.scheme() == Scheme::Http && file.authority() == &Authority::from_static("127.0.0.1:8080")));
728      },
729    );
730  }
731
732  #[test]
733  fn simple_locations_env() {
734    test_config_from_env(
735      vec![
736        ("HTSGET_DATA_SERVER_ADDR", "127.0.0.1:8080"),
737        (
738          "HTSGET_LOCATIONS",
739          "[ { location=file://data, prefix=bam }, { location=file://data, prefix=cram }]",
740        ),
741      ],
742      |config| {
743        assert_multiple(config);
744      },
745    );
746  }
747
748  #[test]
749  fn simple_locations() {
750    test_config_from_file(
751      r#"
752    data_server.addr = "127.0.0.1:8080"
753    data_server.local_path = "data"
754    
755    locations = "file://data"
756    "#,
757      |config| {
758        assert_eq!(config.locations().len(), 1);
759        let config = config.locations.into_inner();
760        let location = config[0].as_simple().unwrap();
761        assert_eq!(location.prefix_or_id(), None);
762        assert_file_location(location, "data");
763      },
764    );
765  }
766
767  #[cfg(feature = "aws")]
768  #[test]
769  fn simple_locations_s3() {
770    test_config_from_file(
771      r#"
772    locations = "s3://bucket"
773    "#,
774      |config| {
775        assert_eq!(config.locations().len(), 1);
776        let config = config.locations.into_inner();
777        let location = config[0].as_simple().unwrap();
778        assert_eq!(location.prefix_or_id(), None);
779        assert!(matches!(location.backend(),
780            Backend::S3(s3) if s3.bucket() == "bucket"));
781      },
782    );
783  }
784
785  #[cfg(feature = "url")]
786  #[test]
787  fn simple_locations_url() {
788    test_config_from_file(
789      r#"
790    locations = "https://example.com"
791    "#,
792      |config| {
793        assert_eq!(config.locations().len(), 1);
794        let config = config.locations.into_inner();
795        let location = config[0].as_simple().unwrap();
796        assert_eq!(location.prefix_or_id(), None);
797        assert!(matches!(location.backend(),
798            Backend::Url(url) if url.url() == &"https://example.com".parse::<Uri>().unwrap()));
799      },
800    );
801  }
802
803  #[test]
804  fn simple_locations_multiple() {
805    test_config_from_file(
806      r#"
807    data_server.addr = "127.0.0.1:8080"
808    locations = [ { location = "file://data", prefix = "bam" }, { location = "file://data", prefix = "cram" }]
809    "#,
810      |config| {
811        assert_multiple(config);
812      },
813    );
814  }
815
816  #[cfg(feature = "aws")]
817  #[test]
818  fn simple_locations_multiple_mixed() {
819    test_config_from_file(
820      r#"
821    data_server.addr = "127.0.0.1:8080"
822    data_server.local_path = "data"
823    locations = [ { location = "file://data", prefix = "bam" }, { location = "file://data", prefix = "cram" }, { location = "s3://bucket", prefix = "vcf" } ]
824    "#,
825      |config| {
826        assert_eq!(config.locations().len(), 3);
827        let config = config.locations.into_inner();
828
829        let location = config[0].as_simple().unwrap();
830        assert_eq!(location.prefix_or_id().unwrap().as_prefix().unwrap(), "bam");
831        assert_file_location(location, "data");
832
833        let location = config[1].as_simple().unwrap();
834        assert_eq!(
835          location.prefix_or_id().unwrap().as_prefix().unwrap(),
836          "cram"
837        );
838        assert_file_location(location, "data");
839
840        let location = config[2].as_simple().unwrap();
841        assert_eq!(location.prefix_or_id().unwrap().as_prefix().unwrap(), "vcf");
842        assert!(matches!(location.backend(),
843            Backend::S3(s3) if s3.bucket() == "bucket"));
844      },
845    );
846  }
847
848  #[test]
849  fn config_server_auth() {
850    test_config_from_file(
851      r#"
852      ticket_server.auth.jwks_url = "https://www.example.com/"
853      ticket_server.auth.validate_issuer = ["iss1"]
854      ticket_server.auth.authorization_url = "https://www.example.com"
855      data_server.auth.jwks_url = "https://www.example.com/"
856      data_server.auth.validate_audience = ["aud1"]
857      data_server.auth.authorization_url = "https://www.example.com"
858      "#,
859      |config| {
860        let auth = config.ticket_server().auth().unwrap();
861        assert_eq!(
862          auth.auth_mode().unwrap(),
863          &AuthMode::Jwks("https://www.example.com/".parse().unwrap())
864        );
865        assert_eq!(
866          auth.validate_issuer(),
867          Some(vec!["iss1".to_string()].as_slice())
868        );
869        assert_eq!(
870          auth.authorization_url().unwrap(),
871          &UrlOrStatic::Url("https://www.example.com".parse::<Uri>().unwrap())
872        );
873        let auth = config
874          .data_server()
875          .as_data_server_config()
876          .unwrap()
877          .auth()
878          .unwrap();
879        assert_eq!(
880          auth.auth_mode().unwrap(),
881          &AuthMode::Jwks("https://www.example.com/".parse().unwrap())
882        );
883        assert_eq!(
884          auth.validate_audience(),
885          Some(vec!["aud1".to_string()].as_slice())
886        );
887        assert_eq!(
888          auth.authorization_url().unwrap(),
889          &UrlOrStatic::Url("https://www.example.com".parse::<Uri>().unwrap())
890        );
891      },
892    );
893  }
894
895  #[test]
896  fn config_server_auth_global() {
897    test_config_from_file(
898      r#"
899      auth.jwks_url = "https://www.example.com/"
900      auth.validate_audience = ["aud1"]
901      auth.authorization_url = "https://www.example.com"
902      "#,
903      |config| {
904        let auth = config.auth.unwrap();
905        assert_eq!(
906          auth.auth_mode().unwrap(),
907          &AuthMode::Jwks("https://www.example.com/".parse().unwrap())
908        );
909        assert_eq!(
910          auth.validate_audience(),
911          Some(vec!["aud1".to_string()].as_slice())
912        );
913        assert_eq!(
914          auth.authorization_url().unwrap(),
915          &UrlOrStatic::Url("https://www.example.com".parse::<Uri>().unwrap())
916        );
917      },
918    );
919  }
920
921  #[cfg(feature = "aws")]
922  #[test]
923  fn no_data_server() {
924    test_config_from_file(
925      r#"
926      data_server = "None"
927      locations = "s3://bucket"
928    "#,
929      |config| {
930        assert!(config.data_server().as_data_server_config().is_err());
931      },
932    );
933  }
934
935  fn assert_multiple(config: Config) {
936    assert_eq!(config.locations().len(), 2);
937    let config = config.locations.into_inner();
938
939    println!("{config:#?}");
940
941    let location = config[0].as_simple().unwrap();
942    assert_eq!(location.prefix_or_id().unwrap().as_prefix().unwrap(), "bam");
943    assert_file_location(location, "data");
944
945    let location = config[1].as_simple().unwrap();
946    assert_eq!(
947      location.prefix_or_id().unwrap().as_prefix().unwrap(),
948      "cram"
949    );
950    assert_file_location(location, "data");
951  }
952
953  fn assert_file_location(location: &SimpleLocation, local_path: &str) {
954    assert!(matches!(location.backend(),
955            Backend::File(file) if file.local_path() == local_path && file.scheme() == Scheme::Http && file.authority() == &Authority::from_static("127.0.0.1:8080")));
956  }
957}