From 92f872154a9f1a595315d7d90c57af80a05b8816 Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Tue, 19 Sep 2023 12:38:08 +0200 Subject: [PATCH 01/10] introduce not_status --- src/drupal.rs | 4 ++-- src/lib.rs | 46 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/drupal.rs b/src/drupal.rs index 78581d4..8485284 100644 --- a/src/drupal.rs +++ b/src/drupal.rs @@ -602,7 +602,7 @@ pub async fn log_in( // Build request manually if validating a specific status code. let goose_request = GooseRequest::builder() .path(login.url) - .expect_status_code(validate.status.unwrap()) + .expect_status_code(validate.status.unwrap().1) .build(); user.request(goose_request).await.unwrap() } else { @@ -680,7 +680,7 @@ pub async fn log_in( let goose_request = GooseRequest::builder() .path(login.url) .method(GooseMethod::Post) - .expect_status_code(validate.status.unwrap()) + .expect_status_code(validate.status.unwrap().1) .set_request_builder(reqwest_request_builder.form(¶ms)) .build(); user.request(goose_request).await.unwrap() diff --git a/src/lib.rs b/src/lib.rs index 1bddf59..27adee0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,7 @@ pub mod text; #[derive(Clone, Debug)] pub struct Validate<'a> { /// Optionally validate the response status code. - status: Option, + status: Option<(bool, u16)>, /// Optionally validate the response title. title: Option<&'a str>, /// Optionally validate arbitrary texts in the response html. @@ -97,7 +97,7 @@ impl<'a> Validate<'a> { #[derive(Clone, Debug)] pub struct ValidateBuilder<'a> { /// Optionally validate the response status code. - status: Option, + status: Option<(bool, u16)>, /// Optionally validate the response title. title: Option<&'a str>, /// Optionally validate arbitrary texts in the response html. @@ -132,7 +132,24 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` pub fn status(mut self, status: u16) -> Self { - self.status = Some(status); + self.status = Some((false, status)); + self + } + + /// Define an HTTP status not expected to be returned when loading the page. + /// + /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`]. + /// + /// # Example + /// ```rust + /// use goose_eggs::Validate; + /// + /// let _validate = Validate::builder() + /// .not_status(404) + /// .build(); + /// ``` + pub fn not_status(mut self, status: u16) -> Self { + self.status = Some((true, status)); self } @@ -875,8 +892,27 @@ pub async fn validate_page<'a>( } // Validate status code if defined. - if let Some(status) = validate.status { - if response.status() != status { + if let Some((inverse, status)) = validate.status { + // If inverse is true, error if response.status == status + if inverse && response.status() == status { + // Get as much as we can from the response for useful debug logging. + let headers = &response.headers().clone(); + let response_status = response.status(); + let html = response.text().await.unwrap_or_else(|_| "".to_string()); + user.set_failure( + &format!( + "{}: response status == {}]: {}", + goose.request.raw.url, status, response_status + ), + &mut goose.request, + Some(headers), + Some(&html), + )?; + // Exit as soon as validation fails, to avoid cascades of + // errors whe na page fails to load. + return Ok(html); + // If inverse is false, error if response.status != status + } else if !inverse && response.status() != status { // Get as much as we can from the response for useful debug logging. let headers = &response.headers().clone(); let response_status = response.status(); From 0d5da0da9855888a421cedd30c3defda029bc369 Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Tue, 19 Sep 2023 12:48:38 +0200 Subject: [PATCH 02/10] introduce not_title --- CHANGELOG.md | 1 + src/lib.rs | 38 +++++++++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b1d715..616e4a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.5.2-dev - match "http://example.com/example.css", "/path/to/example.css", and "path/to/example.css" formatted paths for all types of static assets + - introduce `not_status()`, `not_title()` ## 0.5.1 January 28, 2023 - in `drupal::log_in` diff --git a/src/lib.rs b/src/lib.rs index 27adee0..c02e4b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,7 @@ pub struct Validate<'a> { /// Optionally validate the response status code. status: Option<(bool, u16)>, /// Optionally validate the response title. - title: Option<&'a str>, + title: Option<(bool, &'a str)>, /// Optionally validate arbitrary texts in the response html. texts: Vec<&'a str>, /// Optionally validate the response headers. @@ -99,7 +99,7 @@ pub struct ValidateBuilder<'a> { /// Optionally validate the response status code. status: Option<(bool, u16)>, /// Optionally validate the response title. - title: Option<&'a str>, + title: Option<(bool, &'a str)>, /// Optionally validate arbitrary texts in the response html. texts: Vec<&'a str>, /// Optionally validate the response headers. @@ -167,7 +167,25 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` pub fn title(mut self, title: impl Into<&'a str>) -> Self { - self.title = Some(title.into()); + self.title = Some((false, title.into())); + self + } + + /// Create a [`Validate`] object to validate that response title does not contain the + /// specified text. + /// + /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`]. + /// + /// # Example + /// ```rust + /// use goose_eggs::Validate; + /// + /// let _validate = Validate::builder() + /// .not_title("Home page") + /// .build(); + /// ``` + pub fn not_title(mut self, title: impl Into<&'a str>) -> Self { + self.title = Some((true, title.into())); self } @@ -973,8 +991,18 @@ pub async fn validate_page<'a>( match response.text().await { Ok(html) => { // Validate title if defined. - if let Some(title) = validate.title { - if !valid_title(&html, title) { + if let Some((inverse, title)) = validate.title { + if inverse && valid_title(&html, title) { + user.set_failure( + &format!("{}: title found: {}", goose.request.raw.url, title), + &mut goose.request, + Some(headers), + Some(&html), + )?; + // Exit as soon as validation fails, to avoid cascades of + // errors when a page fails to load. + return Ok(html); + } else if !inverse && !valid_title(&html, title) { user.set_failure( &format!("{}: title not found: {}", goose.request.raw.url, title), &mut goose.request, From f342639df55ef053164c9999fa87a0932a66ae7a Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Tue, 19 Sep 2023 14:58:06 +0200 Subject: [PATCH 03/10] introduce not_text and not_texts --- CHANGELOG.md | 2 +- src/lib.rs | 109 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 104 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 616e4a1..c910980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.5.2-dev - match "http://example.com/example.css", "/path/to/example.css", and "path/to/example.css" formatted paths for all types of static assets - - introduce `not_status()`, `not_title()` + - introduce `not_status()`, `not_title()`, `not_text()`, `not_texts()` ## 0.5.1 January 28, 2023 - in `drupal::log_in` diff --git a/src/lib.rs b/src/lib.rs index c02e4b3..e5a03a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,7 @@ pub struct Validate<'a> { /// Optionally validate the response title. title: Option<(bool, &'a str)>, /// Optionally validate arbitrary texts in the response html. - texts: Vec<&'a str>, + texts: Vec<(bool, &'a str)>, /// Optionally validate the response headers. headers: Vec<(&'a str, &'a str)>, /// Optionally validate whether or not the page redirects @@ -101,7 +101,7 @@ pub struct ValidateBuilder<'a> { /// Optionally validate the response title. title: Option<(bool, &'a str)>, /// Optionally validate arbitrary texts in the response html. - texts: Vec<&'a str>, + texts: Vec<(bool, &'a str)>, /// Optionally validate the response headers. headers: Vec<(&'a str, &'a str)>, /// Optionally validate whether or not the page redirects @@ -216,7 +216,40 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` pub fn text(mut self, text: &'a str) -> Self { - self.texts.push(text); + self.texts.push((false, text)); + self + } + + /// Create a [`Validate`] object to validate that the response page does not contain the + /// specified text. + /// + /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`]. + /// + /// # Example + /// ```rust + /// use goose_eggs::Validate; + /// + /// let _validate = Validate::builder() + /// .not_text("example not on page") + /// .build(); + /// ``` + /// + /// It's possible to call this function multiple times (and together with `text()`, + /// `texts()` and `not_texts()`) to validate that multiple texts do or do not appear + /// on the page. Alternatively you can call [`ValidateBuilder::texts`]. + /// + /// # Multiple Example + /// ```rust + /// use goose_eggs::Validate; + /// + /// let _validate = Validate::builder() + /// .not_text("example not on the page") + /// .not_text("another not on the page") + /// .text("this is on the page") + /// .build(); + /// ``` + pub fn not_text(mut self, text: &'a str) -> Self { + self.texts.push((true, text)); self } @@ -234,9 +267,63 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` /// + /// It's possible to call this function multiple times (and together with `text()`, `not_text()` + /// and `not_texts()`) to validate that multiple texts do or do not appear on the page. + /// Alternatively you can call [`ValidateBuilder::texts`]. + /// + /// # Example + /// ```rust + /// use goose_eggs::Validate; + /// + /// let _validate = Validate::builder() + /// .texts(vec!["example", "another"]) + /// .not_texts(vec!["foo", "bar"]) + /// .texts(vec!["also this", "and this"]) + /// .build(); + /// ``` + /// /// Alternatively you can call [`ValidateBuilder::text`]. pub fn texts(mut self, texts: Vec<&'a str>) -> Self { - self.texts = texts; + for text in texts { + self = self.text(text); + } + self + } + + /// Create a [`Validate`] object to validate that the response page does not contains the + /// specified texts. + /// + /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`]. + /// + /// # Example + /// ```rust + /// use goose_eggs::Validate; + /// + /// let _validate = Validate::builder() + /// .not_texts(vec!["example", "another"]) + /// .build(); + /// ``` + /// + /// It's possible to call this function multiple times (and together with `text()`, `not_text()` + /// and `texts()`) to validate that multiple texts do or do not appear on the page. + /// Alternatively you can call [`ValidateBuilder::texts`]. + /// + /// # Example + /// ```rust + /// use goose_eggs::Validate; + /// + /// let _validate = Validate::builder() + /// .not_texts(vec!["example", "another"]) + /// .texts(vec!["does include foo", "and bar"]) + /// .not_texts(vec!["but not this", "or this"]) + /// .build(); + /// ``` + /// + /// Alternatively you can call [`ValidateBuilder::text`]. + pub fn not_texts(mut self, texts: Vec<&'a str>) -> Self { + for text in texts { + self = self.not_text(text); + } self } @@ -1015,8 +1102,18 @@ pub async fn validate_page<'a>( } } // Validate texts in body if defined. - for text in &validate.texts { - if !valid_text(&html, text) { + for (inverse, text) in &validate.texts { + if *inverse && valid_text(&html, text) { + user.set_failure( + &format!("{}: text found on page: {}", goose.request.raw.url, text), + &mut goose.request, + Some(headers), + Some(&html), + )?; + // Exit as soon as validation fails, to avoid cascades of + // errors when a page fails to load. + return Ok(html); + } else if !inverse && !valid_text(&html, text) { user.set_failure( &format!( "{}: text not found on page: {}", From 1d625cd2da96ca37aa30a7f378fd10cfd54ef235 Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Tue, 19 Sep 2023 15:26:44 +0200 Subject: [PATCH 04/10] introduce not_header and not_header_value --- CHANGELOG.md | 2 +- src/lib.rs | 194 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 155 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c910980..7b70020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.5.2-dev - match "http://example.com/example.css", "/path/to/example.css", and "path/to/example.css" formatted paths for all types of static assets - - introduce `not_status()`, `not_title()`, `not_text()`, `not_texts()` + - introduce `not_status()`, `not_title()`, `not_text()`, `not_texts()`, `not_header()`, and `not_header_value()` ## 0.5.1 January 28, 2023 - in `drupal::log_in` diff --git a/src/lib.rs b/src/lib.rs index e5a03a9..2bd421d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,7 @@ pub struct Validate<'a> { /// Optionally validate arbitrary texts in the response html. texts: Vec<(bool, &'a str)>, /// Optionally validate the response headers. - headers: Vec<(&'a str, &'a str)>, + headers: Vec<(bool, &'a str, &'a str)>, /// Optionally validate whether or not the page redirects redirect: Option, } @@ -103,7 +103,7 @@ pub struct ValidateBuilder<'a> { /// Optionally validate arbitrary texts in the response html. texts: Vec<(bool, &'a str)>, /// Optionally validate the response headers. - headers: Vec<(&'a str, &'a str)>, + headers: Vec<(bool, &'a str, &'a str)>, /// Optionally validate whether or not the page redirects redirect: Option, } @@ -344,8 +344,9 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` /// - /// It's possible to call this function multiple times to validate that multiple - /// headers are set. + /// It's possible to call this function multiple times, and/or together with + /// [`ValidateBuilder::not_header`], [`ValidateBuilder::header_value`] and + /// [`ValidateBuilder::not_header_value`]. /// /// # Multiple Example /// ```rust @@ -357,7 +358,42 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` pub fn header(mut self, header: impl Into<&'a str>) -> Self { - self.headers.push((header.into(), "")); + self.headers.push((false, header.into(), "")); + self + } + + /// Create a [`Validate`] object to validate that the response does not include the + /// specified header. + /// + /// To validate that a header does not contain a specific value (instead of just validating + /// that it does not exist), use [`ValidateBuilder::not_header_value`]. + /// + /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`]. + /// + /// # Example + /// ```rust + /// use goose_eggs::Validate; + /// + /// let _validate = Validate::builder() + /// .not_header("x-cache") + /// .build(); + /// ``` + /// + /// It's possible to call this function multiple times, and/or together with + /// [`ValidateBuilder::header`], [`ValidateBuilder::header_value`] and + /// [`ValidateBuilder::not_header_value`]. + /// + /// # Multiple Example + /// ```rust + /// use goose_eggs::Validate; + /// + /// let _validate = Validate::builder() + /// .not_header("x-cache") + /// .header("x-generator") + /// .build(); + /// ``` + pub fn not_header(mut self, header: impl Into<&'a str>) -> Self { + self.headers.push((true, header.into(), "")); self } @@ -379,8 +415,8 @@ impl<'a> ValidateBuilder<'a> { /// ``` /// /// It's possible to call this function multiple times, and/or together with - /// [`ValidateBuilder::header`] to validate that multiple headers are set and their - /// values. + /// [`ValidateBuilder::header`], [`ValidateBuilder::not_header`] and + /// [`ValidateBuilder::not_header_value`]. /// /// # Multiple Example /// ```rust @@ -396,7 +432,50 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` pub fn header_value(mut self, header: impl Into<&'a str>, value: impl Into<&'a str>) -> Self { - self.headers.push((header.into(), value.into())); + self.headers.push((false, header.into(), value.into())); + self + } + + /// Create a [`Validate`] object to validate that given header does not contain the specified + /// value. + /// + /// To validate that a header simply doesn't exist without confirming that it doesn't contain + /// a specific value, use [`ValidateBuilder::not_header`]. + /// + /// This structure is passed to [`validate_page`] or [`validate_and_load_static_assets`]. + /// + /// # Example + /// ```rust + /// use goose_eggs::Validate; + /// + /// let _validate = Validate::builder() + /// .not_header_value("x-generator", "Drupal 7") + /// .build(); + /// ``` + /// + /// It's possible to call this function multiple times, and/or together with + /// [`ValidateBuilder::header_value`], [`ValidateBuilder::not_header`] and + /// [`ValidateBuilder::header`]. + /// + /// # Multiple Example + /// ```rust + /// use goose_eggs::Validate; + /// + /// let _validate = Validate::builder() + /// // Validate that the "x-cache" header is set. + /// .header("x-cache") + /// // Validate that the "x-generator" header if set does not contain "Drupal 7". + /// .not_header_value("x-generator", "Drupal-7") + /// // Validate that the "x-drupal-cache" header is set to "HIT". + /// .header_value("x-drupal-cache", "HIT") + /// .build(); + /// ``` + pub fn not_header_value( + mut self, + header: impl Into<&'a str>, + value: impl Into<&'a str>, + ) -> Self { + self.headers.push((true, header.into(), value.into())); self } @@ -1039,38 +1118,73 @@ pub async fn validate_page<'a>( // Validate headers if defined. let headers = &response.headers().clone(); - for header in &validate.headers { - if !header_is_set(headers, header.0) { - // Get as much as we can from the response for useful debug logging. - let html = response.text().await.unwrap_or_else(|_| "".to_string()); - user.set_failure( - &format!( - "{}: header not included in response: {:?}", - goose.request.raw.url, header - ), - &mut goose.request, - Some(headers), - Some(&html), - )?; - // Exit as soon as validation fails, to avoid cascades of - // errors when a page fails to load. - return Ok(html); - } - if !header.1.is_empty() && !valid_header_value(headers, *header) { - // Get as much as we can from the response for useful debug logging. - let html = response.text().await.unwrap_or_else(|_| "".to_string()); - user.set_failure( - &format!( - "{}: header does not contain expected value: {:?}", - goose.request.raw.url, header.1 - ), - &mut goose.request, - Some(headers), - Some(&html), - )?; - // Exit as soon as validation fails, to avoid cascades of - // errors when a page fails to load. - return Ok(html); + for (inverse, header, value) in &validate.headers { + if *inverse { + if header_is_set(headers, header) { + // Get as much as we can from the response for useful debug logging. + let html = response.text().await.unwrap_or_else(|_| "".to_string()); + user.set_failure( + &format!( + "{}: header included in response: {:?}", + goose.request.raw.url, header + ), + &mut goose.request, + Some(headers), + Some(&html), + )?; + // Exit as soon as validation fails, to avoid cascades of + // errors when a page fails to load. + return Ok(html); + } + if !value.is_empty() && valid_header_value(headers, (*header, *value)) { + // Get as much as we can from the response for useful debug logging. + let html = response.text().await.unwrap_or_else(|_| "".to_string()); + user.set_failure( + &format!( + "{}: header contains unexpected value: {:?}", + goose.request.raw.url, value + ), + &mut goose.request, + Some(headers), + Some(&html), + )?; + // Exit as soon as validation fails, to avoid cascades of + // errors when a page fails to load. + return Ok(html); + } + } else { + if !header_is_set(headers, header) { + // Get as much as we can from the response for useful debug logging. + let html = response.text().await.unwrap_or_else(|_| "".to_string()); + user.set_failure( + &format!( + "{}: header not included in response: {:?}", + goose.request.raw.url, header + ), + &mut goose.request, + Some(headers), + Some(&html), + )?; + // Exit as soon as validation fails, to avoid cascades of + // errors when a page fails to load. + return Ok(html); + } + if !value.is_empty() && !valid_header_value(headers, (*header, *value)) { + // Get as much as we can from the response for useful debug logging. + let html = response.text().await.unwrap_or_else(|_| "".to_string()); + user.set_failure( + &format!( + "{}: header does not contain expected value: {:?}", + goose.request.raw.url, value + ), + &mut goose.request, + Some(headers), + Some(&html), + )?; + // Exit as soon as validation fails, to avoid cascades of + // errors when a page fails to load. + return Ok(html); + } } } From 013e6fa4ebd3eeb0dd9726157227ae7754095084 Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Mon, 25 Sep 2023 15:19:50 +0200 Subject: [PATCH 05/10] add test coverage --- Cargo.toml | 3 +- tests/validate.rs | 214 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 tests/validate.rs diff --git a/Cargo.toml b/Cargo.toml index e6a4b46..19044d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,4 +25,5 @@ default = ["goose/default", "reqwest/default-tls"] rustls-tls = ["goose/rustls-tls", "reqwest/rustls-tls"] [dev-dependencies] -gumdrop = "0.8" \ No newline at end of file +gumdrop = "0.8" +httpmock = "0.6" \ No newline at end of file diff --git a/tests/validate.rs b/tests/validate.rs new file mode 100644 index 0000000..2cd3d5f --- /dev/null +++ b/tests/validate.rs @@ -0,0 +1,214 @@ +use gumdrop::Options; +use httpmock::{Method::GET, MockServer}; + +use goose::config::GooseConfiguration; +use goose::prelude::*; + +// Paths used in load tests performed during these tests. +const PATH: &str = "/one"; + +const HTML: &str = r#" + + + Title 1234ABCD + + +

Test text on the page.

+ +"#; + +// Test transaction. +pub async fn get_path_valid(user: &mut GooseUser) -> TransactionResult { + let goose = user.get(PATH).await?; + goose_eggs::validate_and_load_static_assets( + user, + goose, + &goose_eggs::Validate::builder() + .title("1234ABCD") + .not_title("Example") + .text("Test text") + .text("") + .not_text("") + .header_value("foo", "bar") + .not_header("bar") + .build(), + ) + .await?; + + Ok(()) +} + +// Build appropriate configuration for these tests. +fn build_configuration(server: &MockServer) -> GooseConfiguration { + // Declare server_url so its lifetime is sufficient when needed. + let server_url = server.base_url(); + + // Common elements in all our tests. + let configuration = vec![ + "--users", + "1", + "--hatch-rate", + "4", + "--iterations", + "1", + "--host", + &server_url, + "--co-mitigation", + "disabled", + "--quiet", + ]; + + // Parse these options to generate a GooseConfiguration. + GooseConfiguration::parse_args_default(&configuration) + .expect("failed to parse options and generate a configuration") +} + +async fn run_load_test(server: &MockServer) -> GooseMetrics { + // Run the Goose Attack. + let goose_metrics = build_load_test( + build_configuration(server), + vec![scenario!("LoadTest").register_transaction(transaction!(get_path_valid))], + None, + None, + ) + .execute() + .await + .unwrap(); + + // Load test always launches 1 user and makes 1 request. + assert!(goose_metrics.total_users == 1); + // Provide debug if this fails. + if goose_metrics.requests.len() != 1 { + println!("EXPECTED ONE REQUEST: {:#?}", goose_metrics.requests); + } + assert!(goose_metrics.requests.len() == 1); + + goose_metrics +} + +// Create a GooseAttack object from the configuration, Scenarios, and optional start and +// stop Transactions. +#[allow(dead_code)] +pub fn build_load_test( + configuration: GooseConfiguration, + scenarios: Vec, + start_transaction: Option<&Transaction>, + stop_transaction: Option<&Transaction>, +) -> GooseAttack { + // First set up the common base configuration. + let mut goose = crate::GooseAttack::initialize_with_config(configuration).unwrap(); + + for scenario in scenarios { + goose = goose.register_scenario(scenario.clone()); + } + + if let Some(transaction) = start_transaction { + goose = goose.test_start(transaction.clone()); + } + + if let Some(transaction) = stop_transaction { + goose = goose.test_stop(transaction.clone()); + } + + goose +} + +#[tokio::test] +// Make a single request and validate everything. +async fn test_valid() { + // Start the mock server. + let server = MockServer::start(); + + let mock_endpoint = + // Set up PATH, store in vector at KEY_ONE. + server.mock(|when, then| { + when.method(GET).path(PATH); + then.status(200) + .header("foo", "bar") + .body(HTML); + }); + + let goose_metrics = run_load_test(&server).await; + assert!(mock_endpoint.hits() == 1); + + // Provide debug if this fails. + if !goose_metrics.errors.is_empty() { + println!("UNEXPECTED ERRORS: {:#?}", goose_metrics.errors); + } + assert!(goose_metrics.errors.is_empty()); +} + +#[tokio::test] +// Make a single request and confirm detection of invalid status code. +async fn test_invalid_status() { + // Start the mock server. + let server = MockServer::start(); + + let mock_endpoint = + // Set up PATH, store in vector at KEY_ONE. + server.mock(|when, then| { + when.method(GET).path(PATH); + then.status(404) + .header("foo", "bar") + .body(HTML); + }); + + let goose_metrics = run_load_test(&server).await; + assert!(mock_endpoint.hits() == 1); + + // Provide debug if this fails. + if goose_metrics.errors.len() != 1 { + println!("EXPECTED ONE ERRORS: {:#?}", goose_metrics.errors); + } + assert!(goose_metrics.errors.len() == 1); +} + +#[tokio::test] +// Make a single request and confirm detection of invalid header. +async fn test_invalid_header() { + // Start the mock server. + let server = MockServer::start(); + + let mock_endpoint = + // Set up PATH, store in vector at KEY_ONE. + server.mock(|when, then| { + when.method(GET).path(PATH); + then.status(200) + .header("bar", "foo") + .body(HTML); + }); + + let goose_metrics = run_load_test(&server).await; + assert!(mock_endpoint.hits() == 1); + + // Provide debug if this fails. + if goose_metrics.errors.len() != 1 { + println!("EXPECTED ONE ERRORS: {:#?}", goose_metrics.errors); + } + assert!(goose_metrics.errors.len() == 1); +} + +#[tokio::test] +// Make a single request and confirm detection of invalid header value. +async fn test_invalid_header_value() { + // Start the mock server. + let server = MockServer::start(); + + let mock_endpoint = + // Set up PATH, store in vector at KEY_ONE. + server.mock(|when, then| { + when.method(GET).path(PATH); + then.status(200) + .header("foo", "invalid") + .body(HTML); + }); + + let goose_metrics = run_load_test(&server).await; + assert!(mock_endpoint.hits() == 1); + + // Provide debug if this fails. + if goose_metrics.errors.len() != 1 { + println!("EXPECTED ONE ERRORS: {:#?}", goose_metrics.errors); + } + assert!(goose_metrics.errors.len() == 1); +} From 4cc48257839a6e7471d12089684433e840ed019e Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Mon, 30 Oct 2023 11:31:06 +0100 Subject: [PATCH 06/10] replace tuple with ValidateStatus --- src/drupal.rs | 8 ++++---- src/lib.rs | 42 +++++++++++++++++++++++++++++------------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/drupal.rs b/src/drupal.rs index 8485284..0a9ecf7 100644 --- a/src/drupal.rs +++ b/src/drupal.rs @@ -598,11 +598,11 @@ pub async fn log_in( }; // Load the log in page. - let goose = if validate.status.is_some() { + let goose = if let Some(validate_status) = validate.status.as_ref() { // Build request manually if validating a specific status code. let goose_request = GooseRequest::builder() .path(login.url) - .expect_status_code(validate.status.unwrap().1) + .expect_status_code(validate_status.status_code) .build(); user.request(goose_request).await.unwrap() } else { @@ -672,7 +672,7 @@ pub async fn log_in( ("op", &"Log+in".to_string()), ]; // Post the log in form. - let mut logged_in_user = if validate.status.is_some() { + let mut logged_in_user = if let Some(validate_status) = validate.status.as_ref() { // Build request manually if validating a specific status code. let url = user.build_url(login.url)?; // A request builder object is necessary to post a form. @@ -680,7 +680,7 @@ pub async fn log_in( let goose_request = GooseRequest::builder() .path(login.url) .method(GooseMethod::Post) - .expect_status_code(validate.status.unwrap().1) + .expect_status_code(validate_status.status_code) .set_request_builder(reqwest_request_builder.form(¶ms)) .build(); user.request(goose_request).await.unwrap() diff --git a/src/lib.rs b/src/lib.rs index 2bd421d..0f02097 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,15 @@ use reqwest::header::HeaderMap; pub mod drupal; pub mod text; +/// Validate that a status code is equal or not equal to a specified value. +#[derive(Clone, Debug)] +pub struct ValidateStatus { + // Whether to validate that the status code is equal or not equal to the specified valie. + equals: bool, + // Status code to validate + status_code: u16, +} + /// Define one or more items to be validated in a web page response. For complete /// documentation, refer to [`ValidateBuilder`]. /// @@ -26,7 +35,7 @@ pub mod text; #[derive(Clone, Debug)] pub struct Validate<'a> { /// Optionally validate the response status code. - status: Option<(bool, u16)>, + status: Option, /// Optionally validate the response title. title: Option<(bool, &'a str)>, /// Optionally validate arbitrary texts in the response html. @@ -97,7 +106,7 @@ impl<'a> Validate<'a> { #[derive(Clone, Debug)] pub struct ValidateBuilder<'a> { /// Optionally validate the response status code. - status: Option<(bool, u16)>, + status: Option, /// Optionally validate the response title. title: Option<(bool, &'a str)>, /// Optionally validate arbitrary texts in the response html. @@ -131,8 +140,11 @@ impl<'a> ValidateBuilder<'a> { /// .status(200) /// .build(); /// ``` - pub fn status(mut self, status: u16) -> Self { - self.status = Some((false, status)); + pub fn status(mut self, status_code: u16) -> Self { + self.status = Some(ValidateStatus { + equals: true, + status_code, + }); self } @@ -148,8 +160,11 @@ impl<'a> ValidateBuilder<'a> { /// .not_status(404) /// .build(); /// ``` - pub fn not_status(mut self, status: u16) -> Self { - self.status = Some((true, status)); + pub fn not_status(mut self, status_code: u16) -> Self { + self.status = Some(ValidateStatus { + equals: false, + status_code, + }); self } @@ -1076,9 +1091,9 @@ pub async fn validate_page<'a>( } // Validate status code if defined. - if let Some((inverse, status)) = validate.status { - // If inverse is true, error if response.status == status - if inverse && response.status() == status { + if let Some(validate_status) = validate.status.as_ref() { + // If equals is false, error if response.status == status + if !validate_status.equals && response.status() == validate_status.status_code { // Get as much as we can from the response for useful debug logging. let headers = &response.headers().clone(); let response_status = response.status(); @@ -1086,7 +1101,7 @@ pub async fn validate_page<'a>( user.set_failure( &format!( "{}: response status == {}]: {}", - goose.request.raw.url, status, response_status + goose.request.raw.url, validate_status.status_code, response_status ), &mut goose.request, Some(headers), @@ -1095,8 +1110,9 @@ pub async fn validate_page<'a>( // Exit as soon as validation fails, to avoid cascades of // errors whe na page fails to load. return Ok(html); - // If inverse is false, error if response.status != status - } else if !inverse && response.status() != status { + // If equals is true, error if response.status != status + } else if validate_status.equals && response.status() != validate_status.status_code + { // Get as much as we can from the response for useful debug logging. let headers = &response.headers().clone(); let response_status = response.status(); @@ -1104,7 +1120,7 @@ pub async fn validate_page<'a>( user.set_failure( &format!( "{}: response status != {}]: {}", - goose.request.raw.url, status, response_status + goose.request.raw.url, validate_status.status_code, response_status ), &mut goose.request, Some(headers), From fe37f6fdb008baaed445b1bee49cb11901ac07ec Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Mon, 30 Oct 2023 12:19:26 +0100 Subject: [PATCH 07/10] replace tuple with ValidateTitle --- src/lib.rs | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0f02097..2c7a9fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,7 @@ use reqwest::header::HeaderMap; pub mod drupal; pub mod text; -/// Validate that a status code is equal or not equal to a specified value. +/// Validate that the status code is equal or not equal to a specified value. #[derive(Clone, Debug)] pub struct ValidateStatus { // Whether to validate that the status code is equal or not equal to the specified valie. @@ -28,6 +28,15 @@ pub struct ValidateStatus { status_code: u16, } +/// Validate that the page title is equal or not equal to a specified value. +#[derive(Clone, Debug)] +pub struct ValidateTitle<'a> { + // Whether to validate that the status code is equal or not equal to the specified valie. + equals: bool, + // Status code to validate + title: &'a str, +} + /// Define one or more items to be validated in a web page response. For complete /// documentation, refer to [`ValidateBuilder`]. /// @@ -37,7 +46,7 @@ pub struct Validate<'a> { /// Optionally validate the response status code. status: Option, /// Optionally validate the response title. - title: Option<(bool, &'a str)>, + title: Option>, /// Optionally validate arbitrary texts in the response html. texts: Vec<(bool, &'a str)>, /// Optionally validate the response headers. @@ -108,7 +117,7 @@ pub struct ValidateBuilder<'a> { /// Optionally validate the response status code. status: Option, /// Optionally validate the response title. - title: Option<(bool, &'a str)>, + title: Option>, /// Optionally validate arbitrary texts in the response html. texts: Vec<(bool, &'a str)>, /// Optionally validate the response headers. @@ -182,7 +191,10 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` pub fn title(mut self, title: impl Into<&'a str>) -> Self { - self.title = Some((false, title.into())); + self.title = Some(ValidateTitle { + equals: true, + title: title.into(), + }); self } @@ -200,7 +212,10 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` pub fn not_title(mut self, title: impl Into<&'a str>) -> Self { - self.title = Some((true, title.into())); + self.title = Some(ValidateTitle { + equals: false, + title: title.into(), + }); self } @@ -1208,10 +1223,13 @@ pub async fn validate_page<'a>( match response.text().await { Ok(html) => { // Validate title if defined. - if let Some((inverse, title)) = validate.title { - if inverse && valid_title(&html, title) { + if let Some(validate_title) = validate.title.as_ref() { + if !validate_title.equals && valid_title(&html, validate_title.title) { user.set_failure( - &format!("{}: title found: {}", goose.request.raw.url, title), + &format!( + "{}: title found: {}", + goose.request.raw.url, validate_title.title + ), &mut goose.request, Some(headers), Some(&html), @@ -1219,9 +1237,13 @@ pub async fn validate_page<'a>( // Exit as soon as validation fails, to avoid cascades of // errors when a page fails to load. return Ok(html); - } else if !inverse && !valid_title(&html, title) { + } else if validate_title.equals && !valid_title(&html, validate_title.title) + { user.set_failure( - &format!("{}: title not found: {}", goose.request.raw.url, title), + &format!( + "{}: title not found: {}", + goose.request.raw.url, validate_title.title + ), &mut goose.request, Some(headers), Some(&html), From 255887e65bd363001aa5c0b380377828fd35fc68 Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Mon, 30 Oct 2023 12:37:04 +0100 Subject: [PATCH 08/10] replace tuple with ValidateText --- src/lib.rs | 51 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2c7a9fc..6a6c8d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,7 @@ pub mod text; /// Validate that the status code is equal or not equal to a specified value. #[derive(Clone, Debug)] pub struct ValidateStatus { - // Whether to validate that the status code is equal or not equal to the specified valie. + // Whether to validate that the status code is equal or not equal to the specified value. equals: bool, // Status code to validate status_code: u16, @@ -31,12 +31,21 @@ pub struct ValidateStatus { /// Validate that the page title is equal or not equal to a specified value. #[derive(Clone, Debug)] pub struct ValidateTitle<'a> { - // Whether to validate that the status code is equal or not equal to the specified valie. - equals: bool, - // Status code to validate + // Whether to validate that the title contains or does not contain the specified value. + exists: bool, + // Title text to validate title: &'a str, } +/// Validate that text on the page is equal or not equal to a specified value. +#[derive(Clone, Debug)] +pub struct ValidateText<'a> { + // Whether to validate that the page contains or does not contain the specified value. + exists: bool, + // Text to validate + text: &'a str, +} + /// Define one or more items to be validated in a web page response. For complete /// documentation, refer to [`ValidateBuilder`]. /// @@ -48,7 +57,7 @@ pub struct Validate<'a> { /// Optionally validate the response title. title: Option>, /// Optionally validate arbitrary texts in the response html. - texts: Vec<(bool, &'a str)>, + texts: Vec>, /// Optionally validate the response headers. headers: Vec<(bool, &'a str, &'a str)>, /// Optionally validate whether or not the page redirects @@ -119,7 +128,7 @@ pub struct ValidateBuilder<'a> { /// Optionally validate the response title. title: Option>, /// Optionally validate arbitrary texts in the response html. - texts: Vec<(bool, &'a str)>, + texts: Vec>, /// Optionally validate the response headers. headers: Vec<(bool, &'a str, &'a str)>, /// Optionally validate whether or not the page redirects @@ -192,7 +201,7 @@ impl<'a> ValidateBuilder<'a> { /// ``` pub fn title(mut self, title: impl Into<&'a str>) -> Self { self.title = Some(ValidateTitle { - equals: true, + exists: true, title: title.into(), }); self @@ -213,7 +222,7 @@ impl<'a> ValidateBuilder<'a> { /// ``` pub fn not_title(mut self, title: impl Into<&'a str>) -> Self { self.title = Some(ValidateTitle { - equals: false, + exists: false, title: title.into(), }); self @@ -246,7 +255,7 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` pub fn text(mut self, text: &'a str) -> Self { - self.texts.push((false, text)); + self.texts.push(ValidateText { exists: true, text }); self } @@ -279,7 +288,10 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` pub fn not_text(mut self, text: &'a str) -> Self { - self.texts.push((true, text)); + self.texts.push(ValidateText { + exists: false, + text, + }); self } @@ -1224,7 +1236,8 @@ pub async fn validate_page<'a>( Ok(html) => { // Validate title if defined. if let Some(validate_title) = validate.title.as_ref() { - if !validate_title.equals && valid_title(&html, validate_title.title) { + // Be sure the title doesn't contain the specified text. + if !validate_title.exists && valid_title(&html, validate_title.title) { user.set_failure( &format!( "{}: title found: {}", @@ -1237,7 +1250,8 @@ pub async fn validate_page<'a>( // Exit as soon as validation fails, to avoid cascades of // errors when a page fails to load. return Ok(html); - } else if validate_title.equals && !valid_title(&html, validate_title.title) + // Be sure the title contains the specified text. + } else if validate_title.exists && !valid_title(&html, validate_title.title) { user.set_failure( &format!( @@ -1254,10 +1268,13 @@ pub async fn validate_page<'a>( } } // Validate texts in body if defined. - for (inverse, text) in &validate.texts { - if *inverse && valid_text(&html, text) { + for validate_text in &validate.texts { + if !validate_text.exists && valid_text(&html, validate_text.text) { user.set_failure( - &format!("{}: text found on page: {}", goose.request.raw.url, text), + &format!( + "{}: text found on page: {}", + goose.request.raw.url, validate_text.text + ), &mut goose.request, Some(headers), Some(&html), @@ -1265,11 +1282,11 @@ pub async fn validate_page<'a>( // Exit as soon as validation fails, to avoid cascades of // errors when a page fails to load. return Ok(html); - } else if !inverse && !valid_text(&html, text) { + } else if validate_text.exists && !valid_text(&html, validate_text.text) { user.set_failure( &format!( "{}: text not found on page: {}", - goose.request.raw.url, text + goose.request.raw.url, validate_text.text ), &mut goose.request, Some(headers), From 02012c222cc2b10c53e27c061514b2a9929b8ba3 Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Mon, 30 Oct 2023 12:45:55 +0100 Subject: [PATCH 09/10] replace tuple with ValidateHeader --- src/lib.rs | 73 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6a6c8d0..9485a43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,15 +37,26 @@ pub struct ValidateTitle<'a> { title: &'a str, } -/// Validate that text on the page is equal or not equal to a specified value. +/// Validate that the specified text exists or does not exist on the page. #[derive(Clone, Debug)] pub struct ValidateText<'a> { - // Whether to validate that the page contains or does not contain the specified value. + // Whether to validate that the page contains or does not contain the specified text. exists: bool, // Text to validate text: &'a str, } +/// Validate that the specified header exists or does not exist, optionally containing a specified value. +#[derive(Clone, Debug)] +pub struct ValidateHeader<'a> { + // Whether to validate that the page contains or does not contain the specified header. + exists: bool, + // Header to validate + header: &'a str, + // Header value to validate + value: &'a str, +} + /// Define one or more items to be validated in a web page response. For complete /// documentation, refer to [`ValidateBuilder`]. /// @@ -59,7 +70,7 @@ pub struct Validate<'a> { /// Optionally validate arbitrary texts in the response html. texts: Vec>, /// Optionally validate the response headers. - headers: Vec<(bool, &'a str, &'a str)>, + headers: Vec>, /// Optionally validate whether or not the page redirects redirect: Option, } @@ -130,7 +141,7 @@ pub struct ValidateBuilder<'a> { /// Optionally validate arbitrary texts in the response html. texts: Vec>, /// Optionally validate the response headers. - headers: Vec<(bool, &'a str, &'a str)>, + headers: Vec>, /// Optionally validate whether or not the page redirects redirect: Option, } @@ -400,7 +411,11 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` pub fn header(mut self, header: impl Into<&'a str>) -> Self { - self.headers.push((false, header.into(), "")); + self.headers.push(ValidateHeader { + exists: true, + header: header.into(), + value: "", + }); self } @@ -435,7 +450,11 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` pub fn not_header(mut self, header: impl Into<&'a str>) -> Self { - self.headers.push((true, header.into(), "")); + self.headers.push(ValidateHeader { + exists: false, + header: header.into(), + value: "", + }); self } @@ -474,7 +493,11 @@ impl<'a> ValidateBuilder<'a> { /// .build(); /// ``` pub fn header_value(mut self, header: impl Into<&'a str>, value: impl Into<&'a str>) -> Self { - self.headers.push((false, header.into(), value.into())); + self.headers.push(ValidateHeader { + exists: true, + header: header.into(), + value: value.into(), + }); self } @@ -517,7 +540,11 @@ impl<'a> ValidateBuilder<'a> { header: impl Into<&'a str>, value: impl Into<&'a str>, ) -> Self { - self.headers.push((true, header.into(), value.into())); + self.headers.push(ValidateHeader { + exists: false, + header: header.into(), + value: value.into(), + }); self } @@ -1161,15 +1188,15 @@ pub async fn validate_page<'a>( // Validate headers if defined. let headers = &response.headers().clone(); - for (inverse, header, value) in &validate.headers { - if *inverse { - if header_is_set(headers, header) { + for validate_header in &validate.headers { + if !validate_header.exists { + if header_is_set(headers, validate_header.header) { // Get as much as we can from the response for useful debug logging. let html = response.text().await.unwrap_or_else(|_| "".to_string()); user.set_failure( &format!( "{}: header included in response: {:?}", - goose.request.raw.url, header + goose.request.raw.url, validate_header.header ), &mut goose.request, Some(headers), @@ -1179,13 +1206,18 @@ pub async fn validate_page<'a>( // errors when a page fails to load. return Ok(html); } - if !value.is_empty() && valid_header_value(headers, (*header, *value)) { + if !validate_header.value.is_empty() + && valid_header_value( + headers, + (validate_header.header, validate_header.value), + ) + { // Get as much as we can from the response for useful debug logging. let html = response.text().await.unwrap_or_else(|_| "".to_string()); user.set_failure( &format!( "{}: header contains unexpected value: {:?}", - goose.request.raw.url, value + goose.request.raw.url, validate_header.value ), &mut goose.request, Some(headers), @@ -1196,13 +1228,13 @@ pub async fn validate_page<'a>( return Ok(html); } } else { - if !header_is_set(headers, header) { + if !header_is_set(headers, validate_header.header) { // Get as much as we can from the response for useful debug logging. let html = response.text().await.unwrap_or_else(|_| "".to_string()); user.set_failure( &format!( "{}: header not included in response: {:?}", - goose.request.raw.url, header + goose.request.raw.url, validate_header.header ), &mut goose.request, Some(headers), @@ -1212,13 +1244,18 @@ pub async fn validate_page<'a>( // errors when a page fails to load. return Ok(html); } - if !value.is_empty() && !valid_header_value(headers, (*header, *value)) { + if !validate_header.value.is_empty() + && !valid_header_value( + headers, + (validate_header.header, validate_header.value), + ) + { // Get as much as we can from the response for useful debug logging. let html = response.text().await.unwrap_or_else(|_| "".to_string()); user.set_failure( &format!( "{}: header does not contain expected value: {:?}", - goose.request.raw.url, value + goose.request.raw.url, validate_header.value ), &mut goose.request, Some(headers), From 73c13a3e023215e210947f033f14dd7184150906 Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Mon, 30 Oct 2023 12:47:05 +0100 Subject: [PATCH 10/10] don't make internal structs public --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9485a43..c33c7a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ pub mod text; /// Validate that the status code is equal or not equal to a specified value. #[derive(Clone, Debug)] -pub struct ValidateStatus { +struct ValidateStatus { // Whether to validate that the status code is equal or not equal to the specified value. equals: bool, // Status code to validate @@ -30,7 +30,7 @@ pub struct ValidateStatus { /// Validate that the page title is equal or not equal to a specified value. #[derive(Clone, Debug)] -pub struct ValidateTitle<'a> { +struct ValidateTitle<'a> { // Whether to validate that the title contains or does not contain the specified value. exists: bool, // Title text to validate @@ -39,7 +39,7 @@ pub struct ValidateTitle<'a> { /// Validate that the specified text exists or does not exist on the page. #[derive(Clone, Debug)] -pub struct ValidateText<'a> { +struct ValidateText<'a> { // Whether to validate that the page contains or does not contain the specified text. exists: bool, // Text to validate @@ -48,7 +48,7 @@ pub struct ValidateText<'a> { /// Validate that the specified header exists or does not exist, optionally containing a specified value. #[derive(Clone, Debug)] -pub struct ValidateHeader<'a> { +struct ValidateHeader<'a> { // Whether to validate that the page contains or does not contain the specified header. exists: bool, // Header to validate