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