johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! The appview knows an organization's members from its index: a member's
//! push is credited to them, their merge lands a pull, and `getOrg` and
//! `getProfile` name them, with the invitations and hidden members only to
//! those inside.
use super::*;
/// An organization, a member in public, one invited (and on spaces, a
/// hidden member), and an outsider, as the pages and their queries see
/// them.
#[test]
fn the_appview_counts_organization_members() {
// Started first, so its index sees the records made after.
let server = gateway_with(None, true);
let mut all: [Sandbox; 4] = people();
// Organizations, and pulls on public repositories, are read from the
// appview.
for p in &mut all {
p.set("KNOTLESS_APPVIEW", &server.base);
}
let [org, member, hidden, outsider] = all;
let ok = |s: &Sandbox, args: &[&str]| {
let (code, out) = s.cli(args, b"");
assert_eq!(code, 0, "{args:?}: {out}");
out
};
let o = org.account.handle.as_str();
ok(&org, &["account", "delegate", o, &server.did]);
ok(&org, &["org", "add", &member.account.handle]);
ok(&org, &["org", "add", &hidden.account.handle]);
let url = org.url("shared");
// Made once an organization, it names the gateway members push through.
ok(&org, &["repo", "create", &url]);
let work = org.work("w", 1);
org.git(&work, &["push", "-q", &url, "main"]);
org.git(&work, &["checkout", "-q", "-b", "x"]);
org.commit_file(&work, "x", "x\n");
org.git(&work, &["push", "-q", &url, "x"]);
let pull = ok(&org, &["pull", "create", &url, "main", &url, "x", "X"]);
ok(&member, &["org", "join", o, "--public"]);
// On spaces, a hidden member; else an invitation in public.
if net().spaces {
ok(&hidden, &["org", "join", o, "--hidden"]);
}
let xrpc =
|nsid: &str, query: &str| format!("{}/xrpc/dev.knotless.{nsid}?{query}", server.base);
let ask = |nsid: &str, query: &str, cookie: Option<&str>| {
browse("GET", &xrpc(nsid, query), cookie, None, None)
};
let actor = |s: &Sandbox| json!({"did": s.account.did, "handle": s.account.handle});
let get_org = |cookie: Option<&str>| ask("getOrg", &format!("actor={o}"), cookie).2;
let profile = |s: &Sandbox, cookie: Option<&str>| {
ask("getProfile", &format!("actor={}", s.account.did), cookie).2
};
// The member's push is theirs, as a collaborator's is.
let theirs = member.work("theirs", 1);
member.git(&theirs, &["push", "-q", &url, "main:refs/heads/theirs"]);
let activity = format!("actor={}", member.account.did);
wait_until(|| {
let days = ask("getActorActivity", &activity, None).2["days"].clone();
let days = days.as_array().cloned().unwrap_or_default();
days.iter().any(|d| d["pushes"].as_u64() > Some(0))
});
// Their merge lands the pull, as the index reads the repository.
let repo = format!("at://{}/dev.knotless.repo/shared", org.account.did);
let thread = format!("repo={repo}&uri={pull}");
wait_until(|| ask("getThread", &thread, None).2["item"]["state"] == "open");
ok(&member, &["pull", "merge", &url, &pull]);
wait_until(|| ask("getThread", &thread, None).2["item"]["state"] == "merged");
// Anyone sees its members in public, and the member its organization.
let public = json!([{
"actor": actor(&member), "role": "member", "standing": "public",
}]);
let shown = |answer: &Value| {
let mut members = answer["members"].clone();
for m in members.as_array_mut().into_iter().flatten() {
let m = m.as_object_mut().unwrap();
assert!(m.remove("invitedAt").is_some(), "{m:?}");
// Each invitation says when it lapses.
assert!(m.remove("expiresAt").is_some(), "{m:?}");
m.remove("joinedAt");
}
members
};
wait_until(|| shown(&get_org(None)) == public);
let outside = session_cookie(&server, &outsider.account);
let seen = get_org(Some(&outside));
assert_eq!(
(&seen["org"], &seen["organization"], &seen["inside"]),
(&actor(&org), &json!(true), &json!(false))
);
assert_eq!(shown(&seen), public);
let of = json!([{"actor": actor(&org)}]);
assert_eq!(profile(&member, None)["orgs"], of);
assert_eq!(profile(&outsider, None).get("orgs"), None);
// Its profile says it is one, so the pages show its members.
assert_eq!(profile(&org, None)["organization"], true);
assert_eq!(profile(&member, None).get("organization"), None);
// Inside, its invitations and hidden members too.
let standing = if net().spaces { "hidden" } else { "invited" };
let all = json!([
{"actor": actor(&member), "role": "member", "standing": "public"},
{"actor": actor(&hidden), "role": "member", "standing": standing},
]);
let mine = session_cookie(&server, &org.account);
let as_org = get_org(Some(&mine));
assert_eq!(as_org["inside"], true, "{as_org}");
let mut listed = shown(&as_org);
let by_did = |v: &Value| v["actor"]["did"].as_str().unwrap_or_default().to_string();
listed.as_array_mut().unwrap().sort_by_key(by_did);
let mut want = all.clone();
want.as_array_mut().unwrap().sort_by_key(by_did);
assert_eq!(listed, want);
if net().spaces {
// A hidden member sees it all, and their membership on their own
// profile, which no one else sees.
let theirs = session_cookie(&server, &hidden.account);
assert_eq!(get_org(Some(&theirs))["inside"], true);
assert_eq!(profile(&hidden, Some(&theirs))["orgs"], of);
assert_eq!(profile(&hidden, None).get("orgs"), None);
}
// An account that is none, and one there is not.
let none = ask("getOrg", &format!("actor={}", outsider.account.did), None).2;
assert_eq!(
(&none["organization"], &none["members"]),
(&json!(false), &json!([]))
);
let (code, _, said) = ask("getOrg", "actor=nobody.invalid", None);
assert_eq!((code, &said["error"]), (400, &json!("ProfileNotFound")));
}