fix: guard state field workflow transitions
This commit is contained in:
@@ -3920,12 +3920,18 @@ async fn scoped_close_ticket(
|
|||||||
browser_ticket_detail(&api, &path.id)
|
browser_ticket_detail(&api, &path.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reject_unguarded_ticket_start(operation: &TicketBackendOperation) -> Result<()> {
|
fn generic_ticket_state_change(operation: &TicketBackendOperation) -> Option<&TicketStateChange> {
|
||||||
if matches!(
|
match operation {
|
||||||
operation,
|
|
||||||
TicketBackendOperation::SetWorkflowState { change, .. }
|
TicketBackendOperation::SetWorkflowState { change, .. }
|
||||||
if change.to == "inprogress" && change.from != "inprogress"
|
| TicketBackendOperation::SetStateField { change, .. } => Some(change),
|
||||||
) {
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reject_unguarded_ticket_start(operation: &TicketBackendOperation) -> Result<()> {
|
||||||
|
if generic_ticket_state_change(operation)
|
||||||
|
.is_some_and(|change| change.to == "inprogress" && change.from != "inprogress")
|
||||||
|
{
|
||||||
return Err(Error::TicketAssignmentConflict(
|
return Err(Error::TicketAssignmentConflict(
|
||||||
"inprogress is guarded by Queue acceptance or atomic ready-state Coder assignment"
|
"inprogress is guarded by Queue acceptance or atomic ready-state Coder assignment"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -3936,8 +3942,7 @@ fn reject_unguarded_ticket_start(operation: &TicketBackendOperation) -> Result<(
|
|||||||
|
|
||||||
fn reject_unguarded_ticket_completion(operation: &TicketBackendOperation) -> Result<()> {
|
fn reject_unguarded_ticket_completion(operation: &TicketBackendOperation) -> Result<()> {
|
||||||
reject_unguarded_ticket_start(operation)?;
|
reject_unguarded_ticket_start(operation)?;
|
||||||
if matches!(operation, TicketBackendOperation::SetWorkflowState { change, .. } if change.to == "done")
|
if generic_ticket_state_change(operation).is_some_and(|change| change.to == "done") {
|
||||||
{
|
|
||||||
return Err(Error::TicketAssignmentConflict(
|
return Err(Error::TicketAssignmentConflict(
|
||||||
"done is guarded by MergeRequestComplete with an approved immutable revision and operation_id".to_string(),
|
"done is guarded by MergeRequestComplete with an approved immutable revision and operation_id".to_string(),
|
||||||
));
|
));
|
||||||
@@ -13329,37 +13334,60 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn generic_worker_state_change_cannot_enter_inprogress() {
|
fn generic_worker_state_change_cannot_enter_inprogress() {
|
||||||
for from in ["planning", "ready", "queued"] {
|
for from in ["planning", "ready", "queued"] {
|
||||||
let operation = TicketBackendOperation::SetWorkflowState {
|
let change = || {
|
||||||
id: TicketIdOrSlug::Query("T1".to_string()),
|
TicketStateChange::new(
|
||||||
change: TicketStateChange::new(
|
|
||||||
from,
|
from,
|
||||||
"inprogress",
|
"inprogress",
|
||||||
"worker-tool",
|
"worker-tool",
|
||||||
"bypass assignment-aware start",
|
"bypass assignment-aware start",
|
||||||
),
|
)
|
||||||
};
|
};
|
||||||
let error = reject_unguarded_ticket_completion(&operation).unwrap_err();
|
let operations = [
|
||||||
assert!(
|
TicketBackendOperation::SetWorkflowState {
|
||||||
error
|
id: TicketIdOrSlug::Query("T1".to_string()),
|
||||||
.to_string()
|
change: change(),
|
||||||
.contains("atomic ready-state Coder assignment")
|
},
|
||||||
);
|
TicketBackendOperation::SetStateField {
|
||||||
|
id: TicketIdOrSlug::Query("T1".to_string()),
|
||||||
|
field: "state".to_string(),
|
||||||
|
change: change(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for operation in operations {
|
||||||
|
let error = reject_unguarded_ticket_completion(&operation).unwrap_err();
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.to_string()
|
||||||
|
.contains("atomic ready-state Coder assignment")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn flow_or_generic_worker_state_change_is_not_ticket_completion_authority() {
|
fn flow_or_generic_worker_state_change_is_not_ticket_completion_authority() {
|
||||||
let operation = TicketBackendOperation::SetWorkflowState {
|
let change = || {
|
||||||
id: TicketIdOrSlug::Query("T1".to_string()),
|
TicketStateChange::new(
|
||||||
change: TicketStateChange::new(
|
|
||||||
"inprogress",
|
"inprogress",
|
||||||
"done",
|
"done",
|
||||||
"flow reached terminal state",
|
"flow reached terminal state",
|
||||||
"terminal flow state",
|
"terminal flow state",
|
||||||
),
|
)
|
||||||
};
|
};
|
||||||
let error = reject_unguarded_ticket_completion(&operation).unwrap_err();
|
for operation in [
|
||||||
assert!(error.to_string().contains("MergeRequestComplete"));
|
TicketBackendOperation::SetWorkflowState {
|
||||||
|
id: TicketIdOrSlug::Query("T1".to_string()),
|
||||||
|
change: change(),
|
||||||
|
},
|
||||||
|
TicketBackendOperation::SetStateField {
|
||||||
|
id: TicketIdOrSlug::Query("T1".to_string()),
|
||||||
|
field: "state".to_string(),
|
||||||
|
change: change(),
|
||||||
|
},
|
||||||
|
] {
|
||||||
|
let error = reject_unguarded_ticket_completion(&operation).unwrap_err();
|
||||||
|
assert!(error.to_string().contains("MergeRequestComplete"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user