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..1833a04
--- /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.len() > 0 {
+ println!("UNEXPECTED ERRORS: {:#?}", goose_metrics.errors);
+ }
+ assert!(goose_metrics.errors.len() == 0);
+}
+
+#[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);
+}