use mockito::{Matcher, Mock, Server, ServerGuard};
use super::{
CHORE_ONLY, FEAT_AND_FIX, mock_analysis, mock_head_ref, mock_no_mutations, mock_tag_lookup,
mock_tags, options, publisher, write_fixture_repo,
};
use crate::{BumpLevel, ReleaseAction, ReleaseOptions, ReleaseOutcome, ReleasePhase};
const TAGS_V075_V023: &str = r#"[{"name":"v0.2.3","commit":{"sha":"tag023sha"}},{"name":"v0.7.5","commit":{"sha":"tag075sha"}}]"#;
fn mock_compare_from(server: &mut ServerGuard, base_tag: &str, body: &str) -> Mock {
server
.mock(
"GET",
format!("/repos/acme/demo/compare/{base_tag}...basecommitsha?per_page=100&page=1")
.as_str(),
)
.with_status(200)
.with_body(body)
.create()
}
fn mock_behind_analysis(server: &mut ServerGuard, compare_body: &str) -> Vec<Mock> {
vec![
mock_head_ref(server, "basecommitsha", 1),
mock_tags(server, TAGS_V075_V023),
mock_compare_from(server, "v0.7.5", compare_body),
]
}
fn mock_nothing_after_analysis(server: &mut ServerGuard) -> Vec<Mock> {
let mut mocks = mock_no_mutations(server);
for (method, path) in [
("GET", r"^/repos/acme/demo/git/ref/tags/"),
("GET", r"^/repos/acme/demo/git/ref/heads/automation/"),
("GET", r"^/repos/acme/demo/pulls"),
("POST", r"^/repos/acme/demo/pulls$"),
("POST", r"^/repos/acme/demo/git/tags$"),
] {
mocks.push(server.mock(method, Matcher::Regex(path.to_owned())).expect(0).create());
}
mocks
}
fn assert_refused(configure: impl FnOnce(&mut ReleaseOptions), compare_body: &str) {
let temp_dir = write_fixture_repo();
let mut server = Server::new();
let _analysis = mock_behind_analysis(&mut server, compare_body);
let nothing_after = mock_nothing_after_analysis(&mut server);
let mut release_options = options(temp_dir.path());
configure(&mut release_options);
let error = publisher(&server)
.release(&release_options)
.expect_err("a manifest behind the latest tag must not release");
let message = error.to_string();
assert!(
message.contains("Cargo.toml version 0.2.3 is lower than the latest release tag v0.7.5"),
"unexpected error: {message}"
);
assert!(message.contains("Set the manifest version to 0.7.5"), "unexpected error: {message}");
for mock in ¬hing_after {
mock.assert();
}
}
#[test]
fn direct_release_refuses_a_manifest_behind_the_latest_tag() {
assert_refused(|_| {}, FEAT_AND_FIX);
}
#[test]
fn release_pull_request_mode_refuses_a_manifest_behind_the_latest_tag() {
assert_refused(|release_options| release_options.create_pr = true, FEAT_AND_FIX);
}
#[test]
fn dry_run_refuses_a_manifest_behind_the_latest_tag() {
assert_refused(|release_options| release_options.dry_run = true, FEAT_AND_FIX);
assert_refused(
|release_options| {
release_options.dry_run = true;
release_options.create_pr = true;
},
FEAT_AND_FIX,
);
}
#[test]
fn forced_bumps_and_phases_refuse_a_manifest_behind_the_latest_tag() {
assert_refused(|release_options| release_options.bump = Some(BumpLevel::Major), CHORE_ONLY);
assert_refused(|release_options| release_options.phase = ReleasePhase::Bump, FEAT_AND_FIX);
assert_refused(|release_options| release_options.phase = ReleasePhase::Tag, FEAT_AND_FIX);
}
#[test]
fn a_manifest_behind_the_latest_tag_fails_even_with_nothing_to_release() {
assert_refused(|_| {}, CHORE_ONLY);
}
fn dry_run_action(
configure: impl FnOnce(&mut ReleaseOptions),
mock_reads: impl FnOnce(&mut ServerGuard) -> Vec<Mock>,
) -> Option<ReleaseAction> {
let temp_dir = write_fixture_repo();
let mut server = Server::new();
let _reads = mock_reads(&mut server);
let no_mutations = mock_no_mutations(&mut server);
let mut release_options = options(temp_dir.path());
release_options.dry_run = true;
configure(&mut release_options);
let report = publisher(&server).release(&release_options).expect("release report");
assert_eq!(report.outcome, ReleaseOutcome::DryRun);
for mock in &no_mutations {
mock.assert();
}
report.action
}
#[test]
fn dry_run_records_the_direct_release_action() {
let action = dry_run_action(|_| {}, |server| mock_analysis(server, 1, FEAT_AND_FIX));
assert_eq!(action, Some(ReleaseAction::CommitAndRelease { branch: String::from("main") }));
}
#[test]
fn dry_run_records_the_bump_phase_action() {
let action = dry_run_action(
|release_options| release_options.phase = ReleasePhase::Bump,
|server| mock_analysis(server, 1, FEAT_AND_FIX),
);
assert_eq!(action, Some(ReleaseAction::CommitBump { branch: String::from("main") }));
}
#[test]
fn dry_run_records_the_release_pull_request_action() {
let action = dry_run_action(
|release_options| release_options.create_pr = true,
|server| mock_analysis(server, 1, FEAT_AND_FIX),
);
assert_eq!(
action,
Some(ReleaseAction::ProposeReleasePullRequest {
release_branch: String::from("automation/release"),
base: String::from("main"),
})
);
}
#[test]
fn dry_run_records_the_merged_release_tag_action() {
let action = dry_run_action(
|release_options| release_options.create_pr = true,
|server| {
vec![
mock_head_ref(server, "basecommitsha", 1),
mock_tags(server, r#"[{"name":"v0.2.2","commit":{"sha":"tagsha"}}]"#),
mock_compare_from(server, "v0.2.2", FEAT_AND_FIX),
mock_tag_lookup(server, "v0.2.3", 404, r#"{"message":"Not Found"}"#),
]
},
);
assert_eq!(action, Some(ReleaseAction::TagManifestVersion { branch: String::from("main") }));
}
#[test]
fn skipped_runs_record_no_action() {
let temp_dir = write_fixture_repo();
let mut server = Server::new();
let _head = mock_head_ref(&mut server, "basecommitsha", 1);
let _tags = mock_tags(&mut server, r#"[{"name":"v0.2.3","commit":{"sha":"tagsha"}}]"#);
let _compare = mock_compare_from(&mut server, "v0.2.3", CHORE_ONLY);
let _no_mutations = mock_no_mutations(&mut server);
let report = publisher(&server).release(&options(temp_dir.path())).expect("release report");
assert_eq!(report.outcome, ReleaseOutcome::SkippedNoReleasableChanges);
assert_eq!(report.action, None);
}