//! Owners delegate their writes and share repositories from the pages, //! and make, share and land private ones there, through the same code as //! the command line. use super::*; /// Runs the browser that approves as `who` on the page at `url`. fn approve(who: &Account, url: &str) { let command = format!("{} {url}", browser(who, "")); let mut words = command.split_whitespace(); let out = std::process::Command::new(words.next().unwrap()) .args(words) .output() .unwrap(); assert!( out.status.success(), "{}", String::from_utf8_lossy(&out.stderr) ); } /// An owner delegates their writes to the server from the pages, as /// `knotless account delegate` does, and can then add a collaborator there; and a /// pull into a repository with a runner but no workflows waits for no /// checks. #[test] fn delegating_writes_from_the_pages() { // Started first, so its index sees the repositories made after. let server = gateway_with(None, true); let owner = person(account()); let collab = account(); let url = owner.url("shared"); let work = owner.work("w", 1); owner.git(&work, &["push", "-q", &url, "main"]); owner.git(&work, &["checkout", "-q", "-b", "x"]); owner.commit_file(&work, "x", "x\n"); owner.git(&work, &["push", "-q", &url, "x"]); let (code, pull) = owner.cli(&["pull", "create", &url, "main", &url, "x", "X"], b""); assert_eq!(code, 0, "{pull}"); let mine = session_cookie(&server, &owner.account); let me = Some(mine.as_str()); let xrpc = |nsid: &str| format!("{}/xrpc/dev.knotless.{nsid}", server.base); let write = |nsid: &str, cookie: Option<&str>, body: Value| { browse("POST", &xrpc(nsid), cookie, None, Some(body)) }; let repo = format!("at://{}/dev.knotless.repo/shared", owner.account.did); let ask = |nsid: &str, query: &str| { let url = format!("{}?repo={repo}{query}", xrpc(nsid)); browse("GET", &url, me, None, None).2 }; // Not yet delegated, sharing for writing says to delegate first. assert_eq!(ask("getRepoSettings", "").get("delegated"), None); let share = json!({"repo": repo, "with": collab.handle}); let (code, _, said) = write("shareRepo", me, share.clone()); assert_eq!(code, 400, "{said}"); assert!( said["message"] .as_str() .unwrap() .contains("Delegate writes to this server"), "{said}" ); // Only someone logged in delegates; nothing waits under a state never // given. assert_eq!(write("delegateWrites", None, json!({})).0, 401); let nope = json!({"state": "nope"}); assert_eq!(write("awaitDelegation", None, nope.clone()).0, 401); assert_eq!(write("awaitDelegation", me, nope).0, 400); // Started, approved at the PDS, and waited for. let (code, _, started) = write("delegateWrites", me, json!({})); assert_eq!(code, 200, "{started}"); approve(&owner.account, started["url"].as_str().unwrap()); let state = json!({"state": started["state"]}); let mut said = json!({"done": false}); while said["done"] == false { let (code, _, now) = write("awaitDelegation", me, state.clone()); assert_eq!(code, 200, "{now}"); said = now; } assert_eq!(said["gateway"], server.did); assert_eq!(ask("getRepoSettings", "")["delegated"], server.did); // Now a collaborator is added, through this server. let (code, _, said) = write("shareRepo", me, share); assert_eq!(code, 200, "{said}"); assert_eq!(said["collaborators"][0]["did"], collab.did); assert_eq!(ask("getRepoSettings", "")["gateway"], server.did); // With a runner but no workflow a pull runs, nothing waits for checks. let runner = json!({"repo": repo, "runner": collab.handle}); assert_eq!(write("addRunner", me, runner).0, 200); let said = ask("getMergeStatus", &format!("&pull={pull}")); assert_eq!( (&said["state"], &said["checks"]), (&json!("clean"), &json!("none")), "{said}" ); } /// A private repository made from the pages, shared there for reading, /// and its pull landed there, each read and written as the account /// logged in, never as the server. #[test] fn private_repositories_made_and_shared_from_the_pages() { let server = gateway_with(None, true); let [owner, member] = people(); let mine = session_cookie(&server, &owner.account); let theirs = session_cookie(&server, &member.account); let (me, them) = (Some(mine.as_str()), Some(theirs.as_str())); let xrpc = |nsid: &str| format!("{}/xrpc/dev.knotless.{nsid}", server.base); let write = |nsid: &str, cookie: Option<&str>, body: Value| { browse("POST", &xrpc(nsid), cookie, None, Some(body)) }; let made = write("createRepo", me, json!({"name": "secret", "private": true})); if !net().spaces { // Without spaces, there is no private repository to make. assert_eq!(made.0, 400, "{}", made.2); return; } assert_eq!(made.0, 200, "{}", made.2); let (_, listed) = owner.cli(&["repo", "list", &owner.account.handle], b""); let private = listed.lines().any(|l| l == "secret\t\tprivate"); assert!(private, "{listed}"); let url = owner.url("secret"); let work = owner.work("w", 1); owner.git(&work, &["push", "-q", &url, "main"]); owner.git(&work, &["checkout", "-q", "-b", "x"]); let head = owner.commit_file(&work, "x", "x\n"); owner.git(&work, &["push", "-q", &url, "x"]); let (code, pull) = owner.cli(&["pull", "create", &url, "main", &url, "x", "X"], b""); assert_eq!(code, 0, "{pull}"); let repo = format!("at://{}/dev.knotless.repo/secret", owner.account.did); let ask = |nsid: &str, query: &str, cookie: Option<&str>| { let url = format!("{}?repo={repo}{query}", xrpc(nsid)); browse("GET", &url, cookie, None, None) }; let readers = || ask("getRepoSettings", "", me).2["readers"].clone(); // Shared for reading, the member is listed to its owner, and reads it. assert_eq!(readers(), json!([])); assert_eq!(ask("getRepo", "", them).0, 404); let share = json!({"repo": repo, "with": member.account.handle, "write": false}); let (code, _, said) = write("shareRepo", me, share); assert_eq!((code, &said), (200, &json!({"collaborators": []}))); assert_eq!(readers()[0]["did"], member.account.did); let (code, _, shown) = ask("getRepo", "", them); assert_eq!((code, &shown["private"]), (200, &json!(true)), "{shown}"); // Only its owner sees who reads it. assert_eq!(ask("getRepoSettings", "", them).2["readers"], json!([])); // Its owner changes it, which no public index holds. let described = json!({"repo": repo, "description": "Hush"}); assert_eq!(write("updateRepo", me, described).0, 200); // Its pull's merge box, workflows and runners read as each asker. let status = |cookie| ask("getMergeStatus", &format!("&pull={pull}"), cookie); let (code, _, said) = status(them); assert_eq!(code, 200, "{said}"); assert_eq!( (&said["state"], &said["may"]["merge"]), (&json!("clean"), &json!(false)) ); assert_eq!(status(me).2["may"]["merge"], true); let listed = ask("listWorkflows", "", them); assert_eq!(listed.0, 200, "{}", listed.2); assert_eq!(ask("listRunners", "", them).2, json!({"runners": []})); let one = json!({"repo": repo, "pull": pull}); assert_eq!(write("mergePull", them, one.clone()).0, 400); let (code, _, merged) = write("mergePull", me, one); assert_eq!(code, 200, "{merged}"); assert!( merged["commit"].is_string() && merged["commit"] != json!(head), "{merged}" ); // Unshared, they read it no more. let unshare = json!({"repo": repo, "with": member.account.did}); assert_eq!(write("unshareRepo", me, unshare).0, 200); assert_eq!(readers(), json!([])); }