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

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
424 lines · 18.6 KB Raw History
//! Saying things on pulls and issues from the pages, as the account logged
//! in to them: commenting and reviewing, editing and deleting what one
//! wrote, closing and reopening, labelling, hiding and reporting, through
//! the same code as the command line.

use super::*;

/// The `name=value` of the session cookie a login set.
fn session(set: &[String]) -> String {
    let found = set.iter().find(|c| c.starts_with("knotless_session="));
    pair(found.unwrap()).to_string()
}

#[test]
fn talking_on_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("talk");
    let work = owner.work("w", 1);
    owner.git(&work, &["push", "-q", &url, "main", "main:feature"]);
    assert_eq!(owner.cli(&["label", "define", &url, "kind/bug"], b"").0, 0);
    let mut author = person(account());
    let (code, issue) = author.cli(&["issue", "create", &url, "Broken", "-m", "It is."], b"");
    assert_eq!(code, 0, "{issue}");
    let (code, pull) = author.cli(
        &["pull", "create", &url, "main", &url, "feature", "Fix"],
        b"",
    );
    assert_eq!(code, 0, "{pull}");
    let repo = format!("at://{}/dev.knotless.repo/talk", owner.account.did);
    let xrpc = |nsid: &str| format!("{}/xrpc/dev.knotless.{nsid}", server.base);
    let thread = |uri: &str, cookie: Option<&str>| {
        let url = format!("{}?repo={repo}&uri={uri}", xrpc("getThread"));
        browse("GET", &url, cookie, None, None).2
    };
    let write = |nsid: &str, cookie: Option<&str>, body: Value| {
        browse("POST", &xrpc(nsid), cookie, None, Some(body))
    };
    wait_until(|| thread(&pull, None)["item"]["title"] == "Fix");
    wait_until(|| thread(&issue, None)["item"]["title"] == "Broken");
    let mine = session(&log_in(&server, &author.account, ""));
    let me = Some(mine.as_str());
    let theirs = session(&log_in(&server, &owner.account, ""));
    let owning = Some(theirs.as_str());

    // What each may do, past saying something: no one asking, nothing.
    assert_eq!(thread(&issue, None).get("viewer"), None);
    let author_may = json!({"setState": true, "hide": false, "notifiedFor": "author"});
    assert_eq!(thread(&issue, me)["viewer"], author_may);
    let owner_may = &thread(&issue, owning)["viewer"];
    assert_eq!(
        (&owner_may["setState"], &owner_may["hide"]),
        (&json!(true), &json!(true))
    );
    assert_eq!(owner_may["labels"][0]["name"], "kind/bug");

    // Commenting, as the account logged in, and only so.
    let said = json!({"repo": repo, "subject": issue, "body": "Me too."});
    let (status, _, wrote) = write("createComment", me, said.clone());
    assert_eq!(status, 200, "{wrote}");
    let comment = wrote["uri"].as_str().unwrap().to_string();
    assert!(comment.starts_with(&format!(
        "at://{}/dev.knotless.comment/",
        author.account.did
    )));
    let shown = thread(&issue, me);
    assert_eq!(shown["comments"][0]["uri"], comment.as_str());
    assert_eq!(shown["comments"][0]["body"], "Me too.");
    assert_eq!(write("createComment", None, said.clone()).0, 401);
    let away = browse(
        "POST",
        &xrpc("createComment"),
        me,
        Some("https://evil.example"),
        Some(said),
    );
    assert_eq!(away.0, 403);
    let empty = json!({"repo": repo, "subject": issue, "body": " "});
    assert_eq!(write("createComment", me, empty).0, 400);
    let nowhere = json!({"repo": repo, "subject": format!("{issue}x"), "body": "?"});
    assert_eq!(write("createComment", me, nowhere).0, 400);
    let odd = json!({"repo": repo, "subject": issue, "body": "Hm", "verdict": "maybe"});
    let (status, _, said) = write("createComment", me, odd);
    assert_eq!(status, 400, "{said}");
    assert!(
        said.to_string().contains("a verdict is approve or changes"),
        "{said}"
    );

    // A review, by the owner, says what it found, and the pull's author
    // hears of it, on the pages and the command line.
    let review = json!({"repo": repo, "subject": pull, "body": "", "verdict": "changes"});
    let (status, _, reviewed) = write("createComment", owning, review);
    assert_eq!(status, 200, "{reviewed}");
    assert_eq!(thread(&pull, None)["comments"][0]["verdict"], "changes");
    let heard = || browse("GET", &xrpc("listNotifications"), me, None, None).2;
    wait_until(|| heard()["notifications"][0]["verdict"] == "changes");
    assert_eq!(heard()["notifications"][0]["reasonSubject"], pull.as_str());
    author.set("KNOTLESS_APPVIEW", &server.base);
    let (code, out) = author.cli(&["notification", "list", "--peek"], b"");
    assert_eq!(code, 0, "{out}");
    assert!(out.lines().next().unwrap().contains("\tchanges\t"), "{out}");

    // On its lines, with no review, and the thread it starts resolved by
    // the pull's author.
    let at = json!([{"path": "f", "side": "new", "line": 1, "body": "This"}]);
    let lined = json!({"repo": repo, "subject": pull, "body": "", "lines": at});
    let (status, _, on) = write("createComment", owning, lined);
    assert_eq!(status, 200, "{on}");
    let on = on["uri"].as_str().unwrap().to_string();
    let resolving = json!({"repo": repo, "subject": on, "state": "resolved"});
    assert_eq!(write("setItemState", me, resolving).0, 200);
    let resolved = |c: &Value| c["uri"] == on.as_str() && c["resolved"] == true;
    wait_until(|| {
        thread(&pull, None)["comments"]
            .as_array()
            .unwrap()
            .iter()
            .any(resolved)
    });
    // Mentions: written as facets by whoever writes, linked in the thread
    // and heard of by whom they name, once the edit that names them is
    // in; a handle no one has stays a word, and one named twice is two.
    let named = person(account());
    let said = json!({"repo": repo, "subject": issue, "body": "Who knows?"});
    let (_, _, asking) = write("createComment", me, said);
    let asking = asking["uri"].as_str().unwrap().to_string();
    let body = format!(
        "Ask @{} (not @nobody.invalid), or `@{0}`; @{0} knows.",
        named.account.handle
    );
    let edit = json!({"repo": repo, "subject": asking, "body": body});
    assert_eq!(write("editItem", me, edit).0, 200);
    let rkey = asking.rsplit('/').next().unwrap();
    let records = author.records("dev.knotless.comment");
    let (_, record) = records.iter().find(|(k, _)| k == rkey).unwrap();
    let facets = &record["facets"];
    assert_eq!(facets.as_array().unwrap().len(), 2, "{record}");
    assert_eq!(facets[0]["features"][0]["did"], named.account.did);
    assert_eq!(facets[1]["features"][0]["did"], named.account.did);
    let link = format!(
        r#"<a href="/{}">@{}</a>"#,
        named.account.did, named.account.handle
    );
    wait_until(|| {
        let shown = thread(&issue, None);
        let comments = shown["comments"].as_array().unwrap().clone();
        comments
            .iter()
            .any(|c| c["html"].as_str().unwrap_or_default().contains(&link))
    });
    let theirs_named = session(&log_in(&server, &named.account, ""));
    let heard_named = || {
        browse(
            "GET",
            &xrpc("listNotifications"),
            Some(theirs_named.as_str()),
            None,
            None,
        )
        .2
    };
    wait_until(|| heard_named()["notifications"][0]["uri"] == asking.as_str());
    let mention = &heard_named()["notifications"][0];
    assert_eq!(
        (
            &mention["reason"],
            &mention["reasonSubject"],
            &mention["title"]
        ),
        (&json!("mention"), &json!(issue), &json!("Broken"))
    );
    // A comment naming the author of what it is on is heard of once, as a
    // comment.
    let body = format!("Thanks, @{}.", author.account.handle);
    let thanks = json!({"repo": repo, "subject": issue, "body": body});
    let (status, _, thanked) = write("createComment", owning, thanks);
    assert_eq!(status, 200, "{thanked}");
    let thanked = thanked["uri"].as_str().unwrap().to_string();
    wait_until(|| heard()["notifications"][0]["uri"] == thanked.as_str());
    let all = heard()["notifications"].as_array().unwrap().clone();
    let of = all.iter().filter(|n| n["uri"] == thanked.as_str());
    let reasons: Vec<&Value> = of.map(|n| &n["reason"]).collect();
    assert_eq!(reasons, [&json!("comment")]);

    // Editing what one wrote, and only that.
    let edit = json!({"repo": repo, "subject": comment, "body": "Me too, still."});
    let (status, _, edited) = write("editItem", me, edit.clone());
    assert_eq!((status, &edited["uri"]), (200, &json!(comment)));
    let shown = thread(&issue, None);
    assert_eq!(shown["comments"][0]["body"], "Me too, still.");
    assert!(shown["comments"][0]["updatedAt"].is_string());
    assert_eq!(write("editItem", owning, edit).0, 400);
    let retitle = json!({"repo": repo, "subject": issue, "title": "Broken badly"});
    assert_eq!(write("editItem", me, retitle).0, 200);
    assert_eq!(thread(&issue, None)["item"]["title"], "Broken badly");

    // Closing and reopening, as its author.
    let closing = |state: &str| json!({"repo": repo, "subject": issue, "state": state});
    let (status, _, closed) = write("setItemState", me, closing("closed"));
    assert_eq!(status, 200, "{closed}");
    assert!(
        closed["uri"]
            .as_str()
            .unwrap()
            .contains("/dev.knotless.state/")
    );
    assert_eq!(thread(&issue, None)["item"]["state"], "closed");
    assert_eq!(write("setItemState", me, closing("merged")).0, 400);
    assert_eq!(write("setItemState", me, closing("open")).0, 200);
    assert_eq!(thread(&issue, None)["item"]["state"], "open");

    // Labels, by the owner; not by its author, a stranger to its labels.
    let labelling = |removed: bool| json!({"repo": repo, "subject": issue, "label": "kind/bug", "removed": removed});
    let (status, _, labelled) = write("labelItem", owning, labelling(false));
    assert_eq!(status, 200, "{labelled}");
    assert_eq!(labelled["labels"][0]["name"], "kind/bug");
    assert_eq!(
        thread(&issue, None)["item"]["labels"][0]["name"],
        "kind/bug"
    );
    assert_eq!(write("labelItem", me, labelling(false)).0, 400);
    let (_, _, unlabelled) = write("labelItem", owning, labelling(true));
    assert_eq!(unlabelled, json!({"labels": []}));
    assert_eq!(thread(&issue, None)["item"].get("labels"), None);

    // Hiding, by the owner alone, and showing again.
    let hiding = |hidden: bool| json!({"repo": repo, "subject": comment, "hidden": hidden});
    assert_eq!(write("hideItem", me, hiding(true)).0, 400);
    let (status, _, hidden) = write(
        "hideItem",
        owning,
        json!({"repo": repo, "subject": comment}),
    );
    assert_eq!((status, hidden), (200, json!({"hidden": true})));
    let shown = thread(&issue, None)["comments"].as_array().unwrap().clone();
    assert!(
        !shown.iter().any(|c| c["uri"] == comment.as_str()),
        "{shown:?}"
    );
    assert_eq!(
        write("hideItem", owning, hiding(false)).2,
        json!({"hidden": false})
    );
    assert_eq!(thread(&issue, None)["comments"][0]["uri"], comment.as_str());

    // Reporting it, to the moderation service.
    let report = json!({"subject": comment, "reason": "spam", "text": "Again and again."});
    let (status, _, sent) = write("createReport", owning, report);
    assert_eq!(status, 200, "{sent}");
    assert!(sent["reportId"].as_i64().is_some(), "{sent}");
    let boring = json!({"subject": comment, "reason": "boring"});
    assert_eq!(write("createReport", owning, boring).0, 400);

    // Taking one's comment back.
    let taken = json!({"repo": repo, "subject": comment});
    assert_eq!(write("deleteComment", owning, taken.clone()).0, 400);
    let (status, _, deleted) = write("deleteComment", me, taken);
    assert_eq!((status, &deleted["uri"]), (200, &json!(comment)));
    let shown = thread(&issue, None)["comments"].as_array().unwrap().clone();
    assert!(
        !shown.iter().any(|c| c["uri"] == comment.as_str()),
        "{shown:?}"
    );

    // A server keeping no index writes all the same, with nothing to
    // index.
    let plain = gateway();
    let cookie = session(&log_in(&plain, &owner.account, ""));
    let at = |nsid: &str| format!("{}/xrpc/dev.knotless.{nsid}", plain.base);
    let post = |nsid: &str, body: Value| browse("POST", &at(nsid), Some(&cookie), None, Some(body));
    let said = json!({"repo": repo, "subject": issue, "body": "Here too."});
    let (status, _, wrote) = post("createComment", said);
    assert_eq!(status, 200, "{wrote}");
    let comment = wrote["uri"].as_str().unwrap();
    let hiding = |hidden: bool| json!({"repo": repo, "subject": comment, "hidden": hidden});
    assert_eq!(post("hideItem", hiding(true)).2, json!({"hidden": true}));
    assert_eq!(post("hideItem", hiding(false)).2, json!({"hidden": false}));
}

/// Pictures in what is said: uploaded from the pages or the command line,
/// attached to the record that shows them while it does, and shown from
/// here rather than from the author's PDS.
#[test]
fn pictures_in_what_is_said() {
    let server = gateway_with(None, true);
    let owner = person(account());
    let url = owner.url("shots");
    let work = owner.work("w", 1);
    owner.git(&work, &["push", "-q", &url, "main"]);
    let author = person(account());
    let repo = format!("at://{}/dev.knotless.repo/shots", owner.account.did);
    let xrpc = |nsid: &str| format!("{}/xrpc/dev.knotless.{nsid}", server.base);
    let thread = |uri: &str| {
        let url = format!("{}?repo={repo}&uri={uri}", xrpc("getThread"));
        browse("GET", &url, None, None, None).2
    };
    let mine = session(&log_in(&server, &author.account, ""));
    let me = Some(mine.as_str());
    let write = |nsid: &str, body: Value| browse("POST", &xrpc(nsid), me, None, Some(body));
    let upload = |bytes: &[u8], cookie: Option<&str>| {
        let asked = format!("{}?repo={repo}", xrpc("uploadImage"));
        let req = reqwest::blocking::Client::new().post(asked);
        let mut req = req.header("content-type", "image/png").body(bytes.to_vec());
        if let Some(cookie) = cookie {
            req = req.header("cookie", cookie);
        }
        let res = req.send().unwrap();
        (res.status().as_u16(), res.json::<Value>().unwrap())
    };
    let image = |uri: &str, cid: &str| {
        let asked = format!("{}?repo={repo}&uri={uri}&cid={cid}", xrpc("getAttachment"));
        let res = reqwest::blocking::get(asked).unwrap();
        let kind = res.headers().get("content-type").cloned();
        let kind = kind.map(|k| k.to_str().unwrap().to_string());
        (res.status().as_u16(), kind, res.bytes().unwrap().to_vec())
    };

    // Uploaded from the pages, as the account logged in, and only a picture.
    let png = b"\x89PNG\r\n\x1a\nshot";
    let (status, up) = upload(png, me);
    assert_eq!(status, 200, "{up}");
    let cid = up["blob"]["ref"]["$link"].as_str().unwrap().to_string();
    let link = up["url"].as_str().unwrap().to_string();
    assert!(
        link.ends_with(&format!("did={}&cid={cid}", author.account.did)),
        "{link}"
    );
    assert_eq!(upload(png, None).0, 401);
    assert_eq!(upload(b"%PDF-1.7", me).0, 400);

    // A comment showing it attaches it, and is drawn showing it from here.
    let (code, issue) = author.cli(&["issue", "create", &url, "Looks wrong"], b"");
    assert_eq!(code, 0, "{issue}");
    wait_until(|| thread(&issue)["item"]["title"] == "Looks wrong");
    let body = format!("Like this:\n\n![shot]({link})");
    let images = json!([{"image": up["blob"]}, {"image": up["blob"]}]);
    let said = json!({"repo": repo, "subject": issue, "body": body, "images": images});
    let (status, _, wrote) = write("createComment", said);
    assert_eq!(status, 200, "{wrote}");
    let comment = wrote["uri"].as_str().unwrap().to_string();
    let rkey = comment.rsplit('/').next().unwrap();
    let record = author.record("dev.knotless.comment", rkey);
    assert_eq!(
        record["attachments"],
        json!([{"$type": "dev.knotless.defs#image", "image": up["blob"]}])
    );
    let html = thread(&issue)["comments"][0]["html"]
        .as_str()
        .unwrap()
        .to_string();
    assert!(html.contains("/xrpc/dev.knotless.getAttachment?"), "{html}");
    let (status, kind, bytes) = image(&comment, &cid);
    assert_eq!(
        (status, kind.as_deref(), &bytes[..]),
        (200, Some("image/png"), &png[..])
    );
    // Only one its record attaches, of a pull, issue or comment.
    assert_eq!(image(&comment, "bafkreinone").0, 404);
    assert_eq!(image(&repo, &cid).0, 404);

    // Edited not to show it any more, it lets it go.
    let edit = json!({"repo": repo, "subject": comment, "body": "Never mind."});
    assert_eq!(write("editItem", edit).0, 200);
    let record = author.record("dev.knotless.comment", rkey);
    assert_eq!(record.get("attachments"), None, "{record}");

    // From the command line, each named shown after the body.
    let shot = author.path("screen shot.png");
    std::fs::write(&shot, png).unwrap();
    let shot = shot.to_str().unwrap();
    let opened = [
        "issue",
        "create",
        &url,
        "Seen here",
        "-m",
        "Look:",
        "--image",
        shot,
    ];
    let (code, issue) = author.cli(&opened, b"");
    assert_eq!(code, 0, "{issue}");
    let rkey = issue.rsplit('/').next().unwrap();
    let record = author.record("dev.knotless.issue", rkey);
    assert!(
        record["body"]
            .as_str()
            .unwrap()
            .starts_with("Look:\n\n![screen shot.png](http"),
        "{record}"
    );
    assert_eq!(
        record["attachments"][0]["image"]["ref"]["$link"],
        cid.as_str()
    );
    wait_until(|| thread(&issue)["item"]["title"] == "Seen here");
    let html = thread(&issue)["html"].as_str().unwrap().to_string();
    assert!(html.contains("/xrpc/dev.knotless.getAttachment?"), "{html}");
    assert_eq!(image(&issue, &cid).0, 200);
    let commented = ["comment", "add", &url, &issue, "Again:", "--image", shot];
    // Only a pull is reviewed.
    let reviewed = author.cli(&[&commented[..], &["--approve"]].concat(), b"");
    assert_ne!(reviewed.0, 0, "{reviewed:?}");
    assert_eq!(author.cli(&commented, b"").0, 0);
    let edited = [
        "issue", "edit", &url, &issue, "-m", "Still:", "--image", shot,
    ];
    assert_eq!(author.cli(&edited, b"").0, 0);
    let record = author.record("dev.knotless.issue", rkey);
    assert_eq!(
        record["attachments"].as_array().unwrap().len(),
        1,
        "{record}"
    );
    for refused in [
        &["issue", "edit", &url, &issue, "--image", shot][..],
        &["issue", "create", &url, "Gone", "--image", "/nowhere.png"],
        &["comment", "add", &url, &issue, "x", "--bogus"],
    ] {
        assert_ne!(author.cli(refused, b"").0, 0, "{refused:?}");
    }
}