~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
136 lines · 6.0 KB Raw History
//! 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")));
}