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}