Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Session Example

The examples/session.rs example demonstrates how you can add JWT authentication support to your load test, making use of the GooseUserData trait. In this example, the session is recorded in the GooseUser object with set_session_data, and retrieved with get_session_data_unchecked.

The HTML report's Request Metrics table cut after its # Fails column: GET / with 1,285 requests and POST /signup with 100, both with 0 fails, and 1,385 in all

The HTML report’s Request Metrics, from cargo run --release --features dashboard --example session -- --host http://127.0.0.1:8081 -u 100 -r 20 -t 2m --no-reset-metrics --report-file --dashboard -q against a local nginx server whose POST /signup answers {"jwtToken":"goose-example-token"}; the run writes report.html and report.json to goose-runs/<YYYYMMDD-HHMMSS>/.

POST /signup ran 100 times, once for each of the 100 users, because it is the on start transaction. The server answers / with 401 to any request without the token, and Goose counts a 401 as a failed request, so 0 fails on GET / means every one of its 1,285 requests carried the token the user stored. With -q Goose does not print the dashboard’s address when it starts: open http://127.0.0.1:5118 while the test runs. If that tab is still open when the run ends, Goose prints the address and waits for Ctrl-C.

Details

In this example, the GooseUserData is a simple struct containing a string. Goose implements GooseUserData for every type that is Clone, Send, Sync and 'static, so the struct only needs to derive Clone:

#![allow(unused)]
fn main() {
#[derive(Clone)]
struct Session {
    jwt_token: String,
}
}

Passing set_session_data a type that is not Clone does not compile.

Each user builds the struct from the JSON its signup request returns, and stores it in its own GooseUser:

    user.set_session_data(Session {
        jwt_token: response.jwt_token,
    });

The session data is retrieved from the GooseUser object with each subsequent request. The example does no validation:

async fn authenticated_index(user: &mut GooseUser) -> TransactionResult {
    // This will panic if the session is missing or if the session is not of the right type.
    // Use `get_session_data` to handle a missing session.
    let session = user.get_session_data_unchecked::<Session>();

    // Create a Reqwest RequestBuilder object and configure bearer authentication when making
    // a GET request for the index.
    let reqwest_request_builder = user
        .get_request_builder(&GooseMethod::Get, "/")?
        .bearer_auth(&session.jwt_token);

    // Add the manually created RequestBuilder and build a GooseRequest object. The
    // path names the request in the metrics, as the RequestBuilder already has its URL.
    let goose_request = GooseRequest::builder()
        .path("/")
        .set_request_builder(reqwest_request_builder)
        .build();

    // Make the actual request, and read the body to the end so the connection is reused.
    let _ = user.request(goose_request).await?.discard_body().await;

    Ok(())
}

If POST /signup does not answer with JSON holding a jwtToken, the user stores no session data, and get_session_data_unchecked panics with Missing session data!.

Complete Source Code

//! Goose load test example, leveraging the per-GooseUser `GooseUserData` field
// to store a per-user session JWT authentication token.
//!
//! ## License
//!
//! Copyright 2020-2022 Jeremy Andrews
//!
//! Licensed under the Apache License, Version 2.0 (the "License");
//! you may not use this file except in compliance with the License.
//! You may obtain a copy of the License at
//!
//! <http://www.apache.org/licenses/LICENSE-2.0>
//!
//! Unless required by applicable law or agreed to in writing, software
//! distributed under the License is distributed on an "AS IS" BASIS,
//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//! See the License for the specific language governing permissions and
//! limitations under the License.

use goose::prelude::*;
use serde::Deserialize;
use std::time::Duration;

#[derive(Clone)]
struct Session {
    jwt_token: String,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct AuthenticationResponse {
    jwt_token: String,
}

#[tokio::main]
async fn main() -> Result<(), GooseError> {
    GooseAttack::initialize()?
        // In this example, we only create a single scenario, named "WebsiteUser".
        .register_scenario(
            scenario!("WebsiteUser")
                // After each transaction runs, sleep randomly from 5 to 15 seconds.
                .set_wait_time(Duration::from_secs(5), Duration::from_secs(15))?
                // This transaction only runs one time when the user first starts.
                .register_transaction(transaction!(website_signup).set_on_start())
                // These next two transactions run repeatedly as long as the load test is running.
                .register_transaction(transaction!(authenticated_index)),
        )
        .execute()
        .await?;

    Ok(())
}

/// Demonstrates how to log in and set a session when a user starts. We flag this transaction as an
/// on_start transaction when registering it above. This means it only runs one time
/// per user, when the user thread first starts.
async fn website_signup(user: &mut GooseUser) -> TransactionResult {
    let params = [("username", "test_user"), ("password", "")];
    let response = match user.post_form("/signup", &params).await?.response {
        Ok(r) => match r.json::<AuthenticationResponse>().await {
            Ok(j) => j,
            Err(e) => return Err(Box::new(e.into())),
        },
        Err(e) => return Err(Box::new(e.into())),
    };

    user.set_session_data(Session {
        jwt_token: response.jwt_token,
    });

    Ok(())
}

/// A very simple transaction that simply loads the front page.
async fn authenticated_index(user: &mut GooseUser) -> TransactionResult {
    // This will panic if the session is missing or if the session is not of the right type.
    // Use `get_session_data` to handle a missing session.
    let session = user.get_session_data_unchecked::<Session>();

    // Create a Reqwest RequestBuilder object and configure bearer authentication when making
    // a GET request for the index.
    let reqwest_request_builder = user
        .get_request_builder(&GooseMethod::Get, "/")?
        .bearer_auth(&session.jwt_token);

    // Add the manually created RequestBuilder and build a GooseRequest object. The
    // path names the request in the metrics, as the RequestBuilder already has its URL.
    let goose_request = GooseRequest::builder()
        .path("/")
        .set_request_builder(reqwest_request_builder)
        .build();

    // Make the actual request, and read the body to the end so the connection is reused.
    let _ = user.request(goose_request).await?.discard_body().await;

    Ok(())
}