use super::*;
use crate::api::{IssueComment, PrData, PullRequest, Review, ReviewComment, User};
use navi_notifier_core::model::{EventKind, ReviewState};
const VIEWER: &str = "me";
const AUTHOR: &str = "octo";
fn user(login: &str) -> User {
User {
login: login.into(),
avatar_url: None,
html_url: None,
}
}
fn ctx() -> DiffContext {
DiffContext {
source_id: "github".into(),
viewer_login: VIEWER.into(),
repo: Repo::new("acme", "widgets"),
now: OffsetDateTime::UNIX_EPOCH,
}
}
fn base_pr() -> PullRequest {
PullRequest {
number: 12,
title: "Add gizmo".into(),
html_url: "https://gh.test/acme/widgets/pull/12".into(),
state: "open".into(),
draft: false,
merged: false,
merged_at: None,
closed_at: None,
updated_at: Some("2024-01-02T03:04:05Z".into()),
merge_commit_sha: None,
user: Some(user(AUTHOR)),
merged_by: None,
requested_reviewers: vec![],
}
}
fn data(pr: PullRequest) -> PrData {
PrData {
pull_request: pr,
reviews: vec![],
review_comments: vec![],
issue_comments: vec![],
}
}
fn initialized() -> PrSnapshot {
PrSnapshot {
initialized: true,
..Default::default()
}
}
fn review(id: u64, login: &str, state: &str) -> Review {
Review {
id,
user: Some(user(login)),
state: state.into(),
submitted_at: Some("2024-01-02T03:04:05Z".into()),
html_url: Some(format!("https://gh.test/r/{id}")),
}
}
fn rcomment(id: u64, login: &str, body: &str, in_reply_to: Option<u64>) -> ReviewComment {
ReviewComment {
id,
user: Some(user(login)),
body: body.into(),
in_reply_to_id: in_reply_to,
html_url: Some(format!("https://gh.test/rc/{id}")),
created_at: Some("2024-01-02T03:04:05Z".into()),
}
}
fn icomment(id: u64, login: &str, body: &str) -> IssueComment {
IssueComment {
id,
user: Some(user(login)),
body: body.into(),
html_url: Some(format!("https://gh.test/ic/{id}")),
created_at: Some("2024-01-02T03:04:05Z".into()),
}
}
fn kinds(events: &[Event]) -> Vec<&EventKind> {
events.iter().map(|e| &e.kind).collect()
}
#[test]
fn first_sighting_emits_outstanding_review_request() {
let mut pr = base_pr();
pr.requested_reviewers = vec![user(VIEWER)];
let (events, snap) = diff(&ctx(), &data(pr), &PrSnapshot::default());
assert_eq!(kinds(&events), vec![&EventKind::ReviewRequested]);
assert!(snap.initialized);
assert!(snap.viewer_requested);
}
#[test]
fn first_sighting_does_not_backfill_history() {
let mut d = data(base_pr());
d.reviews = vec![review(1, "someoneelse", "APPROVED")];
d.issue_comments = vec![icomment(9, "someoneelse", "hey @me look")];
let (events, snap) = diff(&ctx(), &d, &PrSnapshot::default());
assert!(events.is_empty(), "unexpected: {:?}", kinds(&events));
assert!(snap.initialized);
assert!(snap.seen_reviews.contains_key(&1));
assert!(snap.seen_issue_comments.contains(&9));
}
#[test]
fn new_review_request_fires_once() {
let mut pr = base_pr();
pr.requested_reviewers = vec![user(VIEWER)];
let old = initialized();
let (events, snap) = diff(&ctx(), &data(pr.clone()), &old);
assert_eq!(kinds(&events), vec![&EventKind::ReviewRequested]);
let (again, _) = diff(&ctx(), &data(pr), &snap);
assert!(again.is_empty(), "unexpected: {:?}", kinds(&again));
}
#[test]
fn re_review_request_when_already_reviewed() {
let mut pr = base_pr();
pr.requested_reviewers = vec![user(VIEWER)];
let old = PrSnapshot {
viewer_reviewed: true,
..initialized()
};
let (events, _) = diff(&ctx(), &data(pr), &old);
assert_eq!(kinds(&events), vec![&EventKind::ReReviewRequested]);
}
#[test]
fn review_submitted_by_other_on_my_pr() {
let mut pr = base_pr();
pr.user = Some(user(VIEWER));
let mut d = data(pr);
d.reviews = vec![review(5, "reviewer", "CHANGES_REQUESTED")];
let (events, _) = diff(&ctx(), &d, &initialized());
assert_eq!(
kinds(&events),
vec![&EventKind::ReviewSubmitted {
state: ReviewState::ChangesRequested
}]
);
assert_eq!(events[0].actor.login, "reviewer");
}
#[test]
fn my_review_dismissed() {
let mut d = data(base_pr());
d.reviews = vec![review(7, VIEWER, "DISMISSED")];
let old = PrSnapshot {
seen_reviews: [(7, "APPROVED".to_string())].into_iter().collect(),
viewer_reviewed: true,
..initialized()
};
let (events, _) = diff(&ctx(), &d, &old);
assert_eq!(kinds(&events), vec![&EventKind::ReviewDismissed]);
}
#[test]
fn reply_to_my_review_comment_is_on_your_comment() {
let mut d = data(base_pr());
d.review_comments = vec![
rcomment(100, VIEWER, "please rename this", None), rcomment(101, "collab", "done!", Some(100)), ];
let old = PrSnapshot {
seen_review_comments: [100].into_iter().collect(),
..initialized()
};
let (events, _) = diff(&ctx(), &d, &old);
assert_eq!(
kinds(&events),
vec![&EventKind::CommentReply {
on_your_comment: true
}]
);
assert_eq!(events[0].excerpt.as_deref(), Some("done!"));
}
#[test]
fn comment_in_thread_i_never_joined_is_ignored() {
let mut d = data(base_pr());
d.review_comments = vec![
rcomment(200, "alice", "nit", None),
rcomment(201, "bob", "agreed", Some(200)),
];
let (events, _) = diff(&ctx(), &d, &initialized());
assert!(events.is_empty(), "unexpected: {:?}", kinds(&events));
}
#[test]
fn mention_beats_reply() {
let mut d = data(base_pr());
d.issue_comments = vec![icomment(300, "alice", "hey @me can you take a look?")];
let (events, _) = diff(&ctx(), &d, &initialized());
assert_eq!(kinds(&events), vec![&EventKind::Mentioned]);
}
#[test]
fn merged_closed_ready_transitions() {
let mut pr = base_pr();
pr.merged = true;
pr.state = "closed".into();
pr.merged_at = Some("2024-01-03T00:00:00Z".into());
pr.merge_commit_sha = Some("abc123".into());
pr.merged_by = Some(user("merger"));
let (events, _) = diff(&ctx(), &data(pr), &initialized());
assert_eq!(kinds(&events), vec![&EventKind::Merged]);
assert_eq!(events[0].actor.login, "merger");
let mut pr = base_pr();
pr.state = "closed".into();
pr.closed_at = Some("2024-01-03T00:00:00Z".into());
let (events, _) = diff(&ctx(), &data(pr), &initialized());
assert_eq!(kinds(&events), vec![&EventKind::Closed]);
let pr = base_pr(); let old = PrSnapshot {
draft: true,
..initialized()
};
let (events, _) = diff(&ctx(), &data(pr), &old);
assert_eq!(kinds(&events), vec![&EventKind::ReadyForReview]);
}
#[test]
fn mentions_respects_boundaries() {
assert!(mentions("ping @me now", "me"));
assert!(mentions("@Me at the start", "me")); assert!(mentions("(@me)", "me"));
assert!(!mentions("email me@host.com", "me")); assert!(!mentions("hi @mentor", "me")); assert!(!mentions("nothing here", "me"));
}