johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! 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");
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,
"{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:?}");
}
}