bitreq 0.3.5

Simple, minimal-dependency HTTP client
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
// Property-based tests for URL parsing based on WHATWG URL Standard
// https://url.spec.whatwg.org/

mod common;

use bitreq::Url;
use common::valid_url_strategy;
use proptest::prelude::*;

proptest! {
    /// Test that all generated valid URLs can be parsed successfully.
    #[test]
    fn valid_urls_parse_successfully(valid_url in valid_url_strategy()) {
        let result = Url::parse(&valid_url.url_string);
        prop_assert!(
            result.is_ok(),
            "Failed to parse valid URL: {} - Error: {:?}",
            valid_url.url_string,
            result.err()
        );
    }

    /// Test round-trip: as_str() returns the same string that was parsed.
    #[test]
    fn parse_as_str_roundtrip(valid_url in valid_url_strategy()) {
        let parsed = Url::parse(&valid_url.url_string).expect("should parse");
        prop_assert_eq!(
            parsed.as_str(),
            &valid_url.url_string,
            "Round-trip failed for URL"
        );
    }

    /// Test that scheme() returns the expected scheme.
    #[test]
    fn scheme_returns_expected_value(valid_url in valid_url_strategy()) {
        let parsed = Url::parse(&valid_url.url_string).expect("should parse");
        prop_assert_eq!(
            parsed.scheme(),
            &valid_url.scheme,
            "Scheme mismatch"
        );
    }

    /// Test that base_url() returns the expected host.
    #[test]
    fn base_url_returns_expected_value(valid_url in valid_url_strategy()) {
        let parsed = Url::parse(&valid_url.url_string).expect("should parse");
        prop_assert_eq!(
            parsed.base_url(),
            &valid_url.host,
            "Host mismatch"
        );
    }

    /// Test that port() returns the expected effective port.
    ///
    /// Note: port() returns u16 (the effective port), while valid_url.port is Option<u16>
    /// (the explicitly specified port). For known schemes without explicit port, the default
    /// port is returned. For unknown schemes without explicit port, this test is skipped.
    #[test]
    fn port_returns_expected_value(valid_url in valid_url_strategy()) {
        let parsed = Url::parse(&valid_url.url_string).expect("should parse");

        // Compute expected effective port based on explicit port or scheme default
        let expected_port = match valid_url.port {
            Some(p) => p,
            None => match valid_url.scheme.as_str() {
                "http" | "ws" => 80,
                "https" | "wss" => 443,
                "ftp" => 21,
                // Unknown scheme without explicit port - skip this test case
                _ => return Ok(()),
            }
        };

        prop_assert_eq!(
            parsed.port(),
            expected_port,
            "Port mismatch"
        );
    }

    /// Test that path() returns the expected path.
    #[test]
    fn path_returns_expected_value(valid_url in valid_url_strategy()) {
        let parsed = Url::parse(&valid_url.url_string).expect("should parse");
        let expected_path = if valid_url.path.is_empty() { "/" } else { &valid_url.path};
        prop_assert_eq!(
            parsed.path(),
            expected_path,
            "Path mismatch"
        );
    }

    /// Test that query() returns the expected query string.
    #[test]
    fn query_returns_expected_value(valid_url in valid_url_strategy()) {
        let parsed = Url::parse(&valid_url.url_string).expect("should parse");
        prop_assert_eq!(
            parsed.query(),
            valid_url.query.as_deref(),
            "Query mismatch"
        );
    }

    /// Test that fragment() returns the expected fragment.
    #[test]
    fn fragment_returns_expected_value(valid_url in valid_url_strategy()) {
        let parsed = Url::parse(&valid_url.url_string).expect("should parse");
        prop_assert_eq!(
            parsed.fragment(),
            valid_url.fragment.as_deref(),
            "Fragment mismatch"
        );
    }

    /// Test that path_segments() correctly splits the path, filtering empty segments.
    #[test]
    fn path_segments_splits_correctly(valid_url in valid_url_strategy()) {
        let parsed = Url::parse(&valid_url.url_string).expect("should parse");

        let path = &valid_url.path;
        let expected_segments: Vec<&str> = if path.is_empty() {
            vec![]
        } else if let Some(stripped) = path.strip_prefix('/') {
            stripped.split('/').filter(|s| !s.is_empty()).collect()
        } else {
            path.split('/').filter(|s| !s.is_empty()).collect()
        };

        let actual_segments: Vec<&str> = parsed.path_segments().collect();
        prop_assert_eq!(
            actual_segments,
            expected_segments,
            "Path segments mismatch"
        );
    }

    /// Test Display implementation matches as_str().
    #[test]
    fn display_matches_as_str(valid_url in valid_url_strategy()) {
        let parsed = Url::parse(&valid_url.url_string).expect("should parse");
        prop_assert_eq!(
            format!("{}", parsed),
            parsed.as_str(),
            "Display doesn't match as_str()"
        );
    }

    /// Test that parsing is deterministic - same input always produces same output.
    #[test]
    fn parsing_is_deterministic(valid_url in valid_url_strategy()) {
        let parsed1 = Url::parse(&valid_url.url_string).expect("should parse");
        let parsed2 = Url::parse(&valid_url.url_string).expect("should parse");
        prop_assert_eq!(parsed1, parsed2, "Parsing is not deterministic");
    }

    /// Test query_pairs() correctly parses query parameters, filtering empty keys.
    /// Note: query_pairs() now decodes percent-encoded values, but our test URLs
    /// don't contain percent-encoded characters, so decoded output matches input.
    #[test]
    fn query_pairs_parses_correctly(
        valid_url in valid_url_strategy()
            .prop_filter("has query", |u| u.query.is_some())
    ) {
        let parsed = Url::parse(&valid_url.url_string).expect("should parse");
        let query = valid_url.query.as_ref().unwrap();

        let expected_pairs: Vec<(String, String)> = query
            .split('&')
            .map(|pair| {
                let pair = pair.trim();
                if let Some(eq_pos) = pair.find('=') {
                    (pair[..eq_pos].trim().to_string(), pair[eq_pos + 1..].trim().to_string())
                } else {
                    (pair.trim().to_string(), String::new())
                }
            })
            .filter(|(k, _)| !k.is_empty())
            .collect();

        let actual_pairs: Vec<(String, String)> = parsed.query_pairs().collect();
        prop_assert_eq!(
            actual_pairs,
            expected_pairs,
            "Query pairs mismatch"
        );
    }
}

// Property tests for empty segment and empty key filtering behavior
proptest! {
    /// Test that path_segments never returns empty string segments.
    #[test]
    fn path_segments_never_returns_empty(valid_url in valid_url_strategy()) {
        let parsed = Url::parse(&valid_url.url_string).expect("should parse");
        let segments: Vec<&str> = parsed.path_segments().collect();

        for segment in &segments {
            prop_assert!(
                !segment.is_empty(),
                "path_segments() returned empty segment for URL: {}",
                valid_url.url_string
            );
        }
    }

    /// Test that query_pairs never returns pairs with empty keys.
    #[test]
    fn query_pairs_never_returns_empty_keys(valid_url in valid_url_strategy()) {
        let parsed = Url::parse(&valid_url.url_string).expect("should parse");
        let pairs: Vec<(String, String)> = parsed.query_pairs().collect();

        for (key, _) in &pairs {
            prop_assert!(
                !key.is_empty(),
                "query_pairs() returned empty key for URL: {}",
                valid_url.url_string
            );
        }
    }
}

// Additional edge case tests using proptest for schemes with special characters.
proptest! {
    #[test]
    fn scheme_with_special_chars_parses(
        base in "[a-z]",
        special in prop::sample::select(vec!['+', '-', '.']),
        suffix in "[a-z0-9]{0,3}",
        port in any::<u16>()
    ) {
        let scheme = format!("{}{}{}", base, special, suffix);
        // Unknown schemes require an explicit port
        let url_string = format!("{}://example.com:{}", scheme, port);
        let parsed = Url::parse(&url_string);
        prop_assert!(
            parsed.is_ok(),
            "Failed to parse URL with scheme '{}': {:?}",
            scheme,
            parsed.as_ref().err()
        );
        let parsed = parsed.unwrap();
        prop_assert_eq!(parsed.scheme(), scheme);
    }
}

// Tests for invalid and arbitrary input strings.
// These ensure the parser never panics and correctly rejects invalid input.
proptest! {
    /// Test that parsing arbitrary random strings never panics.
    /// The result can be Ok or Err, but it must not panic.
    #[test]
    fn arbitrary_strings_never_panic(s in ".*") {
        // This should never panic, regardless of what string is passed
        let _result = Url::parse(&s);
        // If we get here without panicking, the test passes
    }

    /// Test that parsing arbitrary byte sequences (as UTF-8 strings) never panics.
    #[test]
    fn arbitrary_bytes_as_string_never_panic(bytes in prop::collection::vec(any::<u8>(), 0..200)) {
        // Convert bytes to a string, lossy conversion for invalid UTF-8
        let s = String::from_utf8_lossy(&bytes);
        let _result = Url::parse(&s);
        // If we get here without panicking, the test passes
    }

    /// Test that empty string is rejected.
    #[test]
    fn empty_string_fails(s in prop::string::string_regex("").unwrap()) {
        let result = Url::parse(&s);
        prop_assert!(result.is_err(), "Empty string should fail to parse");
    }

    /// Test that strings without "://" separator are rejected.
    #[test]
    fn missing_scheme_separator_fails(
        scheme in "[a-z]{1,10}",
        rest in "[a-z0-9./-]{0,50}"
    ) {
        // Create a URL-like string but without "://"
        let invalid_url = format!("{}{}", scheme, rest);
        let result = Url::parse(&invalid_url);
        prop_assert!(
            result.is_err(),
            "URL without '://' should fail: {}",
            invalid_url
        );
    }

    /// Test that schemes starting with a digit are rejected.
    #[test]
    fn scheme_starting_with_digit_fails(
        digit in "[0-9]",
        rest in "[a-z0-9]{0,10}",
        host in "[a-z]{2,10}"
    ) {
        let invalid_url = format!("{}{}://{}", digit, rest, host);
        let result = Url::parse(&invalid_url);
        prop_assert!(
            result.is_err(),
            "Scheme starting with digit should fail: {}",
            invalid_url
        );
    }

    /// Test that schemes with invalid characters are rejected.
    #[test]
    fn scheme_with_invalid_chars_fails(
        prefix in "[a-z]{1,5}",
        invalid_char in prop::sample::select(vec!['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', ' ', '\t']),
        suffix in "[a-z]{0,5}",
        host in "[a-z]{2,10}"
    ) {
        let invalid_url = format!("{}{}{}://{}", prefix, invalid_char, suffix, host);
        let result = Url::parse(&invalid_url);
        prop_assert!(
            result.is_err(),
            "Scheme with invalid char '{}' should fail: {}",
            invalid_char,
            invalid_url
        );
    }

    /// Test that URLs with control characters are rejected.
    ///
    /// We skip cases with trailing whitespaces as they will be trimmed.
    #[test]
    fn control_characters_fail(
        prefix in "[a-z]{1,5}://[a-z]{2,10}",
        ctrl_char in 0u8..32u8,
        suffix in "[a-z]{0,10}"
    ) {
        if ctrl_char.is_ascii_whitespace() && suffix.is_empty() {
            return Ok(());
        }
        let invalid_url = format!("{}{}{}", prefix, ctrl_char as char, suffix);
        let result = Url::parse(&invalid_url);
        prop_assert!(
            result.is_err(),
            "URL with control char (0x{:02x}) should fail: {:?}",
            ctrl_char,
            invalid_url
        );
    }

    /// Test that URLs with non-ASCII characters are rejected.
    #[test]
    fn non_ascii_characters_fail(
        prefix in "[a-z]{1,5}://[a-z]{2,10}",
        non_ascii in 128u8..=255u8,
        suffix in "[a-z]{0,10}"
    ) {
        // Create a string with a non-ASCII byte
        let mut bytes = prefix.into_bytes();
        bytes.push(non_ascii);
        bytes.extend(suffix.bytes());
        let invalid_url = String::from_utf8_lossy(&bytes).into_owned();
        let result = Url::parse(&invalid_url);
        prop_assert!(
            result.is_err(),
            "URL with non-ASCII char should fail: {:?}",
            invalid_url
        );
    }

    /// Test that ports exceeding u16::MAX are rejected.
    #[test]
    fn port_overflow_fails(
        scheme in "[a-z]{1,5}",
        host in "[a-z]{2,10}",
        port in 65536u32..=999999u32
    ) {
        let invalid_url = format!("{}://{}:{}", scheme, host, port);
        let result = Url::parse(&invalid_url);
        prop_assert!(
            result.is_err(),
            "Port {} exceeds u16::MAX and should fail: {}",
            port,
            invalid_url
        );
    }

    /// Test that empty scheme (just "://") is rejected.
    #[test]
    fn empty_scheme_fails(host in "[a-z]{2,10}") {
        let invalid_url = format!("://{}", host);
        let result = Url::parse(&invalid_url);
        prop_assert!(
            result.is_err(),
            "Empty scheme should fail: {}",
            invalid_url
        );
    }

    /// Test various malformed URL patterns.
    #[test]
    fn malformed_url_patterns_fail(
        pattern in prop::sample::select(vec![
            "://example.com".to_string(),        // Empty scheme
            ":example.com".to_string(),          // Missing slashes
            "http//example.com".to_string(),     // Missing colon
            "http:/example.com".to_string(),     // Only one slash
            "http:example.com".to_string(),      // No slashes
            "123://example.com".to_string(),     // Scheme starts with digit
            "http ://example.com".to_string(),   // Space in scheme
            "ht tp://example.com".to_string(),   // Space in scheme
            "".to_string(),                      // Empty string
        ])
    ) {
        let result = Url::parse(&pattern);
        prop_assert!(
            result.is_err(),
            "Malformed pattern should fail: {:?}",
            pattern
        );
    }
}