Skip to main content

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(_) | Backend::JsonPath(_) => {}
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    #[allow(clippy::result_large_err)]
419    Jail::expect_with(|jail| {
420      let file = "test.toml";
421
422      if let Some(contents) = contents {
423        jail.create_file(file, contents)?;
424      }
425
426      for (key, value) in env_variables {
427        jail.set_env(key, value);
428      }
429
430      let path = Path::new(file);
431      test_fn(Config::from_path(path).map_err(|err| err.to_string())?);
432
433      test_fn(
434        from_path::<Config>(path)
435          .map_err(|err| err.to_string())?
436          .validate_file_locations()
437          .map_err(|err| err.to_string())?,
438      );
439      test_fn(
440        from_str::<Config>(contents.unwrap_or(""))
441          .map_err(|err| err.to_string())?
442          .validate_file_locations()
443          .map_err(|err| err.to_string())?,
444      );
445
446      Ok(())
447    });
448  }
449
450  pub(crate) fn test_config_from_env<K, V, F>(env_variables: Vec<(K, V)>, test_fn: F)
451  where
452    K: AsRef<str>,
453    V: Display,
454    F: Fn(Config),
455  {
456    test_config(None, env_variables, test_fn);
457  }
458
459  pub(crate) fn test_config_from_file<F>(contents: &str, test_fn: F)
460  where
461    F: Fn(Config),
462  {
463    test_config(Some(contents), Vec::<(&str, &str)>::new(), test_fn);
464  }
465
466  pub(crate) fn test_serialize_and_deserialize<T, D, F>(input: &str, expected: T, get_result: F)
467  where
468    T: Debug + PartialEq,
469    F: Fn(D) -> T,
470    D: DeserializeOwned + Serialize + Clone,
471  {
472    let config: D = toml::from_str(input).unwrap();
473    assert_eq!(expected, get_result(config.clone()));
474
475    let serialized = toml::to_string(&config).unwrap();
476    let deserialized = toml::from_str(&serialized).unwrap();
477    assert_eq!(expected, get_result(deserialized));
478  }
479
480  #[test]
481  fn config_ticket_server_addr_env() {
482    test_config_from_env(
483      vec![("HTSGET_TICKET_SERVER_ADDR", "127.0.0.1:8082")],
484      |config| {
485        assert_eq!(
486          config.ticket_server().addr(),
487          "127.0.0.1:8082".parse().unwrap()
488        );
489      },
490    );
491  }
492
493  #[test]
494  fn config_ticket_server_cors_allow_origin_env() {
495    test_config_from_env(
496      vec![("HTSGET_TICKET_SERVER_CORS_ALLOW_CREDENTIALS", true)],
497      |config| {
498        assert!(config.ticket_server().cors().allow_credentials());
499      },
500    );
501  }
502
503  #[test]
504  fn config_service_info_id_env() {
505    test_config_from_env(vec![("HTSGET_SERVICE_INFO", "{ id=id }")], |config| {
506      assert_eq!(config.service_info().as_ref().get("id"), Some(&json!("id")));
507    });
508  }
509
510  #[test]
511  fn config_data_server_addr_env() {
512    test_config_from_env(
513      vec![("HTSGET_DATA_SERVER_ADDR", "127.0.0.1:8082")],
514      |config| {
515        assert_eq!(
516          config
517            .data_server()
518            .clone()
519            .as_data_server_config()
520            .unwrap()
521            .addr(),
522          "127.0.0.1:8082".parse().unwrap()
523        );
524      },
525    );
526  }
527
528  #[test]
529  fn config_ticket_server_addr_file() {
530    test_config_from_file(r#"ticket_server.addr = "127.0.0.1:8082""#, |config| {
531      assert_eq!(
532        config.ticket_server().addr(),
533        "127.0.0.1:8082".parse().unwrap()
534      );
535    });
536  }
537
538  #[test]
539  fn config_ticket_server_cors_allow_origin_file() {
540    test_config_from_file(r#"ticket_server.cors.allow_credentials = true"#, |config| {
541      assert!(config.ticket_server().cors().allow_credentials());
542    });
543  }
544
545  #[test]
546  fn config_service_info_id_file() {
547    test_config_from_file(r#"service_info.id = "id""#, |config| {
548      assert_eq!(config.service_info().as_ref().get("id"), Some(&json!("id")));
549    });
550  }
551
552  #[test]
553  fn config_data_server_addr_file() {
554    test_config_from_file(r#"data_server.addr = "127.0.0.1:8082""#, |config| {
555      assert_eq!(
556        config
557          .data_server()
558          .clone()
559          .as_data_server_config()
560          .unwrap()
561          .addr(),
562        "127.0.0.1:8082".parse().unwrap()
563      );
564    });
565  }
566
567  #[test]
568  #[should_panic]
569  fn config_data_server_tls_no_cert() {
570    with_test_certificates(|path, _, _| {
571      let key_path = path.join("key.pem");
572
573      test_config_from_file(
574        &format!(
575          r#"
576        data_server.tls.key = "{}"
577        "#,
578          key_path.to_string_lossy().escape_default()
579        ),
580        |config| {
581          assert!(
582            config
583              .data_server()
584              .clone()
585              .as_data_server_config()
586              .unwrap()
587              .tls()
588              .is_none()
589          );
590        },
591      );
592    });
593  }
594
595  #[test]
596  fn config_data_server_tls() {
597    with_test_certificates(|path, _, _| {
598      let key_path = path.join("key.pem");
599      let cert_path = path.join("cert.pem");
600
601      test_config_from_file(
602        &format!(
603          r#"
604          data_server.tls.key = "{}"
605          data_server.tls.cert = "{}"
606          "#,
607          key_path.to_string_lossy().escape_default(),
608          cert_path.to_string_lossy().escape_default()
609        ),
610        |config| {
611          assert!(
612            config
613              .data_server()
614              .clone()
615              .as_data_server_config()
616              .unwrap()
617              .tls()
618              .is_some()
619          );
620        },
621      );
622    });
623  }
624
625  #[test]
626  fn config_data_server_tls_env() {
627    with_test_certificates(|path, _, _| {
628      let key_path = path.join("key.pem");
629      let cert_path = path.join("cert.pem");
630
631      test_config_from_env(
632        vec![
633          ("HTSGET_DATA_SERVER_TLS_KEY", key_path.to_string_lossy()),
634          ("HTSGET_DATA_SERVER_TLS_CERT", cert_path.to_string_lossy()),
635        ],
636        |config| {
637          assert!(
638            config
639              .data_server()
640              .clone()
641              .as_data_server_config()
642              .unwrap()
643              .tls()
644              .is_some()
645          );
646        },
647      );
648    });
649  }
650
651  #[test]
652  #[should_panic]
653  fn config_ticket_server_tls_no_cert() {
654    with_test_certificates(|path, _, _| {
655      let key_path = path.join("key.pem");
656
657      test_config_from_file(
658        &format!(
659          r#"
660        ticket_server.tls.key = "{}"
661        "#,
662          key_path.to_string_lossy().escape_default()
663        ),
664        |config| {
665          assert!(config.ticket_server().tls().is_none());
666        },
667      );
668    });
669  }
670
671  #[test]
672  fn config_ticket_server_tls() {
673    with_test_certificates(|path, _, _| {
674      let key_path = path.join("key.pem");
675      let cert_path = path.join("cert.pem");
676
677      test_config_from_file(
678        &format!(
679          r#"
680        ticket_server.tls.key = "{}"
681        ticket_server.tls.cert = "{}"
682        "#,
683          key_path.to_string_lossy().escape_default(),
684          cert_path.to_string_lossy().escape_default()
685        ),
686        |config| {
687          assert!(config.ticket_server().tls().is_some());
688        },
689      );
690    });
691  }
692
693  #[test]
694  fn config_ticket_server_tls_env() {
695    with_test_certificates(|path, _, _| {
696      let key_path = path.join("key.pem");
697      let cert_path = path.join("cert.pem");
698
699      test_config_from_env(
700        vec![
701          ("HTSGET_TICKET_SERVER_TLS_KEY", key_path.to_string_lossy()),
702          ("HTSGET_TICKET_SERVER_TLS_CERT", cert_path.to_string_lossy()),
703        ],
704        |config| {
705          assert!(config.ticket_server().tls().is_some());
706        },
707      );
708    });
709  }
710
711  #[test]
712  fn locations_from_data_server_config() {
713    test_config_from_file(
714      r#"
715    data_server.addr = "127.0.0.1:8080"
716    data_server.local_path = "path"
717
718    [[locations]]
719    regex = "123"
720    backend.kind = "File"
721    backend.local_path = "path"
722    "#,
723      |config| {
724        assert_eq!(config.locations().len(), 1);
725        let config = config.locations.into_inner();
726        let regex = config[0].as_regex().unwrap();
727        assert!(matches!(regex.backend(),
728            Backend::File(file) if file.local_path() == "path" && file.scheme() == Scheme::Http && file.authority() == &Authority::from_static("127.0.0.1:8080")));
729      },
730    );
731  }
732
733  #[test]
734  fn simple_locations_env() {
735    test_config_from_env(
736      vec![
737        ("HTSGET_DATA_SERVER_ADDR", "127.0.0.1:8080"),
738        (
739          "HTSGET_LOCATIONS",
740          "[ { location=file://data, prefix=bam }, { location=file://data, prefix=cram }]",
741        ),
742      ],
743      |config| {
744        assert_multiple(config);
745      },
746    );
747  }
748
749  #[test]
750  fn simple_locations() {
751    test_config_from_file(
752      r#"
753    data_server.addr = "127.0.0.1:8080"
754    data_server.local_path = "data"
755    
756    locations = "file://data"
757    "#,
758      |config| {
759        assert_eq!(config.locations().len(), 1);
760        let config = config.locations.into_inner();
761        let location = config[0].as_simple().unwrap();
762        assert_eq!(location.prefix_or_id(), None);
763        assert_file_location(location, "data");
764      },
765    );
766  }
767
768  #[cfg(feature = "aws")]
769  #[test]
770  fn simple_locations_s3() {
771    test_config_from_file(
772      r#"
773    locations = "s3://bucket"
774    "#,
775      |config| {
776        assert_eq!(config.locations().len(), 1);
777        let config = config.locations.into_inner();
778        let location = config[0].as_simple().unwrap();
779        assert_eq!(location.prefix_or_id(), None);
780        assert!(matches!(location.backend(),
781            Backend::S3(s3) if s3.bucket() == "bucket"));
782      },
783    );
784  }
785
786  #[cfg(feature = "url")]
787  #[test]
788  fn simple_locations_url() {
789    test_config_from_file(
790      r#"
791    locations = "https://example.com"
792    "#,
793      |config| {
794        assert_eq!(config.locations().len(), 1);
795        let config = config.locations.into_inner();
796        let location = config[0].as_simple().unwrap();
797        assert_eq!(location.prefix_or_id(), None);
798        assert!(matches!(location.backend(),
799            Backend::Url(url) if url.url() == &"https://example.com".parse::<Uri>().unwrap()));
800      },
801    );
802  }
803
804  #[test]
805  fn simple_locations_multiple() {
806    test_config_from_file(
807      r#"
808    data_server.addr = "127.0.0.1:8080"
809    locations = [ { location = "file://data", prefix = "bam" }, { location = "file://data", prefix = "cram" }]
810    "#,
811      |config| {
812        assert_multiple(config);
813      },
814    );
815  }
816
817  #[cfg(feature = "aws")]
818  #[test]
819  fn simple_locations_multiple_mixed() {
820    test_config_from_file(
821      r#"
822    data_server.addr = "127.0.0.1:8080"
823    data_server.local_path = "data"
824    locations = [ { location = "file://data", prefix = "bam" }, { location = "file://data", prefix = "cram" }, { location = "s3://bucket", prefix = "vcf" } ]
825    "#,
826      |config| {
827        assert_eq!(config.locations().len(), 3);
828        let config = config.locations.into_inner();
829
830        let location = config[0].as_simple().unwrap();
831        assert_eq!(location.prefix_or_id().unwrap().as_prefix().unwrap(), "bam");
832        assert_file_location(location, "data");
833
834        let location = config[1].as_simple().unwrap();
835        assert_eq!(
836          location.prefix_or_id().unwrap().as_prefix().unwrap(),
837          "cram"
838        );
839        assert_file_location(location, "data");
840
841        let location = config[2].as_simple().unwrap();
842        assert_eq!(location.prefix_or_id().unwrap().as_prefix().unwrap(), "vcf");
843        assert!(matches!(location.backend(),
844            Backend::S3(s3) if s3.bucket() == "bucket"));
845      },
846    );
847  }
848
849  #[test]
850  fn config_server_auth() {
851    test_config_from_file(
852      r#"
853      ticket_server.auth.jwks_url = "https://www.example.com/"
854      ticket_server.auth.validate_issuer = ["iss1"]
855      ticket_server.auth.authorization_url = "https://www.example.com"
856      data_server.auth.jwks_url = "https://www.example.com/"
857      data_server.auth.validate_audience = ["aud1"]
858      data_server.auth.authorization_url = "https://www.example.com"
859      "#,
860      |config| {
861        let auth = config.ticket_server().auth().unwrap();
862        assert_eq!(
863          auth.auth_mode().unwrap(),
864          &AuthMode::Jwks("https://www.example.com/".parse().unwrap())
865        );
866        assert_eq!(
867          auth.validate_issuer(),
868          Some(vec!["iss1".to_string()].as_slice())
869        );
870        assert_eq!(
871          auth.authorization_url().unwrap(),
872          &UrlOrStatic::Url("https://www.example.com".parse::<Uri>().unwrap())
873        );
874        let auth = config
875          .data_server()
876          .as_data_server_config()
877          .unwrap()
878          .auth()
879          .unwrap();
880        assert_eq!(
881          auth.auth_mode().unwrap(),
882          &AuthMode::Jwks("https://www.example.com/".parse().unwrap())
883        );
884        assert_eq!(
885          auth.validate_audience(),
886          Some(vec!["aud1".to_string()].as_slice())
887        );
888        assert_eq!(
889          auth.authorization_url().unwrap(),
890          &UrlOrStatic::Url("https://www.example.com".parse::<Uri>().unwrap())
891        );
892      },
893    );
894  }
895
896  #[test]
897  fn config_server_auth_global() {
898    test_config_from_file(
899      r#"
900      auth.jwks_url = "https://www.example.com/"
901      auth.validate_audience = ["aud1"]
902      auth.authorization_url = "https://www.example.com"
903      "#,
904      |config| {
905        let auth = config.auth.unwrap();
906        assert_eq!(
907          auth.auth_mode().unwrap(),
908          &AuthMode::Jwks("https://www.example.com/".parse().unwrap())
909        );
910        assert_eq!(
911          auth.validate_audience(),
912          Some(vec!["aud1".to_string()].as_slice())
913        );
914        assert_eq!(
915          auth.authorization_url().unwrap(),
916          &UrlOrStatic::Url("https://www.example.com".parse::<Uri>().unwrap())
917        );
918      },
919    );
920  }
921
922  #[cfg(feature = "aws")]
923  #[test]
924  fn no_data_server() {
925    test_config_from_file(
926      r#"
927      data_server = "None"
928      locations = "s3://bucket"
929    "#,
930      |config| {
931        assert!(config.data_server().as_data_server_config().is_err());
932      },
933    );
934  }
935
936  fn assert_multiple(config: Config) {
937    assert_eq!(config.locations().len(), 2);
938    let config = config.locations.into_inner();
939
940    println!("{config:#?}");
941
942    let location = config[0].as_simple().unwrap();
943    assert_eq!(location.prefix_or_id().unwrap().as_prefix().unwrap(), "bam");
944    assert_file_location(location, "data");
945
946    let location = config[1].as_simple().unwrap();
947    assert_eq!(
948      location.prefix_or_id().unwrap().as_prefix().unwrap(),
949      "cram"
950    );
951    assert_file_location(location, "data");
952  }
953
954  fn assert_file_location(location: &SimpleLocation, local_path: &str) {
955    assert!(matches!(location.backend(),
956            Backend::File(file) if file.local_path() == local_path && file.scheme() == Scheme::Http && file.authority() == &Authority::from_static("127.0.0.1:8080")));
957  }
958}