sectxtlib/
lib.rs

1mod fields;
2mod parse_error;
3mod parsers;
4mod pgpcleartextmessage;
5mod raw_field;
6mod securitytxt;
7mod securitytxt_options;
8
9pub use fields::{
10    AcknowledgmentsField, CanonicalField, ContactField, EncryptionField, ExpiresField, ExtensionField, HiringField,
11    PolicyField, PreferredLanguagesField,
12};
13pub use parse_error::ParseError;
14pub use securitytxt::SecurityTxt;
15pub use securitytxt_options::SecurityTxtOptions;
16
17#[cfg(test)]
18mod tests {
19    use super::*;
20    use chrono::{DateTime, Utc};
21    use std::{fs, path::PathBuf};
22
23    const URL: &str = "https://securitytxt.org/";
24    const INSECURE_URL: &str = "http://securitytxt.org/";
25    const EXPIRES: &str = "2030-01-01T08:19:03.000Z";
26
27    fn now_dt() -> DateTime<Utc> {
28        DateTime::parse_from_rfc3339("2023-01-01T08:19:03.000Z").unwrap().into()
29    }
30
31    fn expires_dt() -> ExpiresField {
32        ExpiresField::new(EXPIRES, now_dt()).unwrap()
33    }
34
35    fn get_parse_options() -> SecurityTxtOptions {
36        SecurityTxtOptions {
37            now: now_dt(),
38            strict: true,
39        }
40    }
41
42    fn get_tests_dir(category: &str) -> PathBuf {
43        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
44        d.push(format!("resources/test/{category}"));
45        d
46    }
47
48    #[test]
49    fn test_contact_and_expires() {
50        let file = format!("Contact: {URL}\nExpires: {EXPIRES}\n");
51        let sec = SecurityTxt {
52            acknowledgments: vec![],
53            canonical: vec![],
54            contact: vec![ContactField::new(URL).unwrap()],
55            encryption: vec![],
56            expires: expires_dt(),
57            extension: vec![],
58            hiring: vec![],
59            policy: vec![],
60            preferred_languages: None,
61        };
62
63        assert_eq!(file.parse(), Ok(sec));
64    }
65
66    #[test]
67    fn test_comment() {
68        let file = format!("# this is a comment\n#\nContact: {URL}\nExpires: {EXPIRES}\n#\n");
69        let sec = SecurityTxt {
70            acknowledgments: vec![],
71            canonical: vec![],
72            contact: vec![ContactField::new(URL).unwrap()],
73            encryption: vec![],
74            expires: expires_dt(),
75            extension: vec![],
76            hiring: vec![],
77            policy: vec![],
78            preferred_languages: None,
79        };
80
81        assert_eq!(file.parse(), Ok(sec));
82    }
83
84    #[test]
85    fn test_newlines() {
86        let file = format!("\n\n\nContact: {URL}\n\n\nExpires: {EXPIRES}\n\n\n");
87        let sec = SecurityTxt {
88            acknowledgments: vec![],
89            canonical: vec![],
90            contact: vec![ContactField::new(URL).unwrap()],
91            encryption: vec![],
92            expires: expires_dt(),
93            extension: vec![],
94            hiring: vec![],
95            policy: vec![],
96            preferred_languages: None,
97        };
98
99        assert_eq!(file.parse(), Ok(sec));
100    }
101
102    #[test]
103    fn test_acknowledgements() {
104        let file = format!("Contact: {URL}\nExpires: {EXPIRES}\nAcknowledgments: {URL}\n");
105        let sec = SecurityTxt {
106            acknowledgments: vec![AcknowledgmentsField::new(URL).unwrap()],
107            canonical: vec![],
108            contact: vec![ContactField::new(URL).unwrap()],
109            encryption: vec![],
110            expires: expires_dt(),
111            extension: vec![],
112            hiring: vec![],
113            policy: vec![],
114            preferred_languages: None,
115        };
116
117        assert_eq!(file.parse(), Ok(sec));
118    }
119
120    #[test]
121    fn test_contact_missing() {
122        let file = format!("Expires: {EXPIRES}\n");
123
124        assert_eq!(file.parse::<SecurityTxt>(), Err(ParseError::ContactFieldMissing));
125    }
126
127    #[test]
128    fn test_expires_missing() {
129        let file = format!("Contact: {URL}\n");
130
131        assert_eq!(file.parse::<SecurityTxt>(), Err(ParseError::ExpiresFieldMissing));
132    }
133
134    #[test]
135    fn test_trailing_content() {
136        let file = format!("Contact: {URL}\nExpires: {EXPIRES}\nfoo");
137
138        assert_eq!(file.parse::<SecurityTxt>(), Err(ParseError::Malformed));
139    }
140
141    #[test]
142    fn test_preferred_languages() {
143        let file = format!("Contact: {URL}\nExpires: {EXPIRES}\nPreferred-Languages: en, fr\n");
144        let sec = SecurityTxt {
145            acknowledgments: vec![],
146            canonical: vec![],
147            contact: vec![ContactField::new(URL).unwrap()],
148            encryption: vec![],
149            expires: expires_dt(),
150            extension: vec![],
151            hiring: vec![],
152            policy: vec![],
153            preferred_languages: Some(PreferredLanguagesField::new("en, fr").unwrap()),
154        };
155
156        assert_eq!(file.parse::<SecurityTxt>(), Ok(sec));
157    }
158
159    #[test]
160    fn test_preferred_languages_multiple() {
161        let file = format!("Contact: {URL}\nExpires: {EXPIRES}\nPreferred-Languages: en\nPreferred-Languages: de\n");
162
163        assert_eq!(
164            file.parse::<SecurityTxt>(),
165            Err(ParseError::PreferredLanguagesFieldMultiple)
166        );
167    }
168
169    #[test]
170    fn test_expires_multiple() {
171        let file = format!("Contact: {URL}\nExpires: {EXPIRES}\nExpires: {EXPIRES}\n");
172
173        assert_eq!(file.parse::<SecurityTxt>(), Err(ParseError::ExpiresFieldMultiple));
174    }
175
176    #[test]
177    fn test_insecure_http() {
178        let file = format!("Contact: {INSECURE_URL}\nExpires: {EXPIRES}\n");
179
180        assert_eq!(file.parse::<SecurityTxt>(), Err(ParseError::InsecureHTTP));
181    }
182
183    #[test]
184    fn test_signed_contact() {
185        let file = format!(
186            "-----BEGIN PGP SIGNED MESSAGE-----\r
187Hash: SHA256\r
188\r
189Contact: {URL}
190Contact: {URL}\r
191Expires: {EXPIRES}\r
192-----BEGIN PGP SIGNATURE-----\r
193Version: GnuPG v2.2\r
194\r
195abcdefABCDEF/+==\r
196-----END PGP SIGNATURE-----\r
197"
198        );
199        let sec = SecurityTxt {
200            acknowledgments: vec![],
201            canonical: vec![],
202            contact: vec![ContactField::new(URL).unwrap(), ContactField::new(URL).unwrap()],
203            encryption: vec![],
204            expires: expires_dt(),
205            extension: vec![],
206            hiring: vec![],
207            policy: vec![],
208            preferred_languages: None,
209        };
210
211        assert_eq!(file.parse(), Ok(sec));
212    }
213
214    fn _test_category(category: &str) {
215        let paths = get_tests_dir(category).read_dir().unwrap();
216
217        for path in paths {
218            let buf = fs::read_to_string(path.unwrap().path()).unwrap();
219            let parse_options = get_parse_options();
220            let txt = SecurityTxt::parse_with(&buf, &parse_options);
221            assert_eq!(txt.is_ok(), true);
222        }
223    }
224
225    #[test]
226    fn test_category_valid_unsigned() {
227        _test_category("valid_unsigned")
228    }
229
230    #[test]
231    fn test_category_valid_signed() {
232        _test_category("valid_signed")
233    }
234
235    #[test]
236    fn test_category_gen_unsigned() {
237        _test_category("gen_unsigned")
238    }
239}