use crate::cli::pact_broker::main::{
HALClient, PactBrokerError,
broker_path::BrokerPath,
utils::{get_auth, get_broker_url, get_custom_headers, get_retries, get_ssl_options},
};
pub fn create_or_update_version(args: &clap::ArgMatches) -> Result<String, PactBrokerError> {
let broker_url = get_broker_url(args).trim_end_matches('/').to_string();
let auth = get_auth(args);
let custom_headers = get_custom_headers(args);
let ssl_options = get_ssl_options(args);
let pacticipant_name = args.get_one::<String>("pacticipant").unwrap();
let version_number = args.get_one::<String>("version").unwrap();
let branch_name = args.try_get_one::<String>("branch").unwrap();
let tags = args
.get_many::<String>("tag")
.unwrap_or_default()
.cloned()
.collect::<Vec<_>>();
let version_href = BrokerPath::new(&broker_url)
.literal("pacticipants")
.value("pacticipant", pacticipant_name)
.literal("versions")
.value("version", version_number)
.build()?;
let branch_href = branch_name
.map(|branch| {
BrokerPath::new(&broker_url)
.literal("pacticipants")
.value("pacticipant", pacticipant_name)
.literal("branches")
.value("branch", branch)
.literal("versions")
.value("version", version_number)
.build()
})
.transpose()?;
let tag_hrefs = tags
.iter()
.map(|tag| {
BrokerPath::new(&broker_url)
.literal("pacticipants")
.value("pacticipant", pacticipant_name)
.literal("versions")
.value("version", version_number)
.literal("tags")
.value("tag", tag)
.build()
.map(|href| (tag, href))
})
.collect::<Result<Vec<_>, _>>()?;
tokio::runtime::Runtime::new().unwrap().block_on(async {
let hal_client: HALClient = HALClient::with_url(
&broker_url,
Some(auth.clone()),
ssl_options.clone(),
custom_headers.clone(),
)
.with_retry_count(get_retries(args));
if let (Some(branch), Some(branch_href)) = (branch_name, &branch_href) {
let branch_data = serde_json::json!({ "name": branch });
let branch_data_str = branch_data.to_string();
let res = hal_client
.put_json(branch_href, &branch_data_str, None)
.await;
res?;
println!(
"Branch '{}' created for version '{}'",
branch, version_number
);
}
for (tag, tag_href) in &tag_hrefs {
let tag_data = serde_json::json!({ "name": tag });
let tag_data_str = tag_data.to_string();
let res = hal_client.put_json(tag_href, &tag_data_str, None).await;
res?;
println!("Tag '{}' created for version '{}'", tag, version_number);
}
if tags.is_empty() && branch_name.is_none() {
let version_data = serde_json::json!({});
let version_data_str = version_data.to_string();
let res = hal_client
.put_json(&version_href, &version_data_str, None)
.await;
match res {
Ok(_) => {
println!(
"Version '{}' created or updated successfully",
version_number
);
}
Err(err) => {
return Err(PactBrokerError::IoError(format!(
"Failed to create or update version '{}': {}",
version_number, err
)));
}
}
}
Ok("Version created or updated successfully".to_string())
})
}
#[cfg(test)]
mod create_or_update_version_tests {
use super::create_or_update_version;
use crate::cli::pact_broker::main::subcommands::add_create_or_update_version_subcommand;
use pact_consumer::prelude::*;
use pact_models::PactSpecification;
use serde_json::json;
#[test]
fn creates_version_with_tag_and_branch_successfully() {
let config = MockServerConfig {
pact_specification: PactSpecification::V2,
..MockServerConfig::default()
};
let pacticipant_name = "Foo";
let version_number = "1";
let branch_name = "test";
let tag_name = "prod";
let pacticipant_path = format!("/pacticipants/{}", pacticipant_name);
let branch_path = format!(
"{}/branches/{}/versions/{}",
pacticipant_path, branch_name, version_number
);
let tag_path = format!(
"{}/versions/{}/tags/{}",
pacticipant_path, version_number, tag_name
);
let pact_broker_service = PactBuilder::new("pact-broker-cli", "Pact Broker")
.interaction("create branch for version", "", |mut i| {
i.given(format!(
"a pacticipant with name {} exists",
pacticipant_name
));
i.request
.method("PUT")
.path(branch_path.clone())
.header("Accept", "application/hal+json")
.header("Content-Type", "application/json")
.body(json!({ "name": branch_name }).to_string());
i.response
.status(201)
.header("Content-Type", "application/hal+json;charset=utf-8")
.json_body(json!({
"_links": {
"pb:branch": {
"name": branch_name,
},
}
}));
i
})
.interaction("create tag for version", "", |mut i| {
i.request
.method("PUT")
.path(tag_path.clone())
.header("Accept", "application/hal+json")
.header("Content-Type", "application/json")
.body(json!({ "name": tag_name }).to_string());
i.response
.status(201)
.header("Content-Type", "application/hal+json;charset=utf-8")
.json_body(json!({ "name": tag_name }));
i
})
.start_mock_server(None, Some(config));
let mock_server_url = pact_broker_service.url();
let matches = add_create_or_update_version_subcommand().get_matches_from(vec![
"create-or-update-version",
"-b",
mock_server_url.as_str(),
"--pacticipant",
pacticipant_name,
"--version",
version_number,
"--branch",
branch_name,
"--tag",
tag_name,
]);
let result = create_or_update_version(&matches);
assert!(result.is_ok());
let output = result.unwrap();
println!("Output: {}", output);
assert!(output.contains("Version created or updated successfully"));
}
#[test]
fn creates_version_without_tag_or_branch() {
let config = MockServerConfig {
pact_specification: PactSpecification::V2,
..MockServerConfig::default()
};
let pacticipant_name = "Foo";
let version_number = "1";
let version_path = format!(
"/pacticipants/{}/versions/{}",
pacticipant_name, version_number
);
let pact_broker_service = PactBuilder::new("pact-broker-cli", "Pact Broker")
.interaction("create version without tag or branch", "", |mut i| {
i.given(format!(
"a pacticipant with name {} exists",
pacticipant_name
));
i.request
.method("PUT")
.path(version_path.clone())
.header("Accept", "application/hal+json")
.header("Content-Type", "application/json")
.body(json!({}).to_string());
i.response
.status(201)
.header("Content-Type", "application/hal+json;charset=utf-8")
.json_body(json!({}));
i
})
.start_mock_server(None, Some(config));
let mock_server_url = pact_broker_service.url();
let matches = add_create_or_update_version_subcommand().get_matches_from(vec![
"create-or-update-version",
"-b",
mock_server_url.as_str(),
"--pacticipant",
pacticipant_name,
"--version",
version_number,
]);
let result = create_or_update_version(&matches);
assert!(result.is_ok());
let output = result.unwrap();
println!("Output: {}", output);
assert!(output.contains("Version created or updated successfully"));
}
use crate::cli::pact_broker::main::PactBrokerError;
use crate::cli::pact_broker::main::test_utils::spawn_recording_broker;
fn run(broker_url: &str, extra: &[&str]) -> Result<String, PactBrokerError> {
let mut args = vec!["create-or-update-version", "-b", broker_url];
args.extend_from_slice(extra);
create_or_update_version(&add_create_or_update_version_subcommand().get_matches_from(args))
}
#[test]
fn encodes_branch_containing_slash() {
let (broker_url, requests) = spawn_recording_broker();
run(
&broker_url,
&[
"--pacticipant",
"my consumer",
"--version",
"1.0.0",
"--branch",
"fix/foo",
],
)
.unwrap();
assert_eq!(
*requests.lock().unwrap(),
vec!["PUT /pacticipants/my%20consumer/branches/fix%2Ffoo/versions/1.0.0"]
);
}
#[test]
fn encodes_branch_under_context_path_with_trailing_slash() {
let (broker_url, requests) = spawn_recording_broker();
run(
&format!("{broker_url}/ctx/"),
&[
"--pacticipant",
"c",
"--version",
"v",
"--branch",
"fix/foo",
"--tag",
"feat/bar",
],
)
.unwrap();
assert_eq!(
*requests.lock().unwrap(),
vec![
"PUT /ctx/pacticipants/c/branches/fix%2Ffoo/versions/v",
"PUT /ctx/pacticipants/c/versions/v/tags/feat%2Fbar",
]
);
}
#[test]
fn rejects_invalid_value_before_sending_any_request() {
let (broker_url, requests) = spawn_recording_broker();
let result = run(
&broker_url,
&[
"--pacticipant",
"c",
"--version",
"v",
"--branch",
"ok",
"--tag",
"x.json",
],
);
assert!(
matches!(result, Err(PactBrokerError::InvalidPathValue(_))),
"{result:?}"
);
assert!(requests.lock().unwrap().is_empty());
}
}