use super::client::GovernanceProposal;
pub const PROPOSAL_STATUS_APPROVED: &str = "Approved";
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ProposalNotApplicable {
#[error("proposal {proposal_id} governs {actual}, not {expected:?}")]
OtherAction {
proposal_id: String,
expected: Vec<String>,
actual: String,
},
#[error("proposal {proposal_id} for {action} is {status}, not Approved")]
NotApproved {
proposal_id: String,
action: String,
status: String,
},
#[error("proposal {proposal_id} for {action} has no readable payload")]
PayloadUnreadable { proposal_id: String, action: String },
#[error("proposal {proposal_id} came back without a resolved action")]
ActionUnresolved { proposal_id: String },
}
impl ProposalNotApplicable {
pub fn is_definitive(&self) -> bool {
!matches!(self, Self::ActionUnresolved { .. })
}
pub fn is_security_relevant(&self) -> bool {
matches!(
self,
Self::NotApproved { .. } | Self::PayloadUnreadable { .. }
)
}
pub fn reason(&self) -> &'static str {
match self {
Self::OtherAction { .. } => "other_action",
Self::NotApproved { .. } => "not_approved",
Self::PayloadUnreadable { .. } => "payload_unreadable",
Self::ActionUnresolved { .. } => "action_unresolved",
}
}
}
impl GovernanceProposal {
pub fn is_approved(&self) -> bool {
self.status == PROPOSAL_STATUS_APPROVED
}
pub fn ensure_applicable(
&self,
expected_actions: &[&str],
) -> Result<&serde_json::Value, ProposalNotApplicable> {
let Some(action) = self.action.as_deref() else {
return Err(ProposalNotApplicable::ActionUnresolved {
proposal_id: self.id.clone(),
});
};
if !expected_actions.contains(&action) {
return Err(ProposalNotApplicable::OtherAction {
proposal_id: self.id.clone(),
expected: expected_actions.iter().map(|a| (*a).to_string()).collect(),
actual: action.to_string(),
});
}
if !self.is_approved() {
return Err(ProposalNotApplicable::NotApproved {
proposal_id: self.id.clone(),
action: action.to_string(),
status: self.status.clone(),
});
}
self.payload
.as_ref()
.ok_or_else(|| ProposalNotApplicable::PayloadUnreadable {
proposal_id: self.id.clone(),
action: action.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn proposal(
action: Option<&str>,
status: &str,
payload: Option<serde_json::Value>,
) -> GovernanceProposal {
GovernanceProposal {
id: "p-1".to_string(),
policy_id: "pol-1".to_string(),
action: action.map(str::to_string),
status: status.to_string(),
payload,
}
}
#[test]
fn approved_proposal_for_an_expected_action_yields_its_payload() {
let p = proposal(Some("AuthRuleset"), "Approved", Some(json!({"v": 1})));
assert_eq!(p.ensure_applicable(&["AuthRuleset"]), Ok(&json!({"v": 1})));
assert!(p.is_approved());
}
#[test]
fn any_listed_action_is_accepted() {
let p = proposal(Some("Delist"), "Approved", Some(json!({})));
assert!(p.ensure_applicable(&["Listing", "Delist"]).is_ok());
}
#[test]
fn every_non_approved_status_is_refused_definitively_and_flagged() {
for status in [
"Pending",
"Rejected",
"Expired",
"Cancelled",
"Superseded",
"approved",
"",
] {
let p = proposal(Some("AuthRuleset"), status, Some(json!({"v": 1})));
let err = p.ensure_applicable(&["AuthRuleset"]).unwrap_err();
assert_eq!(
err,
ProposalNotApplicable::NotApproved {
proposal_id: "p-1".into(),
action: "AuthRuleset".into(),
status: status.into(),
},
"status {status:?}"
);
assert!(err.is_definitive());
assert!(err.is_security_relevant());
assert_eq!(err.reason(), "not_approved");
}
}
#[test]
fn another_action_is_routine_and_checked_before_status() {
let p = proposal(Some("FeeChange"), "Pending", Some(json!({})));
let err = p.ensure_applicable(&["AuthRuleset"]).unwrap_err();
assert!(matches!(err, ProposalNotApplicable::OtherAction { .. }));
assert!(err.is_definitive());
assert!(!err.is_security_relevant());
}
#[test]
fn unresolved_action_holds_the_event() {
let p = proposal(None, "Approved", Some(json!({})));
let err = p.ensure_applicable(&["AuthRuleset"]).unwrap_err();
assert_eq!(
err,
ProposalNotApplicable::ActionUnresolved {
proposal_id: "p-1".into()
}
);
assert!(!err.is_definitive());
assert!(!err.is_security_relevant());
}
#[test]
fn approved_proposal_without_a_payload_is_refused() {
let p = proposal(Some("AuthRuleset"), "Approved", None);
let err = p.ensure_applicable(&["AuthRuleset"]).unwrap_err();
assert!(matches!(
err,
ProposalNotApplicable::PayloadUnreadable { .. }
));
assert!(err.is_definitive());
assert!(err.is_security_relevant());
}
}