voa_config/file/
config_file.rs

1//! Handling of configuration files.
2
3use std::{collections::HashSet, fs::read_to_string, path::Path, str::FromStr};
4
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    ConfigOrigin,
10    Error,
11    core::{Context, Os, Purpose},
12    file::ConfigTechnologySettings,
13};
14
15/// A thin wrapper around [`Os`] to allow for config specific serialization.
16#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
17#[serde(into = "String", try_from = "String")]
18pub struct ConfigOs(Os);
19
20impl From<Os> for ConfigOs {
21    fn from(value: Os) -> Self {
22        Self(value)
23    }
24}
25
26impl From<ConfigOs> for String {
27    fn from(value: ConfigOs) -> Self {
28        value.0.os_to_string()
29    }
30}
31
32impl FromStr for ConfigOs {
33    type Err = Error;
34
35    fn from_str(s: &str) -> Result<Self, Self::Err> {
36        Ok(ConfigOs(Os::from_str(s)?))
37    }
38}
39
40impl TryFrom<String> for ConfigOs {
41    type Error = Error;
42
43    fn try_from(value: String) -> Result<Self, Self::Error> {
44        Self::from_str(&value)
45    }
46}
47
48/// A thin wrapper around [`Purpose`] to allow for config specific serialization.
49#[derive(Clone, Debug, Deserialize, Serialize)]
50#[serde(into = "String", try_from = "String")]
51pub struct ConfigPurpose(Purpose);
52
53impl From<Purpose> for ConfigPurpose {
54    fn from(value: Purpose) -> Self {
55        Self(value)
56    }
57}
58
59impl From<ConfigPurpose> for String {
60    fn from(value: ConfigPurpose) -> Self {
61        value.0.purpose_to_string()
62    }
63}
64
65impl FromStr for ConfigPurpose {
66    type Err = Error;
67
68    fn from_str(s: &str) -> Result<Self, Self::Err> {
69        Ok(ConfigPurpose(Purpose::from_str(s)?))
70    }
71}
72
73impl TryFrom<String> for ConfigPurpose {
74    type Error = Error;
75
76    fn try_from(value: String) -> Result<Self, Self::Error> {
77        Self::from_str(&value)
78    }
79}
80
81/// A thin wrapper around [`Context`] to allow for config specific serialization.
82#[derive(Clone, Debug, Deserialize, Serialize)]
83#[serde(into = "String", try_from = "String")]
84pub struct ConfigContext(Context);
85
86impl From<Context> for ConfigContext {
87    fn from(value: Context) -> Self {
88        Self(value)
89    }
90}
91
92impl From<ConfigContext> for String {
93    fn from(value: ConfigContext) -> Self {
94        value.0.to_string()
95    }
96}
97
98impl FromStr for ConfigContext {
99    type Err = Error;
100
101    fn from_str(s: &str) -> Result<Self, Self::Err> {
102        Ok(ConfigContext(Context::from_str(s)?))
103    }
104}
105
106impl TryFrom<String> for ConfigContext {
107    type Error = Error;
108
109    fn try_from(value: String) -> Result<Self, Self::Error> {
110        Self::from_str(&value)
111    }
112}
113
114/// Settings for a specific `context` in a VOA `purpose`.
115///
116/// This is a specialization for a specific context, which overrides any default technology settings
117/// set on an OS level.
118#[derive(Debug, Deserialize, Serialize, Validate)]
119pub struct ConfigContextSettings {
120    /// The VOA purpose for this override.
121    #[garde(skip)]
122    pub(crate) purpose: ConfigPurpose,
123
124    /// The VOA context for this override.
125    #[garde(skip)]
126    pub(crate) context: ConfigContext,
127
128    /// The specific technology settings.
129    #[garde(dive)]
130    pub(crate) technology_settings: ConfigTechnologySettings,
131}
132
133impl ConfigContextSettings {
134    /// Creates a new [`ConfigContextSettings`].
135    pub fn new(
136        purpose: impl Into<ConfigPurpose>,
137        context: impl Into<ConfigContext>,
138        technologies: ConfigTechnologySettings,
139    ) -> Self {
140        Self {
141            purpose: purpose.into(),
142            context: context.into(),
143            technology_settings: technologies,
144        }
145    }
146
147    /// Returns a reference to the [`Purpose`].
148    pub fn purpose(&self) -> &Purpose {
149        &self.purpose.0
150    }
151
152    /// Returns a reference to the [`Context`].
153    pub fn context(&self) -> &Context {
154        &self.context.0
155    }
156
157    /// Returns a reference to the [`ConfigTechnologySettings`].
158    pub fn config_technology_settings(&self) -> &ConfigTechnologySettings {
159        &self.technology_settings
160    }
161}
162
163/// Validates that the in a [`ConfigFile`] technology defaults are set, if no context override
164/// is provided.
165///
166/// # Errors
167///
168/// Returns an error, if the `technology_defaults` is [`None`] and `context_override` has no
169/// entries.
170fn validate_technology_defaults(
171    context_override: &[ConfigContextSettings],
172) -> impl FnOnce(&Option<ConfigTechnologySettings>, &()) -> garde::Result + '_ {
173    move |technology_defaults, _| {
174        if technology_defaults.is_none() && context_override.is_empty() {
175            return Err(garde::Error::new(
176                "must be set, if no context_override is defined".to_string(),
177            ));
178        }
179
180        Ok(())
181    }
182}
183
184/// Validates the `context_override` in a [`ConfigFile`].
185///
186/// # Errors
187///
188/// Returns an error, if the entries in `context_override` are not unique.
189fn validate_context_override(
190    context_override: &[ConfigContextSettings],
191    _context: &(),
192) -> garde::Result {
193    let duplicates = {
194        let mut duplicates = HashSet::new();
195        for context_settings in context_override.iter() {
196            let count = context_override
197                .iter()
198                .filter(|settings| {
199                    context_settings.purpose() == settings.purpose()
200                        && context_settings.context() == settings.context()
201                })
202                .count();
203
204            if count > 1 {
205                duplicates.insert((context_settings.purpose(), context_settings.context()));
206            }
207        }
208
209        duplicates
210    };
211
212    if !duplicates.is_empty() {
213        let mut dups_sorted = duplicates
214            .iter()
215            .map(|(purpose, context)| format!("{purpose}/{context}"))
216            .collect::<Vec<_>>();
217        dups_sorted.sort();
218        return Err(garde::Error::new(format!(
219            "cannot contain duplicates, but the following purpose/context combinations are listed more than once: {}",
220            dups_sorted.join(", ")
221        )));
222    }
223
224    Ok(())
225}
226
227#[derive(Clone, Copy, Debug)]
228pub enum ConfigFileType {
229    /// A default configuration file.
230    Config,
231
232    /// A drop-in configuration file.
233    DropIn,
234}
235
236/// Representation of a VOA configuration file.
237#[derive(Debug, Deserialize, Serialize, Validate)]
238pub struct ConfigFile {
239    /// Technology defaults for an OS.
240    #[serde(skip_serializing_if = "Option::is_none", default)]
241    #[garde(custom(validate_technology_defaults(&self.contexts)))]
242    pub(crate) default_technology_settings: Option<ConfigTechnologySettings>,
243
244    /// Context overrides for an OS.
245    ///
246    /// # Note
247    ///
248    /// This list can be empty, if `technology_defaults` is set, else there has to be at least one
249    /// entry.
250    #[serde(skip_serializing_if = "Vec::is_empty", default)]
251    #[garde(custom(validate_context_override))]
252    #[garde(dive)]
253    pub(crate) contexts: Vec<ConfigContextSettings>,
254}
255
256impl ConfigFile {
257    /// Creates a new [`ConfigFile`].
258    pub fn new(
259        technology_settings: Option<ConfigTechnologySettings>,
260        context_override: Vec<ConfigContextSettings>,
261    ) -> Result<Self, Error> {
262        let settings = Self {
263            default_technology_settings: technology_settings,
264            contexts: context_override,
265        };
266        settings.validate().map_err(|source| Error::Validation {
267            context: "creating an override configuration file".to_string(),
268            source,
269        })?;
270
271        Ok(settings)
272    }
273
274    /// Creates a [`ConfigFile`] from a string slice containing YAML data.
275    pub fn from_yaml_str(s: &str) -> Result<Self, Error> {
276        serde_saphyr::from_str(s).map_err(|source| Error::YamlDeserialize {
277            context: "creating an OS override configuration from a YAML string".to_string(),
278            source,
279        })
280    }
281
282    /// Creates a [`ConfigFile`] from a file containing YAML data.
283    ///
284    /// The additional `file_type` sets the origin according to whether the file is a default
285    /// configuration file or a drop-in configuration file.
286    pub fn from_yaml_file(
287        path: impl AsRef<Path>,
288        file_type: ConfigFileType,
289    ) -> Result<Self, Error> {
290        let path = path.as_ref();
291        let data = read_to_string(path).map_err(|source| Error::IoPath {
292            path: path.to_path_buf(),
293            context: "reading the file to string",
294            source,
295        })?;
296
297        let mut settings = Self::from_yaml_str(&data).map_err(|error| {
298            if let Error::Validation { source, .. } = error {
299                Error::Validation {
300                    context: format!("creating an OS override configuration from file {path:?}"),
301                    source,
302                }
303            } else {
304                error
305            }
306        })?;
307
308        // Add the path from which the ConfigTechnologySettings were added to any OS-level and
309        // context-level.
310        if let Some(defaults) = settings.default_technology_settings.as_mut() {
311            defaults.origins.push(match file_type {
312                ConfigFileType::Config => ConfigOrigin::ConfigFile(path.to_path_buf()),
313                ConfigFileType::DropIn => ConfigOrigin::DropInFile(path.to_path_buf()),
314            });
315        }
316        settings.contexts.iter_mut().for_each(|settings| {
317            settings.technology_settings.origins.push(match file_type {
318                ConfigFileType::Config => ConfigOrigin::ConfigFile(path.to_path_buf()),
319                ConfigFileType::DropIn => ConfigOrigin::DropInFile(path.to_path_buf()),
320            })
321        });
322
323        Ok(settings)
324    }
325
326    /// Serializes `self` as a YAML string.
327    pub fn to_yaml_string(&self) -> Result<String, Error> {
328        serde_saphyr::to_string(&self).map_err(|source| Error::YamlSerialize {
329            context: "serializing OS settings",
330            source,
331        })
332    }
333
334    /// Returns the optional [`ConfigTechnologySettings`].
335    pub fn default_technology_settings(&self) -> Option<&ConfigTechnologySettings> {
336        self.default_technology_settings.as_ref()
337    }
338
339    /// Returns a reference to the list of [`ConfigContextSettings`].
340    pub fn contexts(&self) -> &[ConfigContextSettings] {
341        &self.contexts
342    }
343
344    /// Returns a reference to the list of config file origins.
345    ///
346    /// # Note
347    ///
348    /// This returns an empty list, if neither settings nor context settings contain origins.
349    /// However, this can not happen, as the validation of [`ConfigFile`] does not allow it.
350    pub fn origins(&self) -> &[ConfigOrigin] {
351        if let Some(settings) = self.default_technology_settings() {
352            &settings.origins
353        } else if let Some(context_settings) = self.contexts().first() {
354            &context_settings.technology_settings.origins
355        } else {
356            &[]
357        }
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use std::{
364        collections::HashSet,
365        fmt::Display,
366        num::{NonZeroU8, NonZeroUsize},
367        path::PathBuf,
368        str::FromStr,
369        thread::current,
370    };
371
372    use insta::{assert_snapshot, with_settings};
373    use rstest::rstest;
374    use testresult::TestResult;
375
376    use super::*;
377    use crate::{
378        core::{Context, Purpose},
379        file::{
380            ConfigOpenpgpSettings,
381            ConfigTrustAnchorMode,
382            ConfigVerificationMethod,
383            ConfigWebOfTrustMode,
384            ConfigWebOfTrustRoot,
385        },
386        openpgp::{
387            DomainName,
388            NumCertifications,
389            NumDataSignatures,
390            OpenpgpFingerprint,
391            TrustAmountFlow,
392            TrustAmountPartial,
393            TrustAmountRoot,
394        },
395    };
396
397    const SNAPSHOT_PATH: &str = "fixtures/config_file/";
398
399    #[derive(Debug)]
400    enum ConfigDataRepresentation {
401        Concise,
402        Full,
403    }
404
405    impl Display for ConfigDataRepresentation {
406        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407            write!(
408                f,
409                "{}",
410                match self {
411                    Self::Concise => "concise",
412                    Self::Full => "full",
413                }
414            )
415        }
416    }
417
418    /// Ensures that [`ConfigFile::new`] fails on duplicate entries in `context_override`.
419    #[test]
420    fn config_file_new_fails_on_duplicate_context_settings() -> TestResult {
421        match ConfigFile::new(
422            None,
423            vec![
424                ConfigContextSettings::new(
425                    Purpose::from_str("purpose")?,
426                    Context::from_str("context")?,
427                    ConfigTechnologySettings::default(),
428                ),
429                ConfigContextSettings::new(
430                    Purpose::from_str("purpose")?,
431                    Context::from_str("context")?,
432                    ConfigTechnologySettings::default(),
433                ),
434            ],
435        ) {
436            Ok(settings) => panic!(
437                "Should have failed with Error::Validation, but succeeded instead: {settings:?}"
438            ),
439            Err(Error::Validation { source, .. }) => {
440                assert!(source.to_string().contains("cannot contain duplicates"))
441            }
442            Err(error) => {
443                panic!("Should have failed with Error::Validation, but failed differently: {error}")
444            }
445        }
446
447        Ok(())
448    }
449
450    /// Ensures that the string representation of [`ConfigFile`] remains
451    /// consistent when using a default OS-specific technology configuration.
452    #[rstest]
453    #[case::yaml_concise(ConfigDataRepresentation::Concise)]
454    #[case::yaml_full(ConfigDataRepresentation::Full)]
455    fn config_file_default_string_representation(
456        #[case] data_representation: ConfigDataRepresentation,
457    ) -> TestResult {
458        let description = "Configuration with default technology settings and no context-level technology settings";
459        let config = match data_representation {
460            ConfigDataRepresentation::Concise => {
461                ConfigFile::new(Some(ConfigTechnologySettings::default()), Vec::new())?
462            }
463            ConfigDataRepresentation::Full => ConfigFile::new(
464                Some(ConfigTechnologySettings::new(
465                    vec![ConfigOrigin::DropInFile(PathBuf::from(
466                        "/usr/share/voa/example.yaml.d/10-example.yml",
467                    ))],
468                    Some(ConfigOpenpgpSettings::new(
469                        Some(NumDataSignatures::default()),
470                        ConfigVerificationMethod::TrustAnchor(ConfigTrustAnchorMode::new(
471                            Some(NumCertifications::default()),
472                            Some(HashSet::new()),
473                            Some(HashSet::new()),
474                        )?),
475                    )?),
476                )),
477                Vec::new(),
478            )?,
479        };
480        let config_str = config.to_yaml_string()?;
481
482        with_settings!({
483            description => format!("{data_representation}: {description}"),
484            snapshot_path => SNAPSHOT_PATH,
485            prepend_module_to_snapshot => false,
486        }, {
487            assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
488        });
489
490        Ok(())
491    }
492
493    /// Ensures that the string representation of [`ConfigFile`] remains
494    /// consistent when providing an OS specific technology configuration.
495    #[test]
496    fn config_file_as_yaml_defaults_with_wot_openpgp() -> TestResult {
497        let description = "Configuration with OS-level technology settings using the 'web of trust' verification method for OpenPGP and no context-level technology settings";
498        let config = ConfigFile::new(
499            Some(ConfigTechnologySettings::new(
500                vec![ConfigOrigin::DropInFile(PathBuf::from(
501                    "/usr/share/voa/example.yaml.d/10-example.yml",
502                ))],
503                Some(ConfigOpenpgpSettings::new(
504                    Some(NumDataSignatures::new(
505                        NonZeroUsize::new(2).expect("2 is larger than 0"),
506                    )),
507                    ConfigVerificationMethod::WebOfTrust(ConfigWebOfTrustMode::new(
508                        Some(TrustAmountFlow::new(
509                            NonZeroUsize::new(100).expect("100 is larger than 0"),
510                        )),
511                        Some(TrustAmountPartial::new(
512                            NonZeroU8::new(50).expect("50 is larger than 0"),
513                        )?),
514                        Some(HashSet::from_iter([
515                            ConfigWebOfTrustRoot::new(
516                                OpenpgpFingerprint::from_str(
517                                    "e242ed3bffccdf271b7fbaf34ed72d089537b42f",
518                                )?,
519                                Some(TrustAmountRoot::new(
520                                    NonZeroU8::new(100).expect("100 is larger than 0"),
521                                )?),
522                            ),
523                            ConfigWebOfTrustRoot::new(
524                                OpenpgpFingerprint::from_str(
525                                    "d3b0f7c0b825ecbb0f0d7398072947e7b1537b6f",
526                                )?,
527                                Some(TrustAmountRoot::new(
528                                    NonZeroU8::new(120).expect("100 is larger than 0"),
529                                )?),
530                            ),
531                            ConfigWebOfTrustRoot::new(
532                                OpenpgpFingerprint::from_str(
533                                    "b787a81c32997fd39a5f4c0188363902d3586e7b",
534                                )?,
535                                Some(TrustAmountRoot::new(
536                                    NonZeroU8::new(110).expect("100 is larger than 0"),
537                                )?),
538                            ),
539                        ])),
540                        Some(HashSet::from_iter([DomainName::from_str("example.org")?])),
541                    )),
542                )?),
543            )),
544            Vec::new(),
545        )?;
546        let config_str = config.to_yaml_string()?;
547
548        with_settings!({
549            description => description,
550            snapshot_path => SNAPSHOT_PATH,
551            prepend_module_to_snapshot => false,
552        }, {
553            assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
554        });
555
556        Ok(())
557    }
558
559    /// Ensures that the string representation of [`ConfigFile`] remains
560    /// consistent when providing context specific technology configurations.
561    #[test]
562    fn config_file_as_yaml_default_trust_anchor_openpgp_context_trust_anchor_overrides()
563    -> TestResult {
564        let description = "Configuration with OS-level technology settings using the 'trust anchor' verification method for OpenPGP and two context-level technology settings overriding some settings.";
565        let config = ConfigFile::new(
566            Some(ConfigTechnologySettings::new(
567                vec![ConfigOrigin::DropInFile(PathBuf::from(
568                    "/usr/share/voa/example.yaml.d/10-example.yml",
569                ))],
570                Some(ConfigOpenpgpSettings::new(
571                    Some(NumDataSignatures::new(
572                        NonZeroUsize::new(2).expect("2 is larger than 0"),
573                    )),
574                    ConfigVerificationMethod::TrustAnchor(ConfigTrustAnchorMode::new(
575                        Some(NumCertifications::new(
576                            NonZeroUsize::new(4).expect("4 is larger than 0"),
577                        )),
578                        Some(HashSet::from_iter([DomainName::from_str("example.org")?])),
579                        Some(HashSet::from_iter([
580                            OpenpgpFingerprint::from_str(
581                                "e242ed3bffccdf271b7fbaf34ed72d089537b42f",
582                            )?,
583                            OpenpgpFingerprint::from_str(
584                                "d3b0f7c0b825ecbb0f0d7398072947e7b1537b6f",
585                            )?,
586                            OpenpgpFingerprint::from_str(
587                                "b787a81c32997fd39a5f4c0188363902d3586e7b",
588                            )?,
589                            OpenpgpFingerprint::from_str(
590                                "6132b58967cf1ebc05062492c17145e5ee9f82a8",
591                            )?,
592                            OpenpgpFingerprint::from_str(
593                                "6eadeac2dade6347e87c0d24fd455feffa7069f0",
594                            )?,
595                        ])),
596                    )?),
597                )?),
598            )),
599            vec![
600                ConfigContextSettings::new(
601                    Purpose::from_str("package")?,
602                    Context::from_str("default")?,
603                    ConfigTechnologySettings::new(
604                        vec![ConfigOrigin::DropInFile(PathBuf::from(
605                            "/usr/share/voa/example.yaml.d/10-example.yml",
606                        ))],
607                        Some(ConfigOpenpgpSettings::new(
608                            Some(NumDataSignatures::new(
609                                NonZeroUsize::new(3).expect("3 is larger than 0"),
610                            )),
611                            ConfigVerificationMethod::TrustAnchor(ConfigTrustAnchorMode::new(
612                                Some(NumCertifications::new(
613                                    NonZeroUsize::new(5).expect("5 is larger than 0"),
614                                )),
615                                Some(HashSet::from_iter([DomainName::from_str(
616                                    "packages.example.org",
617                                )?])),
618                                Some(HashSet::from_iter([
619                                    OpenpgpFingerprint::from_str(
620                                        "e242ed3bffccdf271b7fbaf34ed72d089537b42f",
621                                    )?,
622                                    OpenpgpFingerprint::from_str(
623                                        "d3b0f7c0b825ecbb0f0d7398072947e7b1537b6f",
624                                    )?,
625                                    OpenpgpFingerprint::from_str(
626                                        "b787a81c32997fd39a5f4c0188363902d3586e7b",
627                                    )?,
628                                    OpenpgpFingerprint::from_str(
629                                        "6132b58967cf1ebc05062492c17145e5ee9f82a8",
630                                    )?,
631                                    OpenpgpFingerprint::from_str(
632                                        "6eadeac2dade6347e87c0d24fd455feffa7069f0",
633                                    )?,
634                                ])),
635                            )?),
636                        )?),
637                    ),
638                ),
639                ConfigContextSettings::new(
640                    Purpose::from_str("image")?,
641                    Context::from_str("installation-medium")?,
642                    ConfigTechnologySettings::new(
643                        vec![ConfigOrigin::DropInFile(PathBuf::from(
644                            "/usr/share/voa/example.yaml.d/10-example.yml",
645                        ))],
646                        Some(ConfigOpenpgpSettings::new(
647                            Some(NumDataSignatures::new(
648                                NonZeroUsize::new(1).expect("1 is larger than 0"),
649                            )),
650                            ConfigVerificationMethod::TrustAnchor(ConfigTrustAnchorMode::new(
651                                Some(NumCertifications::new(
652                                    NonZeroUsize::new(3).expect("3 is larger than 0"),
653                                )),
654                                Some(HashSet::from_iter([DomainName::from_str(
655                                    "packages.example.org",
656                                )?])),
657                                Some(HashSet::from_iter([
658                                    OpenpgpFingerprint::from_str(
659                                        "e242ed3bffccdf271b7fbaf34ed72d089537b42f",
660                                    )?,
661                                    OpenpgpFingerprint::from_str(
662                                        "d3b0f7c0b825ecbb0f0d7398072947e7b1537b6f",
663                                    )?,
664                                    OpenpgpFingerprint::from_str(
665                                        "b787a81c32997fd39a5f4c0188363902d3586e7b",
666                                    )?,
667                                    OpenpgpFingerprint::from_str(
668                                        "6132b58967cf1ebc05062492c17145e5ee9f82a8",
669                                    )?,
670                                    OpenpgpFingerprint::from_str(
671                                        "6eadeac2dade6347e87c0d24fd455feffa7069f0",
672                                    )?,
673                                ])),
674                            )?),
675                        )?),
676                    ),
677                ),
678            ],
679        )?;
680        let config_str = config.to_yaml_string()?;
681
682        with_settings!({
683            description => description,
684            snapshot_path => SNAPSHOT_PATH,
685            prepend_module_to_snapshot => false,
686        }, {
687            assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
688        });
689
690        Ok(())
691    }
692
693    /// Ensures that the string representation of [`ConfigFile`] remains
694    /// consistent when only providing context specific technology configurations.
695    #[test]
696    fn config_file_as_yaml_context_trust_anchor_openpgp() -> TestResult {
697        let description = "Configuration with no OS-level technology settings and one context-level technology settings using the 'trust anchor' verification method for OpenPGP.";
698        let config = ConfigFile::new(
699            None,
700            vec![ConfigContextSettings::new(
701                Purpose::from_str("package")?,
702                Context::from_str("my-repo")?,
703                ConfigTechnologySettings::new(
704                    vec![ConfigOrigin::DropInFile(PathBuf::from(
705                        "/usr/share/voa/example.yaml.d/10-example.yml",
706                    ))],
707                    Some(ConfigOpenpgpSettings::new(
708                        Some(NumDataSignatures::new(
709                            NonZeroUsize::new(3).expect("3 is larger than 0"),
710                        )),
711                        ConfigVerificationMethod::TrustAnchor(ConfigTrustAnchorMode::new(
712                            Some(NumCertifications::new(
713                                NonZeroUsize::new(5).expect("5 is larger than 0"),
714                            )),
715                            Some(HashSet::from_iter([DomainName::from_str(
716                                "packages.example.org",
717                            )?])),
718                            Some(HashSet::from_iter([
719                                OpenpgpFingerprint::from_str(
720                                    "e242ed3bffccdf271b7fbaf34ed72d089537b42f",
721                                )?,
722                                OpenpgpFingerprint::from_str(
723                                    "d3b0f7c0b825ecbb0f0d7398072947e7b1537b6f",
724                                )?,
725                                OpenpgpFingerprint::from_str(
726                                    "b787a81c32997fd39a5f4c0188363902d3586e7b",
727                                )?,
728                                OpenpgpFingerprint::from_str(
729                                    "6132b58967cf1ebc05062492c17145e5ee9f82a8",
730                                )?,
731                                OpenpgpFingerprint::from_str(
732                                    "6eadeac2dade6347e87c0d24fd455feffa7069f0",
733                                )?,
734                            ])),
735                        )?),
736                    )?),
737                ),
738            )],
739        )?;
740        let config_str = config.to_yaml_string()?;
741
742        with_settings!({
743            description => description,
744            snapshot_path => SNAPSHOT_PATH,
745            prepend_module_to_snapshot => false,
746        }, {
747            assert_snapshot!(current().name().expect("current thread should have a name").to_string().replace("::", "__"), config_str);
748        });
749
750        Ok(())
751    }
752}