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    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  /// Get a mutable instance of the service info config.
170  pub fn service_info_mut(&mut self) -> &mut ServiceInfo {
171    &mut self.service_info
172  }
173
174  /// Get the location.
175  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  /// Parse the command line arguments. Returns the config path, or prints the default config.
184  /// Augment the `Command` args from the `clap` parser. Returns an error if the
185  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  /// Parse the command line arguments. Returns the config path, or prints the default config.
197  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  /// Read a config struct from a TOML file.
220  pub fn from_path(path: &Path) -> io::Result<Self> {
221    let mut config: Self = from_path(path)?;
222
223    // Propagate global config to individual ticket and data servers.
224    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  /// Setup tracing, using a global subscriber.
237  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  /// Set the local resolvers from the data server config.
260  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        // Configure the scheme and authority for file locations that haven't been
319        // explicitly set.
320        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        // Ensure authorization header gets forwarded if the data server has authorization set.
335        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}