hive-forge: 10 behaviour tests for lint's pure helpers, which had none

WIP commit so the mutation run below has a clean base to restore to.
This commit is contained in:
atlas 2026-09-02 11:17:14 +02:00
commit a78e783c3b

View file

@ -497,6 +497,166 @@ fn num(it: &Value) -> i64 {
it.get("number").and_then(Value::as_i64).unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
/// Every field on the forgejo structs is `Option`, so a fixture names
/// only what a case is about. Going through `from_value` rather than a
/// struct literal means these exercise the same deserialisation a real
/// API response does — a wire-shape change breaks the tests too.
fn issue(v: Value) -> Issue {
serde_json::from_value(v).expect("fixture is a valid Issue")
}
fn pull(v: Value) -> PullRequest {
serde_json::from_value(v).expect("fixture is a valid PullRequest")
}
/// `exclusive` is Forgejo's real scoped-label marker. A label that
/// merely *looks* scoped (a `/` in a plain label's name) must not
/// count, or `lint unlabeled` silently stops reporting an item that
/// genuinely has no `type/*`.
#[test]
fn only_an_exclusive_label_counts_as_scoped() {
let scoped = issue(json!({ "labels": [{ "name": "area/ops", "exclusive": true }] }));
assert!(has_scoped_label(&scoped, "area/"));
let lookalike = issue(json!({ "labels": [{ "name": "area/ops", "exclusive": false }] }));
assert!(
!has_scoped_label(&lookalike, "area/"),
"a non-exclusive label with a slash in it is not a scoped label"
);
let unset = issue(json!({ "labels": [{ "name": "area/ops" }] }));
assert!(!has_scoped_label(&unset, "area/"), "absent means not exclusive");
}
/// The caller builds the prefix as `"<scope>/"`, which is what stops
/// `--scope area` from matching a label called `areaology`.
#[test]
fn the_scope_prefix_stops_at_the_slash() {
let other = issue(json!({ "labels": [{ "name": "areaology", "exclusive": true }] }));
assert!(!has_scoped_label(&other, "area/"));
let wrong_scope = issue(json!({ "labels": [{ "name": "type/bug", "exclusive": true }] }));
assert!(!has_scoped_label(&wrong_scope, "area/"));
assert!(has_scoped_label(&wrong_scope, "type/"));
}
#[test]
fn an_item_with_no_labels_has_no_scoped_label() {
assert!(!has_scoped_label(&issue(json!({ "labels": [] })), "area/"));
assert!(!has_scoped_label(&issue(json!({})), "area/"));
}
/// `--reviewer NAME` and a bare `no-reviewer` are different questions:
/// "is X on it" versus "is anyone on it".
#[test]
fn requested_reviewer_asks_a_different_question_with_and_without_a_name() {
let pr = pull(json!({ "requested_reviewers": [{ "login": "argus" }] }));
assert!(has_requested_reviewer(&pr, None), "someone is requested");
assert!(has_requested_reviewer(&pr, Some("argus")));
assert!(!has_requested_reviewer(&pr, Some("iris")));
let bare = pull(json!({ "requested_reviewers": [] }));
assert!(!has_requested_reviewer(&bare, None));
assert!(!has_requested_reviewer(&bare, Some("argus")));
// `null`, not an omitted key: `requested_reviewers` carries a
// `deserialize_with`, so serde requires it to be present even though
// its type is `Option`. The forge sends the key with a null value.
let absent = pull(json!({ "requested_reviewers": null }));
assert!(!has_requested_reviewer(&absent, None), "null is not a reviewer");
}
#[test]
fn logins_drops_users_that_have_none() {
let users: Vec<forgejo_api::structs::User> =
serde_json::from_value(json!([{ "login": "atlas" }, {}, { "login": "mara" }]))
.expect("fixture");
assert_eq!(logins(Some(&users)), vec!["atlas", "mara"]);
assert!(logins(None).is_empty());
}
/// Issues and PRs come back from the *same* endpoint; the only thing
/// telling them apart is whether `pull_request` is present. Getting
/// this wrong mislabels every row `lint` prints.
#[test]
fn is_pr_is_decided_by_the_pull_request_object() {
let plain = trim_issue(&issue(json!({ "number": 7, "title": "t" })));
assert_eq!(plain["is_pr"], json!(false));
assert_eq!(kind_label(&plain), "issue");
let pr = trim_issue(&issue(json!({
"number": 8, "title": "t", "pull_request": { "merged": false },
})));
assert_eq!(pr["is_pr"], json!(true));
assert_eq!(kind_label(&pr), "pr");
}
#[test]
fn trim_issue_keeps_the_fields_the_output_prints() {
let v = trim_issue(&issue(json!({
"number": 42,
"title": "a title",
"assignees": [{ "login": "atlas" }, { "login": "argus" }],
})));
assert_eq!(num(&v), 42);
assert_eq!(title(&v), "a title");
assert_eq!(v["assignees"], json!(["atlas", "argus"]));
}
#[test]
fn accessors_survive_a_row_that_is_missing_everything() {
let empty = json!({});
assert_eq!(num(&empty), 0);
assert_eq!(title(&empty), "");
assert_eq!(kind_label(&empty), "issue");
}
/// `All` is **absence** of the filter, not a third value — the forge
/// returns both kinds when `type` is omitted. Turning it into a value
/// would silently narrow every `--type all` query.
#[test]
fn the_both_kinds_filter_is_an_absent_query_param() {
assert!(Kind::All.query_type().is_none());
assert!(matches!(
Kind::Issues.query_type(),
Some(IssueListIssuesQueryType::Issues)
));
assert!(matches!(
Kind::Pulls.query_type(),
Some(IssueListIssuesQueryType::Pulls)
));
}
#[test]
fn state_maps_onto_both_query_enums() {
assert!(matches!(
State::Open.issue_state(),
IssueListIssuesQueryState::Open
));
assert!(matches!(
State::Closed.issue_state(),
IssueListIssuesQueryState::Closed
));
assert!(matches!(State::All.issue_state(), IssueListIssuesQueryState::All));
assert!(matches!(
State::Open.pull_state(),
RepoListPullRequestsQueryState::Open
));
assert!(matches!(
State::Closed.pull_state(),
RepoListPullRequestsQueryState::Closed
));
assert!(matches!(
State::All.pull_state(),
RepoListPullRequestsQueryState::All
));
}
}
fn title(it: &Value) -> &str {
it.get("title").and_then(Value::as_str).unwrap_or("")
}