mr-milchick 3.0.1

CLI for merge request governance in GitLab CI pipelines
Documentation
#![cfg(feature = "github")]

#[path = "support/mock_server.rs"]
mod mock_server;

use mock_server::{GITHUB_PROJECT_KEY, MockGitLabServer, PULL_REQUEST_NUMBER};
use mr_milchick::connectors::github::api::GitHubConfig;
use mr_milchick::connectors::github::{GitHubReviewConnector, MR_MILCHICK_MARKER};
use mr_milchick::core::model::{Actor, ReviewAction, ReviewActionKind};
use mr_milchick::runtime::ReviewConnector;
use serde_json::{Value, json};

fn connector(server: &MockGitLabServer) -> GitHubReviewConnector {
    GitHubReviewConnector::new(
        GitHubConfig {
            base_url: server.github_api_base_url(),
            token: "test-token".to_string(),
        },
        GITHUB_PROJECT_KEY,
        PULL_REQUEST_NUMBER,
        "feat/intentional-cleanup",
        "develop",
        vec!["frontend".to_string()],
    )
}

#[tokio::test]
async fn loads_neutral_snapshot_from_github() {
    let server = MockGitLabServer::start();
    let connector = connector(&server);

    let snapshot = connector
        .load_snapshot()
        .await
        .expect("snapshot should load");

    assert_eq!(snapshot.review_ref.review_id, "3995");
    assert_eq!(snapshot.title, "Frontend adjustments");
    assert_eq!(snapshot.author.username, "arthur");
    assert_eq!(snapshot.changed_files.len(), 1);
    assert_eq!(snapshot.changed_files[0].path, "apps/frontend/button.tsx");
    assert_eq!(snapshot.changed_files[0].previous_path, None);
    assert_eq!(
        snapshot.changed_files[0].patch.as_deref(),
        Some("@@ -1,2 +1,2 @@")
    );
    assert_eq!(snapshot.labels, vec!["frontend".to_string()]);
}

#[tokio::test]
async fn applies_review_actions_idempotently() {
    let server = MockGitLabServer::start();
    let connector = connector(&server);
    let pr_path = format!("/api/github/repos/{GITHUB_PROJECT_KEY}/pulls/{PULL_REQUEST_NUMBER}");
    let comments_path =
        format!("/api/github/repos/{GITHUB_PROJECT_KEY}/issues/{PULL_REQUEST_NUMBER}/comments");

    let actions = vec![
        ReviewAction::AssignReviewers {
            reviewers: vec![
                Actor {
                    username: "principal-reviewer".to_string(),
                    display_name: None,
                },
                Actor {
                    username: "bob".to_string(),
                    display_name: None,
                },
            ],
        },
        ReviewAction::UpsertSummary {
            markdown: "## Summary".to_string(),
        },
    ];

    let first = connector
        .apply_review_actions(&actions)
        .await
        .expect("first apply should succeed");
    assert_eq!(first.applied.len(), 2);
    assert_eq!(
        server.assigned_reviewers(),
        vec!["principal-reviewer", "bob"]
    );
    assert_eq!(server.note_bodies().len(), 1);
    assert!(server.note_bodies()[0].contains(MR_MILCHICK_MARKER));

    let second = connector
        .apply_review_actions(&actions)
        .await
        .expect("second apply should succeed");
    assert_eq!(second.applied[0].action, ReviewActionKind::AssignReviewers);
    assert_eq!(second.skipped[0].action, ReviewActionKind::UpsertSummary);

    assert_eq!(server.request_count("POST", &comments_path), 1);
    assert_eq!(server.request_count("GET", &pr_path), 2);

    let reviewer_assignment_bodies =
        server.request_bodies("POST", &format!("{pr_path}/requested_reviewers"));
    assert_eq!(
        serde_json::from_str::<Value>(&reviewer_assignment_bodies[0])
            .expect("reviewer assignment body should parse"),
        json!({"reviewers": ["principal-reviewer", "bob"]})
    );
}

#[tokio::test]
async fn paginates_changed_files() {
    let server = MockGitLabServer::start_with_github_file_count(101);
    let connector = connector(&server);

    let snapshot = connector
        .load_snapshot()
        .await
        .expect("snapshot should load");

    assert_eq!(snapshot.changed_files.len(), 101);
    let files_path =
        format!("/api/github/repos/{GITHUB_PROJECT_KEY}/pulls/{PULL_REQUEST_NUMBER}/files");
    assert_eq!(server.request_count_prefix("GET", &files_path), 2);
}