//! Opening pulls and issues from the pages, as the account logged in to //! them: the templates they are asked to follow, what a branch would bring //! in, and the writes, through the same code as the command line. use super::*; #[test] fn opening_pulls_and_issues_from_the_pages() { // Started first, so its index sees the repositories made after. let server = gateway_with(None, true); let owner = person(account()); let url = owner.url("open"); let work = owner.work("w", 1); std::fs::create_dir_all(work.join(".knotless")).unwrap(); owner.commit_file( &work, ".knotless/issue.md", "## What\n\n\n", ); owner.commit_file(&work, ".knotless/pull.md", "## Why\n"); owner.git(&work, &["push", "-q", &url, "main"]); owner.git(&work, &["checkout", "-q", "-b", "feature"]); owner.commit_file(&work, "g", "feature\n"); owner.git(&work, &["push", "-q", &url, "feature"]); let fan = person(account()); assert_eq!(fan.cli(&["repo", "fork", &url, "open-too"], b"").0, 0); let fork = fan.url("open-too"); let mine = fan.work("mine", 0); fan.git(&mine, &["fetch", "-q", &fork, "main"]); fan.git(&mine, &["checkout", "-q", "-b", "fix", "FETCH_HEAD"]); fan.commit_file(&mine, "h", "fixed\n"); fan.git(&mine, &["push", "-q", &fork, "fix"]); let repo = format!("at://{}/dev.knotless.repo/open", owner.account.did); let forked = format!("at://{}/dev.knotless.repo/open-too", fan.account.did); let xrpc = |nsid: &str| format!("{}/xrpc/dev.knotless.{nsid}", server.base); let get = |nsid: &str, query: &str| { let url = format!("{}?repo={repo}{query}", xrpc(nsid)); browse("GET", &url, None, None, None) }; // What each kind is asked to follow. let (status, _, issue) = get("getTemplate", "&kind=issue"); assert_eq!(status, 200, "{issue}"); assert_eq!(issue["template"], "## What\n\n\n"); assert_eq!( get("getTemplate", "&kind=pull&base=main").2["template"], "## Why\n" ); assert_eq!(get("getTemplate", "&kind=pull&base=nope").2, json!({})); assert_eq!(get("getTemplate", "&kind=wiki").0, 400); // Where a pull can come from, and what a branch brings in. wait_until(|| { get("getCompare", "").2["heads"] .as_array() .map_or(0, Vec::len) == 2 }); let heads = get("getCompare", "").2["heads"].clone(); assert_eq!(heads[0]["name"], "open"); assert_eq!( (&heads[1]["did"], &heads[1]["name"]), (&json!(fan.account.did), &json!("open-too")) ); let (status, _, feature) = get("getCompare", "&base=main&branch=feature"); assert_eq!(status, 200, "{feature}"); assert_eq!(feature["changes"]["commits"].as_array().unwrap().len(), 1); assert_eq!(feature["changes"]["files"][0]["new"], "g"); wait_until(|| get("getCompare", &format!("&head={forked}&branch=fix")).0 == 200); let fix = get("getCompare", &format!("&head={forked}&branch=fix")).2; assert_eq!(fix["changes"]["files"][0]["new"], "h"); assert_eq!(get("getCompare", "&branch=nope").0, 404); // Opening them, as the account logged in, as the templates ask. let set = log_in(&server, &fan.account, ""); let found = set.iter().find(|c| c.starts_with("knotless_session=")); let cookie = pair(found.unwrap()).to_string(); let me = Some(cookie.as_str()); let write = |nsid: &str, cookie: Option<&str>, body: Value| { browse("POST", &xrpc(nsid), cookie, None, Some(body)) }; let bare = json!({"repo": repo, "title": "Broken", "body": "It is."}); let (status, _, refused) = write("createIssue", me, bare.clone()); assert_eq!(status, 400); assert!( refused["message"].as_str().unwrap().contains("## What"), "{refused}" ); assert_eq!(write("createIssue", None, bare).0, 401); let asked = json!({"repo": repo, "title": "Broken", "body": "## What\n\nIt is."}); let (status, _, opened) = write("createIssue", me, asked); assert_eq!(status, 200, "{opened}"); let issue = opened["uri"].as_str().unwrap().to_string(); assert!(issue.starts_with(&format!("at://{}/dev.knotless.issue/", fan.account.did))); let thread = |uri: &str| { let url = format!("{}?repo={repo}&uri={uri}", xrpc("getThread")); browse("GET", &url, None, None, None).2 }; assert_eq!(thread(&issue)["item"]["title"], "Broken"); let pull = |head: &str, branch: &str| json!({"repo": repo, "base": "main", "head": head, "branch": branch, "title": "Fix it", "body": "## Why\n\nIt was broken."}); let (status, _, opened) = write("createPull", me, pull(&forked, "fix")); assert_eq!(status, 200, "{opened}"); let shown = thread(opened["uri"].as_str().unwrap()); assert_eq!(shown["head"]["branch"], "fix"); assert_eq!(shown["head"]["repo"]["name"], "open-too"); assert_eq!(write("createPull", me, pull(&forked, "nope")).0, 400); let nowhere = format!("at://{}/dev.knotless.repo/nothing", fan.account.did); assert_eq!(write("createPull", me, pull(&nowhere, "fix")).0, 404); // Reviewers asked for as it opens. let mut asking = pull(&forked, "fix"); asking["reviewers"] = json!([{"did": owner.account.did}]); let (status, _, opened) = write("createPull", me, asking); assert_eq!(status, 200, "{opened}"); let asked_pull = opened["uri"].as_str().unwrap().to_string(); let shown = thread(&asked_pull); assert_eq!(shown["item"]["reviewers"][0]["did"], owner.account.did); // And asked for, or no longer, once it is open, as its author; an // issue asks no one. let them = json!([{"did": owner.account.did}]); let stop = json!({"repo": repo, "subject": asked_pull, "removeReviewers": them}); let (status, _, stopped) = write("editItem", me, stop); assert_eq!(status, 200, "{stopped}"); // None asked: the view leaves the list out. let none = thread(&asked_pull)["item"]["reviewers"].clone(); assert!(none.as_array().is_none_or(Vec::is_empty), "{none}"); let again = json!({"repo": repo, "subject": asked_pull, "addReviewers": them}); assert_eq!(write("editItem", me, again).0, 200); let shown = thread(&asked_pull); assert_eq!(shown["item"]["reviewers"][0]["did"], owner.account.did); let of_issue = json!({"repo": repo, "subject": issue, "addReviewers": them}); assert_eq!(write("editItem", me, of_issue).0, 400); // Labels put on as it opens, by those who may put them on: the // owner, whose pages offer them, and not the fan, who is refused // before anything is written. let add = ["label", "define", &url, "kind/bug"]; assert_eq!(owner.cli(&add, b"").0, 0); let set = log_in(&server, &owner.account, ""); let found = set.iter().find(|c| c.starts_with("knotless_session=")); let owns = pair(found.unwrap()).to_string(); let offered = |cookie: &str| { let url = format!("{}?repo={repo}", xrpc("getRepo")); browse("GET", &url, Some(cookie), None, None).2["viewer"]["labels"].clone() }; wait_until(|| offered(&owns)[0]["name"] == "kind/bug"); assert_eq!(offered(&cookie), Value::Null); let labels = json!([{"name": "kind/bug"}]); let body = "## What\n\nStill broken."; let labelled = json!({"repo": repo, "title": "Broken", "body": body, "labels": labels}); let (status, _, refused) = write("createIssue", me, labelled.clone()); assert_eq!(status, 400); assert!( refused["message"].as_str().unwrap().contains("kind/bug"), "{refused}" ); let (status, _, opened) = write("createIssue", Some(&owns), labelled); assert_eq!(status, 200, "{opened}"); let shown = thread(opened["uri"].as_str().unwrap()); assert_eq!(shown["item"]["labels"][0]["name"], "kind/bug"); let mut onto = pull(&forked, "fix"); onto["labels"] = labels; let (status, _, opened) = write("createPull", Some(&owns), onto); assert_eq!(status, 200, "{opened}"); let shown = thread(opened["uri"].as_str().unwrap()); assert_eq!(shown["item"]["labels"][0]["name"], "kind/bug"); // Assigned by the owner, who may, and not by the fan; and asked, by // the owner too, to review the fan's pull. let viewing = |cookie: &str, uri: &str| { let url = format!("{}?repo={repo}&uri={uri}", xrpc("getThread")); browse("GET", &url, Some(cookie), None, None).2["viewer"].clone() }; assert_eq!(viewing(&owns, &issue)["assign"], true); assert_eq!(viewing(&cookie, &issue)["assign"], Value::Null); let fan_did = fan.account.did.as_str(); let assigning = json!({"repo": repo, "subject": issue, "did": fan_did}); assert_eq!(write("assignItem", me, assigning.clone()).0, 400); let (status, _, assigned) = write("assignItem", Some(&owns), assigning); assert_eq!(status, 200, "{assigned}"); assert_eq!(assigned["assignees"][0]["did"], fan_did); assert_eq!(thread(&issue)["item"]["assignees"][0]["did"], fan_did); let owner_did = owner.account.did.as_str(); let mut asking = json!({"repo": repo, "subject": asked_pull, "did": owner_did, "role": "reviewer"}); let (status, _, asked) = write("assignItem", Some(&owns), asking.clone()); assert_eq!(status, 200, "{asked}"); assert_eq!(asked["reviewers"][0]["did"], owner_did); // Its author asks in the pull itself. let (status, _, asked) = write("assignItem", me, asking.clone()); assert_eq!(status, 200, "{asked}"); let mine = thread(&asked_pull)["item"]["reviewers"].clone(); assert_eq!(mine.as_array().map(Vec::len), Some(1), "{mine}"); asking["role"] = "boss".into(); assert_eq!(write("assignItem", Some(&owns), asking).0, 400); // The owner asks for changes to the fan's pull, then dismisses that // review, which the fan may not. main waits for the fan's review too, // which the owner's is not. let gated = [ "branch", "protect", &url, "main", "--gate", &fan.account.handle, ]; assert_eq!(owner.cli(&gated, b"").0, 0); assert_eq!(viewing(&owns, &asked_pull)["dismiss"], true); assert_eq!(viewing(&owns, &issue)["dismiss"], Value::Null); let review = json!({"repo": repo, "subject": asked_pull, "body": "Not yet", "verdict": "changes"}); let (status, _, reviewed) = write("createComment", Some(&owns), review); assert_eq!(status, 200, "{reviewed}"); let dismissing = json!({"repo": repo, "pull": asked_pull, "reviewer": owner_did, "reason": "Moot"}); assert_eq!(write("dismissReview", me, dismissing.clone()).0, 400); let (status, _, dismissed) = write("dismissReview", Some(&owns), dismissing); assert_eq!(status, 200, "{dismissed}"); let shown = thread(&asked_pull); assert_eq!(shown["reviews"][0]["dismissed"], true, "{shown}"); assert_eq!(shown["reviews"][0].get("gate"), None, "{shown}"); let said = shown["comments"].as_array().unwrap(); let dismissal = said.iter().find(|c| c["uri"] == dismissed["uri"]).unwrap(); assert_eq!(dismissal["dismisses"], reviewed["uri"]); }