diff --git a/examples/umami/README.md b/examples/umami/README.md index 25b8de3d..5528c2dd 100644 --- a/examples/umami/README.md +++ b/examples/umami/README.md @@ -13,16 +13,20 @@ By default it will try and load pages from https://drupal-9.0.7.ddev.site/. The load test is split into the following files: - `main.rs`: This file contains the main() function and defines the actual load test; - `common.rs`: This file contains helper functions used by the task functions; - - `english.rs`: This files contains all task functions loading pages in English; - - `spanish.rs`: This files contains all task functions loading pages in Spanish. + - `english.rs`: This file contains all task functions loading pages in English; + - `spanish.rs`: This file contains all task functions loading pages in Spanish; + - `admin.rs`: This file contains all task functions specific to simulating an admin user. ## Load Test Features The load test defines the following users: - - Anonymous English user: this user performs all tasks in English, it runs 3 times as often as the Spanish user; - - Anonymous Spanish user: this user performs all tasks in Spanish, it runs 1/3 as often as the English user. + - Anonymous English user: this user performs all tasks in English, it has a weight of 40, and randomly pauses for 0 to 3 seconds after each task; + - Anonymous Spanish user: this user performs all tasks in Spanish, it has a weight of 9, and randomly pauses for 0 to 3 seconds after each task; + - Admin user: this user logs into the website, it has a weight of 1, and randomly pauses for 3 to 10 seconds after each task. -Each load test user runs the following tasks in their own language, and also loads all static elements on any pages loaded: +Due to user weighting, the load test should simulate at least 50 users when it runs. If you simulate 100 users (with the `-u 100` run time option) then 80 anonymous English users, 18 anonymous Spanish users, and 2 admin users will be simulated. + +Each anonymous load test user runs the following tasks in their own language, and also loads all static elements on any pages loaded: - Loads the front page; - Loads a "basic page"; - Loads the article listing page; @@ -32,4 +36,17 @@ Each load test user runs the following tasks in their own language, and also loa - Loads a random node by nid; - Loads the term listing page filtered by a random term; - Performs a search using a random word from a random node's title; - - Submits website feedback through the contact form; + - Submits website feedback through the contact form. + +Each admin load test user logs in one time in English, and then runs the following tasks and also loads all static elements on any pages loaded: + - Loads the front page; + - Loads the article listing page; + - Loads an "article", edits (not making any actual changes), and saves it (flushing all caches). + + ## Configuring The Admin User + + The load test needs to know what username and password to use to log in. By default it will attempt to log in with the username `admin` and the password `P@ssw0rd1234`. However, you can use the ADMIN_USERNAME and/or ADMIN_PASSWORD environment variables to log in with different values. In the following example, the load test will attempt to log in with the username `foo` and the password `bar`: + + ``` + ADMIN_USERNAME=foo ADMIN_PASSWORD=bar cargo run --release --example umami -- -H https://drupal-9.0.7.ddev.site -v -u50 + ``` diff --git a/examples/umami/admin.rs b/examples/umami/admin.rs new file mode 100644 index 00000000..f7936f0e --- /dev/null +++ b/examples/umami/admin.rs @@ -0,0 +1,210 @@ +use goose::prelude::*; + +use crate::common; + +use rand::seq::SliceRandom; +use std::env; + +/// Log into the website. +pub async fn log_in(user: &GooseUser) -> GooseTaskResult { + // Use ADMIN_USERNAME= to set custom admin username. + let admin_username = match env::var("ADMIN_USERNAME") { + Ok(username) => username, + Err(_) => "admin".to_string(), + }; + // Use ADMIN_PASSWORD= to set custom admin username. + let admin_password = match env::var("ADMIN_PASSWORD") { + Ok(password) => password, + Err(_) => "P@ssw0rd1234".to_string(), + }; + + // Load the log in page. + let mut goose = user.get("/en/user/login").await?; + + // We can't invoke common::validate_and_load_static_assets as while it's important + // to validate the page and load static elements, we then need to extract form elements + // from the HTML of the page. So we duplicate some of the logic, enhancing it for form + // processing. + let mut logged_in_user; + match goose.response { + Ok(response) => { + // Copy the headers so we have them for logging if there are errors. + let headers = &response.headers().clone(); + match response.text().await { + Ok(html) => { + // Be sure we've properly loaded the log in page. + let title = "Log in"; + if !common::valid_title(&html, title) { + return user.set_failure( + &format!("{}: title not found: {}", &goose.request.url, title), + &mut goose.request, + Some(&headers), + Some(&html), + ); + } + + // Load all static elements on the page, as a real user would. + common::load_static_elements(user, &html).await; + + // Scrape the HTML to get the values needed in order to POST to the + // log in form. + let form_build_id = common::get_form_value(&html, "form_build_id"); + if form_build_id.is_none() { + return user.set_failure( + &format!("{}: no form_build_id on page", goose.request.url), + &mut goose.request, + Some(&headers), + Some(&html), + ); + } + + // Build log in form with username and password from environment. + let params = [ + ("name", &admin_username), + ("pass", &admin_password), + ("form_build_id", &form_build_id.unwrap()), + ("form_id", &"user_login_form".to_string()), + ("op", &"Log+in".to_string()), + ]; + let request_builder = user.goose_post("/en/user/login").await?; + logged_in_user = user.goose_send(request_builder.form(¶ms), None).await?; + + // A successful log in is redirected. + if !logged_in_user.request.redirected { + return user.set_failure( + &format!( + "{}: login failed (check ADMIN_USERNAME and ADMIN_PASSWORD)", + logged_in_user.request.final_url + ), + &mut logged_in_user.request, + Some(&headers), + None, + ); + } + } + Err(e) => { + return user.set_failure( + &format!("{}: failed to parse page: {}", goose.request.url, e), + &mut goose.request, + Some(&headers), + None, + ); + } + } + } + Err(e) => { + return user.set_failure( + &format!("{}: no response from server: {}", goose.request.url, e), + &mut goose.request, + None, + None, + ); + } + } + // Check the title to verify that the user is logged in. + common::validate_and_load_static_assets(user, logged_in_user, &admin_username).await?; + + Ok(()) +} + +/// Load and edit a random article. +pub async fn edit_article(user: &GooseUser) -> GooseTaskResult { + // First, load a random article. + let nodes = common::get_nodes(&common::ContentType::Article); + let article = nodes.choose(&mut rand::thread_rng()); + let goose = user.get(article.unwrap().url_en).await?; + common::validate_and_load_static_assets(user, goose, article.unwrap().title_en).await?; + + // Next, load the edit link for the chosen article. + let mut goose = user + .get(&format!("/en/node/{}/edit", article.unwrap().nid)) + .await?; + + let mut saved_article; + match goose.response { + Ok(response) => { + // Copy the headers so we have them for logging if there are errors. + let headers = &response.headers().clone(); + match response.text().await { + Ok(html) => { + // Be sure we've properly loaded the edit page. + let title = "Edit Article"; + if !common::valid_title(&html, title) { + return user.set_failure( + &format!("{}: title not found: {}", &goose.request.url, title), + &mut goose.request, + Some(&headers), + Some(&html), + ); + } + + // Load all static elements on the page, as a real user would. + common::load_static_elements(user, &html).await; + + // Scrape the HTML to get the values needed in order to POST to the + // log in form. + let form_build_id = common::get_form_value(&html, "form_build_id"); + if form_build_id.is_none() { + return user.set_failure( + &format!("{}: no form_build_id on page", goose.request.url), + &mut goose.request, + Some(&headers), + Some(&html), + ); + } + let form_token = common::get_form_value(&html, "form_token"); + if form_token.is_none() { + return user.set_failure( + &format!("{}: no form_token on page", goose.request.url), + &mut goose.request, + Some(&headers), + Some(&html), + ); + } + + // Build node form with random word from title. + let params = [ + ("form_build_id", &form_build_id.unwrap()), + ("form_token", &form_token.unwrap()), + ("form_id", &"node_article_edit_form".to_string()), + ("op", &"Save (this translation)".to_string()), + ]; + let request_builder = user + .goose_post(&format!("/en/node/{}/edit", article.unwrap().nid)) + .await?; + saved_article = user.goose_send(request_builder.form(¶ms), None).await?; + + // A successful node save is redirected. + if !saved_article.request.redirected { + return user.set_failure( + &format!("{}: saving article failed", saved_article.request.final_url), + &mut saved_article.request, + Some(&headers), + None, + ); + } + } + Err(e) => { + return user.set_failure( + &format!("{}: failed to parse page: {}", goose.request.url, e), + &mut goose.request, + Some(&headers), + None, + ); + } + } + } + Err(e) => { + return user.set_failure( + &format!("{}: no response from server: {}", goose.request.url, e), + &mut goose.request, + None, + None, + ); + } + } + // Be sure we're viewing the same article after editing it. + common::validate_and_load_static_assets(user, saved_article, article.unwrap().title_en).await?; + + Ok(()) +} diff --git a/examples/umami/main.rs b/examples/umami/main.rs index 52e600d8..2ec53136 100644 --- a/examples/umami/main.rs +++ b/examples/umami/main.rs @@ -1,9 +1,11 @@ +mod admin; mod common; mod english; mod spanish; use goose::prelude::*; +use crate::admin::*; use crate::english::*; use crate::spanish::*; @@ -14,7 +16,8 @@ fn main() -> Result<(), GooseError> { let _goose_metrics = GooseAttack::initialize()? .register_taskset( taskset!("Anonymous English user") - .set_weight(6)? + .set_weight(40)? + .set_wait_time(0, 3)? .register_task(task!(front_page_en).set_name("anon /").set_weight(2)?) .register_task(task!(basic_page_en).set_name("anon /en/basicpage")) .register_task(task!(article_listing_en).set_name("anon /en/articles/")) @@ -40,7 +43,8 @@ fn main() -> Result<(), GooseError> { ) .register_taskset( taskset!("Anonymous Spanish user") - .set_weight(2)? + .set_weight(9)? + .set_wait_time(0, 3)? .register_task(task!(front_page_es).set_name("anon /es/").set_weight(2)?) .register_task(task!(basic_page_es).set_name("anon /es/basicpage")) .register_task(task!(article_listing_es).set_name("anon /es/articles/")) @@ -63,6 +67,19 @@ fn main() -> Result<(), GooseError> { .register_task(task!(search_es).set_name("anon /es/search")) .register_task(task!(anonymous_contact_form_es).set_name("anon /es/contact")), ) + .register_taskset( + taskset!("Admin user") + .set_weight(1)? + .set_wait_time(3, 10)? + .register_task(task!(log_in).set_on_start().set_name("auth /en/user/login")) + .register_task(task!(front_page_en).set_name("auth /").set_weight(2)?) + .register_task(task!(article_listing_en).set_name("auth /en/articles/")) + .register_task( + task!(edit_article) + .set_name("auth /en/node/%/edit") + .set_weight(2)?, + ), + ) .set_default(GooseDefault::Host, "https://drupal-9.0.7.ddev.site/")? .execute()? .print();