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}