Skip to content

Commit 3780159

Browse files
authored
rust-guard: extract first_matching_label helper and add label-check unit tests (#8047)
`first_matching_approval_label` and `first_matching_refusal_label` were near-identical — differing only in which `PolicyContext` field they accessed. Adds a shared private helper and direct unit tests for all four label-check functions. ## Refactor: extract `first_matching_label` Replaces the duplicated body in both functions with a single helper that takes the label list directly: ```rust fn first_matching_label<'a>(item: &'a Value, label_list: &[String]) -> Option<&'a str> { if label_list.is_empty() { return None; } extract_github_label_names(item) .into_iter() .find(|name| label_list.iter().any(|l| l.eq_ignore_ascii_case(name))) } fn first_matching_approval_label<'a>(item: &'a Value, ctx: &PolicyContext) -> Option<&'a str> { first_matching_label(item, &ctx.approval_labels) } fn first_matching_refusal_label<'a>(item: &'a Value, ctx: &PolicyContext) -> Option<&'a str> { first_matching_label(item, &ctx.refusal_labels) } ``` ## Tests: direct unit tests for label-check helpers in `helpers.rs` Adds 10 tests covering `has_approval_label`, `has_refusal_label`, `has_promotion_label`, and `has_demotion_label`: - Case-insensitive matching - Empty list / empty string → feature disabled (`false`) - Missing `labels` field on the item → `false`
2 parents e5f3ffa + b40f348 commit 3780159

1 file changed

Lines changed: 105 additions & 18 deletions

File tree

  • guards/github-guard/rust-guard/src/labels

guards/github-guard/rust-guard/src/labels/helpers.rs

Lines changed: 105 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -387,17 +387,20 @@ pub fn has_approval_label(item: &Value, ctx: &PolicyContext) -> bool {
387387
first_matching_approval_label(item, ctx).is_some()
388388
}
389389

390-
/// Return the first matching approval label name from an item, if any.
391-
fn first_matching_approval_label<'a>(item: &'a Value, ctx: &PolicyContext) -> Option<&'a str> {
392-
if ctx.approval_labels.is_empty() {
390+
/// Return the first item label that matches any entry in `label_list` (case-insensitive).
391+
/// Returns `None` immediately when `label_list` is empty (feature disabled).
392+
fn first_matching_label<'a>(item: &'a Value, label_list: &[String]) -> Option<&'a str> {
393+
if label_list.is_empty() {
393394
return None;
394395
}
395-
let label_names = extract_github_label_names(item);
396-
label_names.into_iter().find(|name| {
397-
ctx.approval_labels
398-
.iter()
399-
.any(|al| al.eq_ignore_ascii_case(name))
400-
})
396+
extract_github_label_names(item)
397+
.into_iter()
398+
.find(|name| label_list.iter().any(|l| l.eq_ignore_ascii_case(name)))
399+
}
400+
401+
/// Return the first matching approval label name from an item, if any.
402+
fn first_matching_approval_label<'a>(item: &'a Value, ctx: &PolicyContext) -> Option<&'a str> {
403+
first_matching_label(item, &ctx.approval_labels)
401404
}
402405

403406
/// Check whether a content item carries at least one label from the configured
@@ -409,15 +412,7 @@ pub fn has_refusal_label(item: &Value, ctx: &PolicyContext) -> bool {
409412

410413
/// Return the first matching refusal label name from an item, if any.
411414
fn first_matching_refusal_label<'a>(item: &'a Value, ctx: &PolicyContext) -> Option<&'a str> {
412-
if ctx.refusal_labels.is_empty() {
413-
return None;
414-
}
415-
let label_names = extract_github_label_names(item);
416-
label_names.into_iter().find(|name| {
417-
ctx.refusal_labels
418-
.iter()
419-
.any(|rl| rl.eq_ignore_ascii_case(name))
420-
})
415+
first_matching_label(item, &ctx.refusal_labels)
421416
}
422417

423418
/// Apply approval-label promotion: if the item carries a configured approval label,
@@ -3659,4 +3654,96 @@ mod tests {
36593654
assert_eq!(MinIntegrity::from_policy_str(level.as_str()), Some(*level));
36603655
}
36613656
}
3657+
3658+
// =========================================================================
3659+
// Tests for label-checking helpers
3660+
// =========================================================================
3661+
3662+
#[test]
3663+
fn test_has_approval_label_matches_case_insensitively() {
3664+
let ctx = PolicyContext {
3665+
approval_labels: vec!["approved".to_string()],
3666+
..Default::default()
3667+
};
3668+
let item = serde_json::json!({"labels": [{"name": "APPROVED"}]});
3669+
assert!(has_approval_label(&item, &ctx));
3670+
}
3671+
3672+
#[test]
3673+
fn test_has_approval_label_no_match_returns_false() {
3674+
let ctx = PolicyContext {
3675+
approval_labels: vec!["approved".to_string()],
3676+
..Default::default()
3677+
};
3678+
let item = serde_json::json!({"labels": [{"name": "other-label"}]});
3679+
assert!(!has_approval_label(&item, &ctx));
3680+
}
3681+
3682+
#[test]
3683+
fn test_has_approval_label_empty_list_returns_false() {
3684+
let ctx = PolicyContext::default();
3685+
let item = serde_json::json!({"labels": [{"name": "approved"}]});
3686+
assert!(!has_approval_label(&item, &ctx));
3687+
}
3688+
3689+
#[test]
3690+
fn test_has_approval_label_no_labels_field_returns_false() {
3691+
let ctx = PolicyContext {
3692+
approval_labels: vec!["approved".to_string()],
3693+
..Default::default()
3694+
};
3695+
let item = serde_json::json!({"number": 1, "title": "no labels field"});
3696+
assert!(!has_approval_label(&item, &ctx));
3697+
}
3698+
3699+
#[test]
3700+
fn test_has_refusal_label_matches_case_insensitively() {
3701+
let ctx = PolicyContext {
3702+
refusal_labels: vec!["spam".to_string()],
3703+
..Default::default()
3704+
};
3705+
let item = serde_json::json!({"labels": [{"name": "SPAM"}]});
3706+
assert!(has_refusal_label(&item, &ctx));
3707+
}
3708+
3709+
#[test]
3710+
fn test_has_refusal_label_empty_list_returns_false() {
3711+
let ctx = PolicyContext::default();
3712+
let item = serde_json::json!({"labels": [{"name": "spam"}]});
3713+
assert!(!has_refusal_label(&item, &ctx));
3714+
}
3715+
3716+
#[test]
3717+
fn test_has_promotion_label_matches_case_insensitively() {
3718+
let ctx = PolicyContext {
3719+
promotion_label: "trusted".to_string(),
3720+
..Default::default()
3721+
};
3722+
let item = serde_json::json!({"labels": [{"name": "TRUSTED"}]});
3723+
assert!(has_promotion_label(&item, &ctx));
3724+
}
3725+
3726+
#[test]
3727+
fn test_has_promotion_label_empty_string_returns_false() {
3728+
let ctx = PolicyContext::default();
3729+
let item = serde_json::json!({"labels": [{"name": "trusted"}]});
3730+
assert!(!has_promotion_label(&item, &ctx));
3731+
}
3732+
3733+
#[test]
3734+
fn test_has_demotion_label_matches_case_insensitively() {
3735+
let ctx = PolicyContext {
3736+
demotion_label: "needs-review".to_string(),
3737+
..Default::default()
3738+
};
3739+
let item = serde_json::json!({"labels": [{"name": "NEEDS-REVIEW"}]});
3740+
assert!(has_demotion_label(&item, &ctx));
3741+
}
3742+
3743+
#[test]
3744+
fn test_has_demotion_label_empty_string_returns_false() {
3745+
let ctx = PolicyContext::default();
3746+
let item = serde_json::json!({"labels": [{"name": "needs-review"}]});
3747+
assert!(!has_demotion_label(&item, &ctx));
3748+
}
36623749
}

0 commit comments

Comments
 (0)