goose_eggs/
lib.rs

1//! Goose Eggs are helpful in writing [`Goose`](https://book.goose.rs/) load tests.
2//!
3//! ## Example
4//! The [Umami example](https://github.com/tag1consulting/goose/tree/main/examples/umami)
5//! included with Goose has been [converted to use the Goose Eggs library](https://github.com/tag1consulting/goose-eggs/tree/main/examples/umami)
6//! and serves as a useful example on how to leverage it when writing load tests.
7//!
8//! ## Feature flags
9//! * `default`: use the native TLS implementation for `goose` and `reqwest`
10//! * `rustls-tls`: use the TLS implemenation provided by `rustls`
11
12use goose::goose::GooseResponse;
13use goose::prelude::*;
14use http::Uri;
15use log::info;
16use regex::Regex;
17use reqwest::header::HeaderMap;
18
19pub mod drupal;
20pub mod text;
21
22/// Validate that the status code is equal or not equal to a specified value.
23#[derive(Clone, Debug)]
24struct ValidateStatus {
25    // Whether to validate that the status code is equal or not equal to the specified value.
26    equals: bool,
27    // Status code to validate
28    status_code: u16,
29}
30
31/// Validate that the page title is equal or not equal to a specified value.
32#[derive(Clone, Debug)]
33struct ValidateTitle<'a> {
34    // Whether to validate that the title contains or does not contain the specified value.
35    exists: bool,
36    // Title text to validate
37    title: &'a str,
38}
39
40/// Validate that the specified text exists or does not exist on the page.
41#[derive(Clone, Debug)]
42struct ValidateText<'a> {
43    // Whether to validate that the page contains or does not contain the specified text.
44    exists: bool,
45    // Text to validate
46    text: &'a str,
47}
48
49/// Validate that the specified header exists or does not exist, optionally containing a specified value.
50#[derive(Clone, Debug)]
51struct ValidateHeader<'a> {
52    // Whether to validate that the page contains or does not contain the specified header.
53    exists: bool,
54    // Header to validate
55    header: &'a str,
56    // Header value to validate
57    value: &'a str,
58}
59
60/// Define one or more items to be validated in a web page response. For complete
61/// documentation, refer to [`ValidateBuilder`].
62///
63/// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
64#[derive(Clone, Debug)]
65pub struct Validate<'a> {
66    /// Optionally validate the response status code.
67    status: Option<ValidateStatus>,
68    /// Optionally validate the response title.
69    title: Option<ValidateTitle<'a>>,
70    /// Optionally validate arbitrary texts in the response html.
71    texts: Vec<ValidateText<'a>>,
72    /// Optionally validate the response headers.
73    headers: Vec<ValidateHeader<'a>>,
74    /// Optionally validate whether or not the page redirects
75    redirect: Option<bool>,
76}
77impl<'a> Validate<'a> {
78    /// Convenience function to bring [`ValidateBuilder`] into scope.
79    pub fn builder() -> ValidateBuilder<'a> {
80        ValidateBuilder::new()
81    }
82
83    /// Create a [`Validate`] object that performs no validation.
84    ///
85    /// This is useful to load all static assets and return the body of the response.
86    ///
87    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
88    ///
89    /// # Example
90    /// ```rust
91    /// use goose_eggs::Validate;
92    ///
93    /// let _validate = Validate::none();
94    /// ```
95    pub fn none() -> Validate<'a> {
96        Validate::builder().build()
97    }
98}
99
100/// Used to build a [`Validate`] object, necessary to invoke the
101/// [`validate_page`] or [`validate_and_load_static_assets`] functions.
102///
103/// # Example
104/// ```rust
105/// use goose::prelude::*;
106/// use goose_eggs::{validate_and_load_static_assets, Validate};
107///
108/// transaction!(load_and_validate_page);
109///
110/// async fn load_and_validate_page(user: &mut GooseUser) -> TransactionResult {
111///     // Make a GET request.
112///     let mut goose = user.get("example/path").await?;
113///
114///     // Build a [`Validate`] object to confirm the response is valid.
115///     let validate = &Validate::builder()
116///         // Validate that the page has `Example` in the title.
117///         .title("Example")
118///         // Validate that the page has `foo` in the returned html body.
119///         .text("foo")
120///         // Validate that the page also has `<a href="bar">` in the returned
121///         // html body.
122///         .text(r#"<a href="bar">"#)
123///         .build();
124///
125///     // Perform the actual validation, using `?` to pass up the error if any
126///     // validation fails.
127///     validate_and_load_static_assets(
128///         user,
129///         goose,
130///         &validate,
131///     ).await?;
132///
133///     Ok(())
134/// }
135#[derive(Clone, Debug)]
136pub struct ValidateBuilder<'a> {
137    /// Optionally validate the response status code.
138    status: Option<ValidateStatus>,
139    /// Optionally validate the response title.
140    title: Option<ValidateTitle<'a>>,
141    /// Optionally validate arbitrary texts in the response html.
142    texts: Vec<ValidateText<'a>>,
143    /// Optionally validate the response headers.
144    headers: Vec<ValidateHeader<'a>>,
145    /// Optionally validate whether or not the page redirects
146    redirect: Option<bool>,
147}
148impl<'a> ValidateBuilder<'a> {
149    // Internally used when building to set defaults.
150    fn new() -> Self {
151        Self {
152            status: None,
153            title: None,
154            texts: vec![],
155            headers: vec![],
156            redirect: None,
157        }
158    }
159
160    /// Define the HTTP status expected to be returned when loading the page.
161    ///
162    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
163    ///
164    /// # Example
165    /// ```rust
166    /// use goose_eggs::Validate;
167    ///
168    /// let _validate = Validate::builder()
169    ///     .status(200)
170    ///     .build();
171    /// ```
172    pub fn status(mut self, status_code: u16) -> Self {
173        self.status = Some(ValidateStatus {
174            equals: true,
175            status_code,
176        });
177        self
178    }
179
180    /// Define an HTTP status not expected to be returned when loading the page.
181    ///
182    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
183    ///
184    /// # Example
185    /// ```rust
186    /// use goose_eggs::Validate;
187    ///
188    /// let _validate = Validate::builder()
189    ///     .not_status(404)
190    ///     .build();
191    /// ```
192    pub fn not_status(mut self, status_code: u16) -> Self {
193        self.status = Some(ValidateStatus {
194            equals: false,
195            status_code,
196        });
197        self
198    }
199
200    /// Create a [`Validate`] object to validate that response title contains the specified
201    /// text.
202    ///
203    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
204    ///
205    /// # Example
206    /// ```rust
207    /// use goose_eggs::Validate;
208    ///
209    /// let _validate = Validate::builder()
210    ///     .title("Home page")
211    ///     .build();
212    /// ```
213    pub fn title(mut self, title: impl Into<&'a str>) -> Self {
214        self.title = Some(ValidateTitle {
215            exists: true,
216            title: title.into(),
217        });
218        self
219    }
220
221    /// Create a [`Validate`] object to validate that response title does not contain the
222    /// specified text.
223    ///
224    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
225    ///
226    /// # Example
227    /// ```rust
228    /// use goose_eggs::Validate;
229    ///
230    /// let _validate = Validate::builder()
231    ///     .not_title("Home page")
232    ///     .build();
233    /// ```
234    pub fn not_title(mut self, title: impl Into<&'a str>) -> Self {
235        self.title = Some(ValidateTitle {
236            exists: false,
237            title: title.into(),
238        });
239        self
240    }
241
242    /// Create a [`Validate`] object to validate that the response page contains the specified
243    /// text.
244    ///
245    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
246    ///
247    /// # Example
248    /// ```rust
249    /// use goose_eggs::Validate;
250    ///
251    /// let _validate = Validate::builder()
252    ///     .text("example")
253    ///     .build();
254    /// ```
255    ///
256    /// It's possible to call this function multiple times to validate that multiple texts
257    /// appear on the page. Alternatively you can call [`ValidateBuilder::texts`].
258    ///
259    /// # Multiple Example
260    /// ```rust
261    /// use goose_eggs::Validate;
262    ///
263    /// let _validate = Validate::builder()
264    ///     .text("example")
265    ///     .text("another")
266    ///     .build();
267    /// ```
268    pub fn text(mut self, text: &'a str) -> Self {
269        self.texts.push(ValidateText { exists: true, text });
270        self
271    }
272
273    /// Create a [`Validate`] object to validate that the response page does not contain the
274    /// specified text.
275    ///
276    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
277    ///
278    /// # Example
279    /// ```rust
280    /// use goose_eggs::Validate;
281    ///
282    /// let _validate = Validate::builder()
283    ///     .not_text("example not on page")
284    ///     .build();
285    /// ```
286    ///
287    /// It's possible to call this function multiple times (and together with `text()`,
288    /// `texts()` and `not_texts()`) to validate that multiple texts do or do not appear
289    /// on the page. Alternatively you can call [`ValidateBuilder::texts`].
290    ///
291    /// # Multiple Example
292    /// ```rust
293    /// use goose_eggs::Validate;
294    ///
295    /// let _validate = Validate::builder()
296    ///     .not_text("example not on the page")
297    ///     .not_text("another not on the page")
298    ///     .text("this is on the page")
299    ///     .build();
300    /// ```
301    pub fn not_text(mut self, text: &'a str) -> Self {
302        self.texts.push(ValidateText {
303            exists: false,
304            text,
305        });
306        self
307    }
308
309    /// Create a [`Validate`] object to validate that the response page contains the specified
310    /// texts.
311    ///
312    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
313    ///
314    /// # Example
315    /// ```rust
316    /// use goose_eggs::Validate;
317    ///
318    /// let _validate = Validate::builder()
319    ///     .texts(vec!["example", "another"])
320    ///     .build();
321    /// ```
322    ///
323    /// It's possible to call this function multiple times (and together with `text()`, `not_text()`
324    /// and `not_texts()`) to validate that multiple texts do or do not appear on the page.
325    /// Alternatively you can call [`ValidateBuilder::texts`].
326    ///
327    /// # Example
328    /// ```rust
329    /// use goose_eggs::Validate;
330    ///
331    /// let _validate = Validate::builder()
332    ///     .texts(vec!["example", "another"])
333    ///     .not_texts(vec!["foo", "bar"])
334    ///     .texts(vec!["also this", "and this"])
335    ///     .build();
336    /// ```
337    ///
338    /// Alternatively you can call [`ValidateBuilder::text`].
339    pub fn texts(mut self, texts: Vec<&'a str>) -> Self {
340        for text in texts {
341            self = self.text(text);
342        }
343        self
344    }
345
346    /// Create a [`Validate`] object to validate that the response page does not contains the
347    /// specified texts.
348    ///
349    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
350    ///
351    /// # Example
352    /// ```rust
353    /// use goose_eggs::Validate;
354    ///
355    /// let _validate = Validate::builder()
356    ///     .not_texts(vec!["example", "another"])
357    ///     .build();
358    /// ```
359    ///
360    /// It's possible to call this function multiple times (and together with `text()`, `not_text()`
361    /// and `texts()`) to validate that multiple texts do or do not appear on the page.
362    /// Alternatively you can call [`ValidateBuilder::texts`].
363    ///
364    /// # Example
365    /// ```rust
366    /// use goose_eggs::Validate;
367    ///
368    /// let _validate = Validate::builder()
369    ///     .not_texts(vec!["example", "another"])
370    ///     .texts(vec!["does include foo", "and bar"])
371    ///     .not_texts(vec!["but not this", "or this"])
372    ///     .build();
373    /// ```
374    ///
375    /// Alternatively you can call [`ValidateBuilder::text`].
376    pub fn not_texts(mut self, texts: Vec<&'a str>) -> Self {
377        for text in texts {
378            self = self.not_text(text);
379        }
380        self
381    }
382
383    /// Create a [`Validate`] object to validate that the response includes the specified
384    /// header.
385    ///
386    /// To validate that a header contains a specific value (instead of just validating
387    /// that it exists), use [`ValidateBuilder::header_value`].
388    ///
389    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
390    ///
391    /// # Example
392    /// ```rust
393    /// use goose_eggs::Validate;
394    ///
395    /// let _validate = Validate::builder()
396    ///     .header("x-cache")
397    ///     .build();
398    /// ```
399    ///
400    /// It's possible to call this function multiple times, and/or together with
401    /// [`ValidateBuilder::not_header`], [`ValidateBuilder::header_value`] and
402    /// [`ValidateBuilder::not_header_value`].
403    ///
404    /// # Multiple Example
405    /// ```rust
406    /// use goose_eggs::Validate;
407    ///
408    /// let _validate = Validate::builder()
409    ///     .header("x-cache")
410    ///     .header("x-generator")
411    ///     .build();
412    /// ```
413    pub fn header(mut self, header: impl Into<&'a str>) -> Self {
414        self.headers.push(ValidateHeader {
415            exists: true,
416            header: header.into(),
417            value: "",
418        });
419        self
420    }
421
422    /// Create a [`Validate`] object to validate that the response does not include the
423    /// specified header.
424    ///
425    /// To validate that a header does not contain a specific value (instead of just validating
426    /// that it does not exist), use [`ValidateBuilder::not_header_value`].
427    ///
428    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
429    ///
430    /// # Example
431    /// ```rust
432    /// use goose_eggs::Validate;
433    ///
434    /// let _validate = Validate::builder()
435    ///     .not_header("x-cache")
436    ///     .build();
437    /// ```
438    ///
439    /// It's possible to call this function multiple times, and/or together with
440    /// [`ValidateBuilder::header`], [`ValidateBuilder::header_value`] and
441    /// [`ValidateBuilder::not_header_value`].
442    ///
443    /// # Multiple Example
444    /// ```rust
445    /// use goose_eggs::Validate;
446    ///
447    /// let _validate = Validate::builder()
448    ///     .not_header("x-cache")
449    ///     .header("x-generator")
450    ///     .build();
451    /// ```
452    pub fn not_header(mut self, header: impl Into<&'a str>) -> Self {
453        self.headers.push(ValidateHeader {
454            exists: false,
455            header: header.into(),
456            value: "",
457        });
458        self
459    }
460
461    /// Create a [`Validate`] object to validate that the response includes the specified
462    /// header which contains the specified value.
463    ///
464    /// To validate that a header simply exists without confirming that it contains a
465    /// specific value, use [`ValidateBuilder::header`].
466    ///
467    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
468    ///
469    /// # Example
470    /// ```rust
471    /// use goose_eggs::Validate;
472    ///
473    /// let _validate = Validate::builder()
474    ///     .header_value("x-generator", "Drupal 7")
475    ///     .build();
476    /// ```
477    ///
478    /// It's possible to call this function multiple times, and/or together with
479    /// [`ValidateBuilder::header`], [`ValidateBuilder::not_header`] and
480    /// [`ValidateBuilder::not_header_value`].
481    ///
482    /// # Multiple Example
483    /// ```rust
484    /// use goose_eggs::Validate;
485    ///
486    /// let _validate = Validate::builder()
487    ///     // Validate that the "x-cache" header is set.
488    ///     .header("x-cache")
489    ///     // Validate that the "x-generator" header is set and contains "Drupal 7".
490    ///     .header_value("x-generator", "Drupal-7")
491    ///     // Validate that the "x-drupal-cache" header is set and contains "HIT".
492    ///     .header_value("x-drupal-cache", "HIT")
493    ///     .build();
494    /// ```
495    pub fn header_value(mut self, header: impl Into<&'a str>, value: impl Into<&'a str>) -> Self {
496        self.headers.push(ValidateHeader {
497            exists: true,
498            header: header.into(),
499            value: value.into(),
500        });
501        self
502    }
503
504    /// Create a [`Validate`] object to validate that given header does not contain the specified
505    /// value.
506    ///
507    /// To validate that a header simply doesn't exist without confirming that it doesn't contain
508    /// a specific value, use [`ValidateBuilder::not_header`].
509    ///
510    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
511    ///
512    /// # Example
513    /// ```rust
514    /// use goose_eggs::Validate;
515    ///
516    /// let _validate = Validate::builder()
517    ///     .not_header_value("x-generator", "Drupal 7")
518    ///     .build();
519    /// ```
520    ///
521    /// It's possible to call this function multiple times, and/or together with
522    /// [`ValidateBuilder::header_value`], [`ValidateBuilder::not_header`] and
523    /// [`ValidateBuilder::header`].
524    ///
525    /// # Multiple Example
526    /// ```rust
527    /// use goose_eggs::Validate;
528    ///
529    /// let _validate = Validate::builder()
530    ///     // Validate that the "x-cache" header is set.
531    ///     .header("x-cache")
532    ///     // Validate that the "x-generator" header if set does not contain "Drupal 7".
533    ///     .not_header_value("x-generator", "Drupal-7")
534    ///     // Validate that the "x-drupal-cache" header is set to "HIT".
535    ///     .header_value("x-drupal-cache", "HIT")
536    ///     .build();
537    /// ```
538    pub fn not_header_value(
539        mut self,
540        header: impl Into<&'a str>,
541        value: impl Into<&'a str>,
542    ) -> Self {
543        self.headers.push(ValidateHeader {
544            exists: false,
545            header: header.into(),
546            value: value.into(),
547        });
548        self
549    }
550
551    /// Create a [`Validate`] object to validate whether or not the response page redirected.
552    ///
553    /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`].
554    ///
555    /// # Example
556    /// ```rust
557    /// use goose_eggs::Validate;
558    ///
559    /// // Verify the response redirected.
560    /// let _validate = Validate::builder().redirect(true).build();
561    ///
562    /// // Verify the response did not redirect.
563    /// let _validate = Validate::builder().redirect(false).build();
564    /// ```
565    pub fn redirect(mut self, redirect: impl Into<bool>) -> Self {
566        self.redirect = Some(redirect.into());
567        self
568    }
569
570    /// Build the [`Validate`] object which is then passed to the
571    /// [`validate_page`] or [`validate_and_load_static_assets`] functions.
572    ///
573    /// # Example
574    /// ```rust
575    /// use goose_eggs::Validate;
576    ///
577    /// // Use the default search form to search for `example keys`.
578    /// let _validate = Validate::builder()
579    ///     .text("example text")
580    ///     .build();
581    /// ```
582    pub fn build(self) -> Validate<'a> {
583        let Self {
584            status,
585            title,
586            texts,
587            headers,
588            redirect,
589        } = self;
590        Validate {
591            status,
592            title,
593            texts,
594            headers,
595            redirect,
596        }
597    }
598}
599
600/// Use a regular expression to get the HTML header from the web page.
601///
602/// # Example
603/// ```rust
604/// use goose_eggs::get_html_header;
605///
606/// // For this example we grab just a subset of a web page, enough to demonstrate. Normally
607/// // you'd use the entire html snippet returned from [`validate_page`] or
608/// // [`validate_and_load_static_assets`].
609/// let html = r#"
610/// <html lang="en" dir="ltr">
611///   <head>
612///     <meta charset="utf-8" />
613///     <link rel="canonical" href="https://example.com/" />
614///     <link rel="shortlink" href="https://example.com/" />
615///     <meta name="Generator" content="Drupal 9 (https://www.drupal.org)" />
616///     <meta name="MobileOptimized" content="width" />
617///     <meta name="HandheldFriendly" content="true" />
618///     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
619///     <title>Example Website</title>
620///   </head>
621/// <body>
622///   This is the web page body.
623/// </body>
624/// </html>
625/// "#;
626///
627/// let html_header = get_html_header(html);
628/// assert!(!html_header.is_none());
629/// ```
630pub fn get_html_header(html: &str) -> Option<String> {
631    let re = Regex::new(r#"<head(.*?)</head>"#).unwrap();
632    // Strip carriage returns to simplify regex.
633    let line = html.replace('\n', "");
634    // Return the entire html header, a subset of the received html.
635    re.captures(&line).map(|value| value[0].to_string())
636}
637
638/// Use a regular expression to get the web page title.
639///
640/// # Example
641/// ```rust
642/// use goose_eggs::{get_html_header, get_title};
643///
644/// // For this example we grab just a subset of a web page, enough to demonstrate. Normally
645/// // you'd use the entire html snippet returned from [`validate_page`] or
646/// // [`validate_and_load_static_assets`].
647/// let html = r#"
648/// <html lang="en" dir="ltr">
649///   <head>
650///     <meta charset="utf-8" />
651///     <link rel="canonical" href="https://example.com/" />
652///     <link rel="shortlink" href="https://example.com/" />
653///     <meta name="Generator" content="Drupal 9 (https://www.drupal.org)" />
654///     <meta name="MobileOptimized" content="width" />
655///     <meta name="HandheldFriendly" content="true" />
656///     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
657///     <title>Example Website</title>
658///   </head>
659/// <body>
660///   This is the web page body.
661/// </body>
662/// </html>
663/// "#;
664///
665/// // Start by extracting the HTML header from the HTML.
666/// let html_header = get_html_header(html).map_or_else(|| "".to_string(), |h| h.to_string());
667/// // Next extract the title from the HTML header.
668/// let title = get_title(&html_header).map_or_else(|| "".to_string(), |t| t.to_string());
669/// assert_eq!(title, "Example Website");
670/// ```
671pub fn get_title(html: &str) -> Option<String> {
672    let re = Regex::new(r#"<title>(.*?)</title>"#).unwrap();
673    // Strip carriage returns to simplify regex.
674    let line = html.replace('\n', "");
675    // Return the entire title, a subset of the received html.
676    re.captures(&line).map(|value| value[1].to_string())
677}
678
679/// Returns a [`bool`] indicating whether or not the title (case insensitive) on the
680/// webpage contains the provided string.
681///
682/// While you can invoke this function directly, it's generally preferred to invoke
683/// [`validate_page`] or [`validate_and_load_static_assets`] which in turn invoke this function.
684///
685/// A valid title is found between `<title></title>` tags inside `<head></head>` tags.
686/// For example, if the title is as follows:
687/// ```html
688/// <head>
689///   <title>this is the title</title>
690/// </head>
691/// ```
692///
693/// Then a call to `valid_title("the title")` will return [`true`], whereas a call to
694/// `valid_title("foo")` will return [`false`].
695///
696/// This function is case insensitive, so in the above example calling
697/// `valid_title("The Title")` and `valid_title("THE TITLE")` will both also return
698/// [`true`]. The function only tests if the title includes the specified text, the
699/// title can also include other text and will still be considered valid.
700///
701/// # Example
702/// ```rust
703/// use goose::prelude::*;
704/// use goose_eggs::valid_title;
705///
706/// transaction!(validate_title).set_on_start();
707///
708/// async fn validate_title(user: &mut GooseUser) -> TransactionResult {
709///     let mut goose = user.get("/").await?;
710///
711///     match goose.response {
712///         Ok(response) => {
713///             // Copy the headers so we have them for logging if there are errors.
714///             let headers = &response.headers().clone();
715///             match response.text().await {
716///                 Ok(html) => {
717///                     // Confirm that the HTML header includes the expected title.
718///                     let title = "example";
719///                     if !valid_title(&html, title) {
720///                         return user.set_failure(
721///                             &format!("{}: title not found: {}", goose.request.raw.url, title),
722///                             &mut goose.request,
723///                             Some(headers),
724///                             Some(&html),
725///                         );
726///                     }
727///                 }
728///                 Err(e) => {
729///                     return user.set_failure(
730///                         &format!("{}: failed to parse page: {}", goose.request.raw.url, e),
731///                         &mut goose.request,
732///                         Some(headers),
733///                         None,
734///                     );
735///                 }
736///             }
737///         }
738///         Err(e) => {
739///             return user.set_failure(
740///                 &format!("{}: no response from server: {}", goose.request.raw.url, e),
741///                 &mut goose.request,
742///                 None,
743///                 None,
744///             );
745///         }
746///     }
747///
748///     Ok(())
749/// }
750/// ```
751pub fn valid_title(html: &str, title: &str) -> bool {
752    // Extract the HTML header from the provided html.
753    let html_header = get_html_header(html).map_or_else(|| "".to_string(), |h| h);
754    // Next extract the title from the HTML header.
755    let html_title = get_title(&html_header).map_or_else(|| "".to_string(), |t| t);
756    // Finally, confirm that the title contains the expected text.
757    html_title
758        .to_ascii_lowercase()
759        .contains(title.to_ascii_lowercase().as_str())
760}
761
762/// Returns a [`bool`] indicating whether or not an arbitrary str (case sensitive) is found
763/// within the html.
764///
765/// Returns [`true`] if the expected str is found, otherwise returns [`false`].
766///
767/// This function is case sensitive, if the text "foo" is specified it will only match "foo",
768/// not "Foo" or "FOO".
769///
770/// While you can invoke this function directly, it's generally preferred to invoke
771/// [`validate_page`] or [`validate_and_load_static_assets`] which in turn invoke this function.
772///
773/// # Example
774/// ```rust
775/// use goose::prelude::*;
776/// use goose_eggs::valid_text;
777///
778/// transaction!(validate_text).set_on_start();
779///
780/// async fn validate_text(user: &mut GooseUser) -> TransactionResult {
781///     let mut goose = user.get("/").await?;
782///
783///     match goose.response {
784///         Ok(response) => {
785///             // Copy the headers so we have them for logging if there are errors.
786///             let headers = &response.headers().clone();
787///             match response.text().await {
788///                 Ok(html) => {
789///                     let text = r#"<code class="language-console">$ cargo new hello_world --bin"#;
790///                     if !valid_text(&html, text) {
791///                         return user.set_failure(
792///                             &format!("{}: text not found: {}", goose.request.raw.url, text),
793///                             &mut goose.request,
794///                             Some(headers),
795///                             Some(&html),
796///                         );
797///                     }
798///                 }
799///                 Err(e) => {
800///                     return user.set_failure(
801///                         &format!("{}: failed to parse page: {}", goose.request.raw.url, e),
802///                         &mut goose.request,
803///                         Some(headers),
804///                         None,
805///                     );
806///                 }
807///             }
808///         }
809///         Err(e) => {
810///             return user.set_failure(
811///                 &format!("{}: no response from server: {}", goose.request.raw.url, e),
812///                 &mut goose.request,
813///                 None,
814///                 None,
815///             );
816///         }
817///     }
818///
819///     Ok(())
820/// }
821/// ```
822pub fn valid_text(html: &str, text: &str) -> bool {
823    html.contains(text)
824}
825
826/// Returns a [`bool`] indicating whether or not a header was set in the server Response.
827///
828/// Returns [`true`] if the expected header was set, otherwise returns [`false`].
829///
830/// While you can invoke this function directly, it's generally preferred to invoke
831/// [`validate_page`] or [`validate_and_load_static_assets`] which in turn invoke this function.
832///
833/// # Example
834/// ```rust
835/// use goose::prelude::*;
836/// use goose_eggs::header_is_set;
837///
838/// transaction!(validate_header).set_on_start();
839///
840/// async fn validate_header(user: &mut GooseUser) -> TransactionResult {
841///     let mut goose = user.get("/").await?;
842///
843///     match goose.response {
844///         Ok(response) => {
845///             // Copy the headers so we have them for logging if there are errors.
846///             let headers = &response.headers().clone();
847///             if !header_is_set(headers, "server") {
848///                 return user.set_failure(
849///                     &format!("{}: header not found: {}", goose.request.raw.url, "server"),
850///                     &mut goose.request,
851///                     Some(headers),
852///                     None,
853///                 );
854///             }
855///         }
856///         Err(e) => {
857///             return user.set_failure(
858///                 &format!("{}: no response from server: {}", goose.request.raw.url, e),
859///                 &mut goose.request,
860///                 None,
861///                 None,
862///             );
863///         }
864///     }
865///
866///     Ok(())
867/// }
868/// ```
869pub fn header_is_set(headers: &HeaderMap, header: &str) -> bool {
870    headers.contains_key(header)
871}
872
873/// Returns a [`bool`] indicating whether or not a header contains an expected value.
874///
875/// Returns [`true`] if the expected value was found, otherwise returns [`false`].
876///
877/// Expects a [`&str`] [`tuple`] with a length of 2 where the first defines the header
878/// name and the second defines the header value, ie `("name", "value")`.
879///
880/// While you can invoke this function directly, it's generally preferred to invoke
881/// [`validate_page`] or [`validate_and_load_static_assets`] which in turn invoke this function.
882///
883/// # Example
884/// ```rust
885/// use goose::prelude::*;
886/// use goose_eggs::valid_header_value;
887///
888/// transaction!(validate_header_value).set_on_start();
889///
890/// async fn validate_header_value(user: &mut GooseUser) -> TransactionResult {
891///     let mut goose = user.get("/").await?;
892///
893///     match goose.response {
894///         Ok(response) => {
895///             // Copy the headers so we have them for logging if there are errors.
896///             let headers = &response.headers().clone();
897///             if !valid_header_value(headers, ("server", "nginx")) {
898///                 return user.set_failure(
899///                     &format!("{}: server header value not correct: {}", goose.request.raw.url, "nginx"),
900///                     &mut goose.request,
901///                     Some(headers),
902///                     None,
903///                 );
904///             }
905///         }
906///         Err(e) => {
907///             return user.set_failure(
908///                 &format!("{}: no response from server: {}", goose.request.raw.url, e),
909///                 &mut goose.request,
910///                 None,
911///                 None,
912///             );
913///         }
914///     }
915///
916///     Ok(())
917/// }
918/// ```
919pub fn valid_header_value<'a>(headers: &HeaderMap, header: (&'a str, &'a str)) -> bool {
920    // A header name is required, exit early if it's empty.
921    if header.0.is_empty() {
922        info!("no header specified");
923        return false;
924    }
925
926    if header_is_set(headers, header.0) {
927        if header.1.is_empty() {
928            false
929        } else {
930            let header_value = match headers.get(header.0) {
931                // Extract the value of the header and try to convert to a &str.
932                Some(v) => v.to_str().unwrap_or(""),
933                None => "",
934            };
935            // Check if the desired value is in the header.
936            if header_value.contains(header.1) {
937                true
938            } else {
939                // Provide some extra debug.
940                info!(
941                    r#"header does not contain expected value: "{}: {}""#,
942                    header.0, header.1
943                );
944                false
945            }
946        }
947    } else {
948        info!("header ({}) not set", header.0);
949        false
950    }
951}
952
953/// Helper to confirm the URI is valid and local.
954fn valid_local_uri(user: &mut GooseUser, uri: &str) -> bool {
955    match uri.parse::<Uri>() {
956        Ok(parsed_uri) => {
957            if let Some(parsed_host) = parsed_uri.host() {
958                if parsed_host == user.base_url.host_str().unwrap() {
959                    // The URI host matches the base_url.
960                    true
961                } else {
962                    // The URI host does not match the base_url.
963                    false
964                }
965            } else {
966                // The URI is a valid relative path.
967                true
968            }
969        }
970        Err(_) => {
971            let url_leading = format!("/{}", uri);
972            match url_leading.parse::<Uri>() {
973                Ok(_) => {
974                    // The URI is a valid relative path (without a leading slash).
975                    true
976                }
977                Err(_) => {
978                    // The URI is not valid.
979                    false
980                }
981            }
982        }
983    }
984}
985
986/// Extract all local static elements defined with a `src=` tag from the the provided html.
987///
988/// While you can invoke this function directly, it's generally preferred to invoke
989/// [`validate_and_load_static_assets`] which in turn invokes this function.
990pub async fn get_src_elements(user: &mut GooseUser, html: &str) -> Vec<String> {
991    // Use a case-insensitive regular expression to find all src=<foo> in the html, where
992    // <foo> is the URL to local image and js assets.
993    // @TODO: parse HTML5 srcset= also
994    let src_elements = Regex::new(r#"(?i)src="(.*?)""#).unwrap();
995    let mut elements: Vec<String> = Vec::new();
996    for url in src_elements.captures_iter(html_escape::decode_html_entities(html).as_ref()) {
997        if valid_local_uri(user, &url[1]) {
998            elements.push(url[1].to_string());
999        }
1000    }
1001    elements
1002}
1003
1004/// Extract all local css elements defined with a `href=` tag from the the provided html.
1005///
1006/// While you can invoke this function directly, it's generally preferred to invoke
1007/// [`validate_and_load_static_assets`] which in turn invokes this function.
1008pub async fn get_css_elements(user: &mut GooseUser, html: &str) -> Vec<String> {
1009    // Use a case-insensitive regular expression to find all href=<foo> in the html, where
1010    // <foo> is the URL to local css assets.
1011    let css = Regex::new(r#"(?i)href="(.*?\.css.*?)""#).unwrap();
1012    let mut elements: Vec<String> = Vec::new();
1013    for url in css.captures_iter(html_escape::decode_html_entities(html).as_ref()) {
1014        if valid_local_uri(user, &url[1]) {
1015            elements.push(url[1].to_string());
1016        }
1017    }
1018    elements
1019}
1020
1021/// Extract and load all local static elements from the the provided html.
1022///
1023/// While you can invoke this function directly, it's generally preferred to invoke
1024/// [`validate_and_load_static_assets`] which in turn invokes this function.
1025///
1026/// # Example
1027/// ```rust
1028/// use goose::prelude::*;
1029/// use goose_eggs::load_static_elements;
1030///
1031/// transaction!(load_page_and_static_elements).set_on_start();
1032///
1033/// async fn load_page_and_static_elements(user: &mut GooseUser) -> TransactionResult {
1034///     let mut goose = user.get("/").await?;
1035///
1036///     match goose.response {
1037///         Ok(response) => {
1038///             // Copy the headers so we have them for logging if there are errors.
1039///             let headers = &response.headers().clone();
1040///             match response.text().await {
1041///                 Ok(html) => {
1042///                     // Load all static elements on page.
1043///                     load_static_elements(user, &html).await;
1044///                 }
1045///                 Err(e) => {
1046///                     return user.set_failure(
1047///                         &format!("{}: failed to parse page: {}", goose.request.raw.url, e),
1048///                         &mut goose.request,
1049///                         Some(headers),
1050///                         None,
1051///                     );
1052///                 }
1053///             }
1054///         }
1055///         Err(e) => {
1056///             return user.set_failure(
1057///                 &format!("{}: no response from server: {}", goose.request.raw.url, e),
1058///                 &mut goose.request,
1059///                 None,
1060///                 None,
1061///             );
1062///         }
1063///     }
1064///
1065///     Ok(())
1066/// }
1067/// ```
1068pub async fn load_static_elements(user: &mut GooseUser, html: &str) {
1069    // Use a case-insensitive regular expression to find all src=<foo> in the html, where
1070    // <foo> is the URL to local image and js assets.
1071    // @TODO: parse HTML5 srcset= also
1072    for url in get_src_elements(user, html).await {
1073        let is_js = url.contains(".js");
1074        let resource_type = if is_js { "js" } else { "img" };
1075        let _ = user
1076            .get_named(&url, &("static asset: ".to_owned() + resource_type))
1077            .await;
1078    }
1079
1080    // Use a case-insensitive regular expression to find all href=<foo> in the html, where
1081    // <foo> is the URL to local css assets.
1082    for url in get_css_elements(user, html).await {
1083        let _ = user.get_named(&url, "static asset: css").await;
1084    }
1085}
1086
1087/// Validate the HTML response and return the HTML body.
1088///
1089/// What is validated is defined with the [`Validate`] structure.
1090///
1091/// If the page doesn't load, an empty [`String`] will be returned. If the page does load
1092/// but validation fails, an Error is returned. If the page loads and there are no
1093/// errors the body is returned as a [`String`].
1094///
1095/// This function is invoked by [validate_and_load_static_assets], which then also invokes
1096/// [load_static_elements] to better simulate a web browser loading a page.
1097///
1098/// # Example
1099/// ```rust
1100/// use goose::prelude::*;
1101/// use goose_eggs::{validate_page, Validate};
1102///
1103/// transaction!(load_page).set_on_start();
1104///
1105/// async fn load_page(user: &mut GooseUser) -> TransactionResult {
1106///     let mut goose = user.get("/").await?;
1107///     validate_page(
1108///         user,
1109///         goose,
1110///         // Validate title and other arbitrary text on the response html.
1111///         &Validate::builder()
1112///             .title("my page")
1113///             .texts(vec!["foo", r#"<a href="bar">"#])
1114///             .build(),
1115///     ).await?;
1116///
1117///     Ok(())
1118/// }
1119/// ```
1120pub async fn validate_page<'a>(
1121    user: &mut GooseUser,
1122    mut goose: GooseResponse,
1123    validate: &'a Validate<'a>,
1124) -> Result<String, Box<TransactionError>> {
1125    let empty = "".to_string();
1126    match goose.response {
1127        Ok(response) => {
1128            // Validate whether or not the request redirected.
1129            if let Some(redirect) = validate.redirect {
1130                if goose.request.redirected != redirect {
1131                    // Get as much as we can from the response for useful debug logging.
1132                    let headers = &response.headers().clone();
1133                    let html = response.text().await.unwrap_or_else(|_| "".to_string());
1134                    let error = if redirect {
1135                        format!("{}: did not redirect", goose.request.raw.url)
1136                    // Unexpected redirect happened.
1137                    } else {
1138                        format!("{}: redirected unexpectedly", goose.request.raw.url)
1139                    };
1140                    user.set_failure(&error, &mut goose.request, Some(headers), Some(&html))?;
1141                    // Exit as soon as validation fails, to avoid cascades of
1142                    // errors whe na page fails to load.
1143                    return Ok(html);
1144                }
1145            }
1146
1147            // Validate status code if defined.
1148            if let Some(validate_status) = validate.status.as_ref() {
1149                // If equals is false, error if response.status == status
1150                if !validate_status.equals && response.status() == validate_status.status_code {
1151                    // Get as much as we can from the response for useful debug logging.
1152                    let headers = &response.headers().clone();
1153                    let response_status = response.status();
1154                    let html = response.text().await.unwrap_or_else(|_| "".to_string());
1155                    user.set_failure(
1156                        &format!(
1157                            "{}: response status == {}]: {}",
1158                            goose.request.raw.url, validate_status.status_code, response_status
1159                        ),
1160                        &mut goose.request,
1161                        Some(headers),
1162                        Some(&html),
1163                    )?;
1164                    // Exit as soon as validation fails, to avoid cascades of
1165                    // errors whe na page fails to load.
1166                    return Ok(html);
1167                // If equals is true, error if response.status != status
1168                } else if validate_status.equals && response.status() != validate_status.status_code
1169                {
1170                    // Get as much as we can from the response for useful debug logging.
1171                    let headers = &response.headers().clone();
1172                    let response_status = response.status();
1173                    let html = response.text().await.unwrap_or_else(|_| "".to_string());
1174                    user.set_failure(
1175                        &format!(
1176                            "{}: response status != {}]: {}",
1177                            goose.request.raw.url, validate_status.status_code, response_status
1178                        ),
1179                        &mut goose.request,
1180                        Some(headers),
1181                        Some(&html),
1182                    )?;
1183                    // Exit as soon as validation fails, to avoid cascades of
1184                    // errors whe na page fails to load.
1185                    return Ok(html);
1186                }
1187            }
1188
1189            // Validate headers if defined.
1190            let headers = &response.headers().clone();
1191            for validate_header in &validate.headers {
1192                if !validate_header.exists {
1193                    if header_is_set(headers, validate_header.header) {
1194                        // Get as much as we can from the response for useful debug logging.
1195                        let html = response.text().await.unwrap_or_else(|_| "".to_string());
1196                        user.set_failure(
1197                            &format!(
1198                                "{}: header included in response: {:?}",
1199                                goose.request.raw.url, validate_header.header
1200                            ),
1201                            &mut goose.request,
1202                            Some(headers),
1203                            Some(&html),
1204                        )?;
1205                        // Exit as soon as validation fails, to avoid cascades of
1206                        // errors when a page fails to load.
1207                        return Ok(html);
1208                    }
1209                    if !validate_header.value.is_empty()
1210                        && valid_header_value(
1211                            headers,
1212                            (validate_header.header, validate_header.value),
1213                        )
1214                    {
1215                        // Get as much as we can from the response for useful debug logging.
1216                        let html = response.text().await.unwrap_or_else(|_| "".to_string());
1217                        user.set_failure(
1218                            &format!(
1219                                "{}: header contains unexpected value: {:?}",
1220                                goose.request.raw.url, validate_header.value
1221                            ),
1222                            &mut goose.request,
1223                            Some(headers),
1224                            Some(&html),
1225                        )?;
1226                        // Exit as soon as validation fails, to avoid cascades of
1227                        // errors when a page fails to load.
1228                        return Ok(html);
1229                    }
1230                } else {
1231                    if !header_is_set(headers, validate_header.header) {
1232                        // Get as much as we can from the response for useful debug logging.
1233                        let html = response.text().await.unwrap_or_else(|_| "".to_string());
1234                        user.set_failure(
1235                            &format!(
1236                                "{}: header not included in response: {:?}",
1237                                goose.request.raw.url, validate_header.header
1238                            ),
1239                            &mut goose.request,
1240                            Some(headers),
1241                            Some(&html),
1242                        )?;
1243                        // Exit as soon as validation fails, to avoid cascades of
1244                        // errors when a page fails to load.
1245                        return Ok(html);
1246                    }
1247                    if !validate_header.value.is_empty()
1248                        && !valid_header_value(
1249                            headers,
1250                            (validate_header.header, validate_header.value),
1251                        )
1252                    {
1253                        // Get as much as we can from the response for useful debug logging.
1254                        let html = response.text().await.unwrap_or_else(|_| "".to_string());
1255                        user.set_failure(
1256                            &format!(
1257                                "{}: header does not contain expected value: {:?}",
1258                                goose.request.raw.url, validate_header.value
1259                            ),
1260                            &mut goose.request,
1261                            Some(headers),
1262                            Some(&html),
1263                        )?;
1264                        // Exit as soon as validation fails, to avoid cascades of
1265                        // errors when a page fails to load.
1266                        return Ok(html);
1267                    }
1268                }
1269            }
1270
1271            // Extract the response body to validate and load static elements.
1272            match response.text().await {
1273                Ok(html) => {
1274                    // Validate title if defined.
1275                    if let Some(validate_title) = validate.title.as_ref() {
1276                        // Be sure the title doesn't contain the specified text.
1277                        if !validate_title.exists && valid_title(&html, validate_title.title) {
1278                            user.set_failure(
1279                                &format!(
1280                                    "{}: title found: {}",
1281                                    goose.request.raw.url, validate_title.title
1282                                ),
1283                                &mut goose.request,
1284                                Some(headers),
1285                                Some(&html),
1286                            )?;
1287                            // Exit as soon as validation fails, to avoid cascades of
1288                            // errors when a page fails to load.
1289                            return Ok(html);
1290                        // Be sure the title contains the specified text.
1291                        } else if validate_title.exists && !valid_title(&html, validate_title.title)
1292                        {
1293                            user.set_failure(
1294                                &format!(
1295                                    "{}: title not found: {}",
1296                                    goose.request.raw.url, validate_title.title
1297                                ),
1298                                &mut goose.request,
1299                                Some(headers),
1300                                Some(&html),
1301                            )?;
1302                            // Exit as soon as validation fails, to avoid cascades of
1303                            // errors when a page fails to load.
1304                            return Ok(html);
1305                        }
1306                    }
1307                    // Validate texts in body if defined.
1308                    for validate_text in &validate.texts {
1309                        if !validate_text.exists && valid_text(&html, validate_text.text) {
1310                            user.set_failure(
1311                                &format!(
1312                                    "{}: text found on page: {}",
1313                                    goose.request.raw.url, validate_text.text
1314                                ),
1315                                &mut goose.request,
1316                                Some(headers),
1317                                Some(&html),
1318                            )?;
1319                            // Exit as soon as validation fails, to avoid cascades of
1320                            // errors when a page fails to load.
1321                            return Ok(html);
1322                        } else if validate_text.exists && !valid_text(&html, validate_text.text) {
1323                            user.set_failure(
1324                                &format!(
1325                                    "{}: text not found on page: {}",
1326                                    goose.request.raw.url, validate_text.text
1327                                ),
1328                                &mut goose.request,
1329                                Some(headers),
1330                                Some(&html),
1331                            )?;
1332                            // Exit as soon as validation fails, to avoid cascades of
1333                            // errors when a page fails to load.
1334                            return Ok(html);
1335                        }
1336                    }
1337                    Ok(html)
1338                }
1339                Err(e) => {
1340                    user.set_failure(
1341                        &format!("{}: failed to parse page: {}", goose.request.raw.url, e),
1342                        &mut goose.request,
1343                        Some(headers),
1344                        None,
1345                    )?;
1346                    Ok(empty)
1347                }
1348            }
1349        }
1350        Err(e) => {
1351            user.set_failure(
1352                &format!("{}: no response from server: {}", goose.request.raw.url, e),
1353                &mut goose.request,
1354                None,
1355                None,
1356            )?;
1357            Ok(empty)
1358        }
1359    }
1360}
1361
1362/// Validate the HTML response, extract and load all static elements on the page, and
1363/// return the HTML body.
1364///
1365/// What is validated is defined with the [`Validate`] structure.
1366///
1367/// If the page doesn't load, an empty [`String`] will be returned. If the page does load
1368/// but validation fails, an Error is returned. If the page loads and there are no
1369/// errors the body is returned as a [`String`].
1370///
1371/// To only validate the page without also loading static elements, use instead
1372/// [validate_page].
1373///
1374/// # Example
1375/// ```rust
1376/// use goose::prelude::*;
1377/// use goose_eggs::{validate_and_load_static_assets, Validate};
1378///
1379/// transaction!(load_page).set_on_start();
1380///
1381/// async fn load_page(user: &mut GooseUser) -> TransactionResult {
1382///     let mut goose = user.get("/").await?;
1383///     validate_and_load_static_assets(
1384///         user,
1385///         goose,
1386///         // Validate title and other arbitrary text on the response html.
1387///         &Validate::builder()
1388///             .title("my page")
1389///             .texts(vec!["foo", r#"<a href="bar">"#])
1390///             .build(),
1391///     ).await?;
1392///
1393///     Ok(())
1394/// }
1395/// ```
1396pub async fn validate_and_load_static_assets<'a>(
1397    user: &mut GooseUser,
1398    goose: GooseResponse,
1399    validate: &'a Validate<'a>,
1400) -> Result<String, Box<TransactionError>> {
1401    match validate_page(user, goose, validate).await {
1402        Ok(html) => {
1403            load_static_elements(user, &html).await;
1404            Ok(html)
1405        }
1406        Err(e) => Err(e),
1407    }
1408}
1409
1410#[cfg(test)]
1411mod tests {
1412    use super::*;
1413    use goose::config::GooseConfiguration;
1414    use goose::goose::get_base_url;
1415    use gumdrop::Options;
1416
1417    const EMPTY_ARGS: Vec<&str> = vec![];
1418    const HOST: &str = "http://example.com";
1419
1420    #[tokio::test]
1421    async fn get_static_elements() {
1422        const HTML: &str = r#"<!DOCTYPE html>
1423        <html>
1424        <body>
1425            <!-- 4 valid CSS paths -->
1426                <!-- valid local http path including host -->
1427                <link href="http://example.com/example.css" rel="stylesheet" />
1428                <!-- valid local http path including host and query parameter -->
1429                <link href="http://example.com/example.css?version=abc123" rel="stylesheet" />
1430                <!-- invalid http path on different subdomain -->
1431                <link href="http://other.example.com/example.css" rel="stylesheet" />
1432                <!-- invalid http path on different domain -->
1433                <link href="http://other.com/example.css" rel="stylesheet" />
1434                <!-- invalid http path not ending in css -->
1435                <link href="http://example.com/example" rel="stylesheet" />
1436                <!-- valid relative path -->
1437                <link href="path/to/example.css" rel="stylesheet" />
1438                <!-- valid absolute path -->
1439                <link href="/path/to/example.css" rel="stylesheet" />
1440            
1441            <!-- 4 valid image paths -->
1442                <!-- valid local http path including host -->
1443                <img src="http://example.com/example.jpg" alt="example image" width="10" height="10"> 
1444                <!-- invalid http path on different subdomain -->
1445                <img src="http://another.example.com/example.jpg" alt="example image" width="10" height="10"> 
1446                <!-- invalid http path on different domain -->
1447                <img src="http://another.com/example.jpg" alt="example image" width="10" height="10"> 
1448                <!-- valid relative path -->
1449                <img src="path/to/example.gif" alt="example image" />
1450                <!-- valid absolute path -->
1451                <img src="/path/to/example.png" alt="example image" />
1452                <!-- valid absolute path with query parameter -->
1453                <img src="/path/to/example.jpg?itok=Q8u7GC4u" alt="example image" />
1454
1455            <!-- 3 valid JS paths -->
1456                <!-- valid local http path including host -->
1457                <script src="http://example.com/example.js"></script> 
1458                <!-- invalid http path on different subdomain -->
1459                <script src="http://different.example.com/example.js"></script> 
1460                <!-- valid relative path -->
1461                <script src="path/to/example.js"></script> 
1462                <!-- valid absolute path -->
1463                <script src="/path/to/example.js"></script> 
1464
1465        </body>
1466        </html>"#;
1467
1468        let configuration = GooseConfiguration::parse_args_default(&EMPTY_ARGS).unwrap();
1469        let base_url = get_base_url(Some(HOST.to_string()), None, None).unwrap();
1470        let mut user =
1471            GooseUser::new(0, "".to_string(), base_url, &configuration, 0, None).unwrap();
1472        let urls = get_css_elements(&mut user, HTML).await;
1473        if urls.len() != 4 {
1474            eprintln!(
1475                "expected matches: {:#?}",
1476                vec![
1477                    "http://example.com/example.css",
1478                    "http://example.com/example.css?version=abc123",
1479                    "path/to/example.css",
1480                    "/path/to/example.css",
1481                ]
1482            );
1483            eprintln!("actual matches: {:#?}", urls);
1484        }
1485        assert_eq!(urls.len(), 4);
1486
1487        let urls = get_src_elements(&mut user, HTML).await;
1488        if urls.len() != 7 {
1489            eprintln!(
1490                "expected matches: {:#?}",
1491                vec![
1492                    "http://example.com/example.jpg",
1493                    "path/to/example.gif",
1494                    "/path/to/example.png",
1495                    "/path/to/example.jpg?itok=Q8u7GC4u",
1496                    "http://example.com/example.js",
1497                    "path/to/example.js",
1498                    "/path/to/example.js",
1499                ]
1500            );
1501            eprintln!("actual matches: {:#?}", urls);
1502        }
1503        assert_eq!(urls.len(), 7);
1504    }
1505}