Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
28 changes: 26 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()))
Comment thread
ctron marked this conversation as resolved.
}
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
})
Expand Down
150 changes: 149 additions & 1 deletion src/restapi.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,71 @@
use crate::utils::DisplayVec;
use goose::goose::{GooseUser, TransactionResult};
use anyhow::Context;
use goose::goose::{GooseMethod, GooseRequest, GooseUser, TransactionError, TransactionResult};
use reqwest::{Client, RequestBuilder};

Comment thread
bxf12315 marked this conversation as resolved.
use crate::utils::GooseUserData;
use rand::Rng;
use serde_json::json;
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use urlencoding::encode;

/// Fetch advisory total count once at application startup
pub async fn get_advisory_total(host: String) -> Result<u64, anyhow::Error> {
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::<serde_json::Value>().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::<serde_json::Value>().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)));

Expand Down Expand Up @@ -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<String, Box<TransactionError>> {
let advisory_id = {
let goose_user_data = user
.get_session_data_mut::<GooseUserData>()
.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::<GooseUserData>()
.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?;

Expand Down
8 changes: 8 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ impl<T: std::fmt::Display> std::fmt::Display for DisplayVec<T> {
}
}

/// 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)]
Comment thread
ctron marked this conversation as resolved.
pub struct GooseUserData {
pub advisory_id: Option<String>,
}

#[cfg(test)]
mod test {
use super::*;
Expand Down