diff --git a/Cargo.lock b/Cargo.lock index 0e81a9e..f433bbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1353,6 +1353,7 @@ dependencies = [ "log", "openid", "packageurl", + "rand 0.8.5", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index e48a30f..7e8a700 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "uuid", tokio = { version = "1.43.1", features = ["sync"] } urlencoding = "2" packageurl = "0.4.2" +rand = "0.8" [features] default = ["postgres"] diff --git a/src/main.rs b/src/main.rs index b11c30d..7bd9312 100644 --- a/src/main.rs +++ b/src/main.rs @@ -214,7 +214,7 @@ async fn main() -> Result<(), anyhow::Error> { "RestAPIUserDelete", wait_time_from, wait_time_to, - custom_client, + custom_client.clone(), )? .set_weight(1)? // With 100 SBOM IDs this ensure they all delete something in the sequential situation @@ -224,7 +224,31 @@ async fn main() -> Result<(), anyhow::Error> { tx!(s.delete_sbom_from_pool_sequential?( scenario.delete_sbom_pool.clone(), delete_counter.clone() - ), name: format!("delete_sbom_from_pool_sequential[{} SBOMs]", pool.len())) + ), + name: format!("delete_sbom_from_pool_sequential[{} SBOMs]", pool.len())) + } + s + }) + .register_scenario({ + let mut s = create_scenario( + "RestAdvisoryLableUser", + wait_time_from, + wait_time_to, + custom_client, + )? + .set_weight(5)?; + // Register advisory label transactions if host is available. + // Since the scenario object doesn't provide host information, we use environment + let host = s + .host + .clone() + .or_else(|| std::env::var("HOST").ok()) + .unwrap_or_else(|| "http://localhost:8080".to_string()); + let total_advisories = restapi::get_advisory_total(host).await.ok(); + if let Some(total) = total_advisories { + tx!(s.find_random_advisory?(Some(total))); + s = s.register_transaction(tx!(put_advisory_labels)); + s = s.register_transaction(tx!(patch_advisory_labels)); } s }) diff --git a/src/restapi.rs b/src/restapi.rs index a99de09..c51af5b 100644 --- a/src/restapi.rs +++ b/src/restapi.rs @@ -1,5 +1,10 @@ use crate::utils::DisplayVec; -use goose::goose::{GooseUser, TransactionResult}; +use anyhow::Context; +use goose::goose::{GooseMethod, GooseRequest, GooseUser, TransactionError, TransactionResult}; +use reqwest::{Client, RequestBuilder}; + +use crate::utils::GooseUserData; +use rand::Rng; use serde_json::json; use std::sync::{ Arc, @@ -7,6 +12,60 @@ use std::sync::{ }; use urlencoding::encode; +/// Fetch advisory total count once at application startup +pub async fn get_advisory_total(host: String) -> Result { + let url = format!("{}/api/v2/advisory", host.trim_end_matches('/')); + + log::info!("Fetching advisory total from: {}", url); + + let response = reqwest::get(&url) + .await + .context("Failed to send request to get advisory total")? + .error_for_status() + .context("Failed to get advisory total")?; + + let json_data = response.json::().await?; + + if let Some(total) = json_data.get("total").and_then(|t| t.as_u64()) { + return Ok(total); + } + Err(anyhow::anyhow!( + "Failed to get advisory total count".to_string(), + )) +} + +/// Get a random advisory ID +pub async fn find_random_advisory( + total_advisories: u64, + user: &mut GooseUser, +) -> TransactionResult { + // Generate random offset using the provided total + let offset = rand::thread_rng().gen_range(0..=total_advisories); + let url = format!("/api/v2/advisory?offset={}&limit=1", offset); + + let response = user.get(&url).await?; + let json_data = response.response?.json::().await?; + + // Extract advisory ID from the response + if let Some(items) = json_data.get("items").and_then(|i| i.as_array()) + && let Some(first_item) = items.first() + && let Some(id) = first_item.get("uuid").and_then(|u| u.as_str()) + { + log::info!("Listing advisory with offset {}: {}", offset, id); + + user.set_session_data(GooseUserData { + advisory_id: Some(id.to_string()), + }); + return Ok(()); + } + + // Return error if no advisory found + Err(Box::new(TransactionError::Custom(format!( + "No advisory found at offset: {}", + offset + )))) +} + pub async fn get_advisory(id: String, user: &mut GooseUser) -> TransactionResult { let uri = format!("/api/v2/advisory/{}", encode(&format!("urn:uuid:{}", id))); @@ -67,6 +126,95 @@ pub async fn search_advisory(user: &mut GooseUser) -> TransactionResult { Ok(()) } +/// Send Advisory labels request +async fn send_advisory_label_request( + advisory_id: String, + user: &mut GooseUser, + method: GooseMethod, + source: &str, + client_method: fn(&Client, String) -> RequestBuilder, +) -> TransactionResult { + let path = format!("/api/v2/advisory/{}/label", advisory_id); + let json = json!({ + "source": source, + "foo": "bar", + "space": "with space", + "empty": "", + }); + + let url = user.build_url(&path)?; + + let reqwest_request_builder = client_method(&user.client, url); + let goose_request = GooseRequest::builder() + .method(method) + .path(path.as_str()) + .set_request_builder(reqwest_request_builder.json(&json)) + .build(); + let _response = user.request(goose_request).await?; + + Ok(()) +} + +/// Get advisory ID from Goose user data +fn get_advisory_id(user: &mut GooseUser) -> Result> { + let advisory_id = { + let goose_user_data = user + .get_session_data_mut::() + .ok_or_else(|| { + Box::new(TransactionError::Custom( + "No GooseUserData found, please initialize user data first".to_string(), + )) + })?; + + goose_user_data.advisory_id.clone().ok_or_else(|| { + Box::new(TransactionError::Custom( + "No advisory_id found in GooseUserData".to_string(), + )) + })? + }; + Ok(advisory_id) +} + +/// Send Advisory labels request using PUT method +pub async fn put_advisory_labels(user: &mut GooseUser) -> TransactionResult { + let advisory_id = get_advisory_id(user)?; + send_advisory_label_request( + advisory_id, + user, + GooseMethod::Put, + "It's a put request", + Client::put, + ) + .await +} + +/// Send Advisory labels request using PATCH method +pub async fn patch_advisory_labels(user: &mut GooseUser) -> TransactionResult { + let advisory_id = { + let goose_user_data = user + .get_session_data_mut::() + .ok_or_else(|| { + Box::new(TransactionError::Custom( + "No GooseUserData found, please initialize user data first".to_string(), + )) + })?; + + goose_user_data.advisory_id.clone().ok_or_else(|| { + Box::new(TransactionError::Custom( + "No advisory_id found in GooseUserData".to_string(), + )) + })? + }; + send_advisory_label_request( + advisory_id, + user, + GooseMethod::Patch, + "It's a patch request", + Client::patch, + ) + .await +} + pub async fn list_importer(user: &mut GooseUser) -> TransactionResult { let _response = user.get("/api/v2/importer").await?; diff --git a/src/utils.rs b/src/utils.rs index f7f7def..349aa7a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -23,6 +23,14 @@ impl std::fmt::Display for DisplayVec { } } +/// This struct is used to store user-specific data that is needed for Goose transactions. +/// +/// The advisory ID is randomly selected from the available advisories. +#[derive(Clone)] +pub struct GooseUserData { + pub advisory_id: Option, +} + #[cfg(test)] mod test { use super::*;