use super::handles::{
AUTHOR_REVERIFY_SECS, FailedPublish, Fetched, Named, StoredHandle, profile_of,
published_description, refresh_handles_with, sweep_avatars, verify_unseen_with,
};
use super::*;
use crate::config::{FontFace, IndexConfig, IndexFonts};
use crate::datastore::{Datastore, schema};
use knot_atproto::Atproto;
use knot_record::manifest::{MANIFEST_COLLECTION, MANIFEST_RKEY, RepoDescription};
use knot_types::{AccountDid, RepoDid};
fn test_url(hostname: &str) -> url::Url {
let mut url = url::Url::parse(&format!("https://{hostname}")).unwrap();
url.set_path("");
url
}
fn page(hostname: &str, generation_id: &str, sections: &[PersonSection]) -> Page {
page_with(&IndexConfig::default(), hostname, generation_id, sections)
}
fn page_with(
index: &IndexConfig,
hostname: &str,
generation_id: &str,
sections: &[PersonSection],
) -> Page {
Page::new(
index,
&Vars {
hostname,
owner: "ptr.pet",
owner_did: "did:plc:owner",
knot_did: "did:web:git.klbr.net",
repos: sections.iter().map(|s| s.repos.len()).sum(),
users: sections.len(),
kaomoji: kaomoji_for(generation_id),
generation: &generation_id[..generation_id.len().min(16)],
},
)
}
fn repo(did: &str, owner: &str, name: &str, has_public: bool) -> RepoEntry {
RepoEntry {
repo_did: did.to_string(),
owner: owner.to_string(),
rkey: name.to_string(),
name: Some(name.to_string()),
head: has_public.then(|| "1".repeat(40)),
public_refs: if has_public {
vec![("refs/heads/main".to_string(), "1".repeat(40))]
} else {
Vec::new()
},
cob_refs: vec![(
"refs/cobs/org.tangled.track.ticket/1".to_string(),
"2".repeat(40),
)],
object_format: "sha1".to_string(),
fingerprint: "fp".to_string(),
git_export_hash: has_public.then(|| "eh".to_string()),
release_id: has_public.then(|| "rel".to_string()),
fetch_bases: Vec::new(),
shallow_branches: Vec::new(),
shallow_head: None,
moved_wants: Vec::new(),
}
}
#[test]
fn groups_two_owners_and_sorts_people_and_repos() {
let entries = [
repo("did:plc:z2", "did:plc:owner_z", "zebra", true),
repo("did:plc:z1", "did:plc:owner_z", "alpha", true),
repo("did:plc:a2", "did:plc:owner_a", "beta", true),
repo("did:plc:a1", "did:plc:owner_a", "acorn", true),
];
let refs: Vec<&RepoEntry> = entries.iter().collect();
let labels = BTreeMap::from([
("did:plc:owner_z".to_string(), "ptr.pet".to_string()),
("did:plc:owner_a".to_string(), "alice.test".to_string()),
]);
let descs = BTreeMap::from([("did:plc:a1".to_string(), "first repo".to_string())]);
let sections = group_sections(&refs, &labels, &descs);
assert_eq!(sections.len(), 2);
assert_eq!(sections[0].label, "alice.test");
assert_eq!(
sections[0]
.repos
.iter()
.map(|r| r.name.as_str())
.collect::<Vec<_>>(),
["acorn", "beta"]
);
assert_eq!(
sections[0].repos[0].description.as_deref(),
Some("first repo")
);
assert_eq!(sections[0].repos[1].description, None);
assert_eq!(sections[1].label, "ptr.pet");
assert_eq!(
sections[1]
.repos
.iter()
.map(|r| r.name.as_str())
.collect::<Vec<_>>(),
["alpha", "zebra"]
);
let html = render(
&test_url("git.klbr.net"),
"git.klbr.net",
"gen-1",
§ions,
&page("git.klbr.net", "gen-1", §ions),
&Moment::default(),
);
let alice_pos = html.find("https://tangled.org/alice.test\"").unwrap();
let ptr_pos = html.find("https://tangled.org/ptr.pet\"").unwrap();
assert!(alice_pos < ptr_pos, "people sorted by handle");
let acorn_pos = html.find("https://tangled.org/alice.test/acorn").unwrap();
let beta_pos = html.find("https://tangled.org/alice.test/beta").unwrap();
assert!(acorn_pos < beta_pos, "repos sorted by name under alice");
let alpha_pos = html.find("https://tangled.org/ptr.pet/alpha").unwrap();
let zebra_pos = html.find("https://tangled.org/ptr.pet/zebra").unwrap();
assert!(alpha_pos < zebra_pos, "repos sorted by name under ptr.pet");
}
#[test]
fn hidden_or_unexported_repo_and_empty_owner_are_absent() {
let entries = [
repo("did:plc:public", "did:plc:owner_pub", "visible", true),
repo(
"did:plc:hidden",
"did:plc:owner_pub",
"secret-cob-only",
false,
),
repo("did:plc:unexported", "did:plc:owner_empty", "ghost", false),
];
let refs: Vec<&RepoEntry> = entries.iter().collect();
let labels = BTreeMap::from([
("did:plc:owner_pub".to_string(), "ptr.pet".to_string()),
("did:plc:owner_empty".to_string(), "ghost.pet".to_string()),
]);
let sections = group_sections(&refs, &labels, &BTreeMap::new());
assert_eq!(sections.len(), 1);
assert_eq!(sections[0].label, "ptr.pet");
assert_eq!(sections[0].repos.len(), 1);
assert_eq!(sections[0].repos[0].name, "visible");
let html = render(
&test_url("git.klbr.net"),
"git.klbr.net",
"gen-1",
§ions,
&page("git.klbr.net", "gen-1", §ions),
&Moment::default(),
);
assert!(html.contains("visible"));
assert!(!html.contains("secret-cob-only"));
assert!(!html.contains("did:plc:hidden"));
assert!(!html.contains("ghost"));
assert!(!html.contains("did:plc:owner_empty"));
}
#[test]
fn escapes_every_user_controlled_string_and_attribute() {
let sections = [PersonSection {
label: "bad\"handle<1>&".to_string(),
did: "did:plc:owner".to_string(),
repos: vec![ServedRepo {
did: "did:plc:amxsvho4opuwe53msnkbwa5j".to_string(),
name: "a&b<c>\"d".to_string(),
description: Some("<script>x</script>\" onmouseover=".to_string()),
active_at: None,
}],
}];
let html = render(
&test_url("git.klbr.net"),
"git.klbr.net\"<host>",
"gen-1",
§ions,
&page("git.klbr.net\"<host>", "gen-1", §ions),
&Moment::default(),
);
assert!(!html.contains("<script>x</script>"), "{html}");
assert!(
html.contains("<script>x</script>" onmouseover="),
"{html}"
);
assert!(html.contains("a&b<c>"d"), "{html}");
assert!(html.contains("bad"handle<1>&"), "{html}");
assert!(
html.contains(
"title=\"https://git.klbr.net/did:plc:amxsvho4opuwe53msnkbwa5j\" aria-label=\"copy git url\" data-clone=\"https://git.klbr.net/did:plc:amxsvho4opuwe53msnkbwa5j\""
),
"{html}"
);
assert!(
html.contains("<meta property=\"og:title\" content=\"git.klbr.net"<host>\">"),
"{html}"
);
assert!(
html.contains(
"<meta property=\"og:site_name\" content=\"git.klbr.net"<host>\">"
),
"{html}"
);
assert!(
html.contains("<meta property=\"og:url\" content=\"https://git.klbr.net/\">"),
"{html}"
);
assert!(
html.contains("<meta property=\"og:image\" content=\"https://git.klbr.net/og.png\">"),
"{html}"
);
}
fn scrape_head_meta(html: &str) -> Vec<(String, String, Option<String>)> {
let head_start = html.find("<head>").expect("has <head>") + "<head>".len();
let head_end = html[head_start..].find("</head>").expect("has </head>") + head_start;
let head = &html[head_start..head_end];
let mut tags = Vec::new();
let mut rest = head;
while let Some(pos) = rest.find("<meta ") {
let after = &rest[pos + "<meta ".len()..];
let end = after.find('>').expect("closed <meta>");
let raw_attrs = &after[..end];
rest = &after[end + 1..];
let attr = |key: &str| -> Option<String> {
let needle = format!("{key}=\"");
let start = raw_attrs.find(&needle)? + needle.len();
let tail = &raw_attrs[start..];
let close = tail.find('"')?;
Some(
tail[..close]
.replace(""", "\"")
.replace("'", "'")
.replace("<", "<")
.replace(">", ">")
.replace("&", "&"),
)
};
let key = attr("property")
.map(|p| format!("property:{p}"))
.or_else(|| attr("name").map(|n| format!("name:{n}")))
.unwrap_or_default();
if !key.is_empty() {
if let Some(content) = attr("content") {
tags.push((key, content, attr("media")));
}
}
}
tags
}
#[test]
fn head_carries_open_graph_twitter_and_theme_color_tags() {
let sections = [
PersonSection {
label: "alice.test".to_string(),
did: "did:plc:owner".to_string(),
repos: vec![
ServedRepo {
did: "did:plc:a1".to_string(),
name: "acorn".to_string(),
description: None,
active_at: None,
},
ServedRepo {
did: "did:plc:a2".to_string(),
name: "beta".to_string(),
description: None,
active_at: None,
},
],
},
PersonSection {
label: "ptr.pet".to_string(),
did: "did:plc:owner".to_string(),
repos: vec![ServedRepo {
did: "did:plc:z1".to_string(),
name: "knot-static".to_string(),
description: None,
active_at: None,
}],
},
];
let html = render(
&test_url("git.klbr.net"),
"git.klbr.net",
"gen-1",
§ions,
&page("git.klbr.net", "gen-1", §ions),
&Moment::default(),
);
let tags = scrape_head_meta(&html);
let find_one = |key: &str| -> String {
let matches: Vec<_> = tags
.iter()
.filter(|(k, _, media)| k == key && media.is_none())
.map(|(_, v, _)| v.clone())
.collect();
assert_eq!(
matches.len(),
1,
"expected exactly 1 tag for {key}, got {matches:?}"
);
matches.into_iter().next().unwrap()
};
let expected_desc = "a knot for tangled";
assert_eq!(find_one("name:description"), expected_desc);
assert_eq!(find_one("property:og:title"), "git.klbr.net");
assert_eq!(find_one("property:og:description"), expected_desc);
assert_eq!(find_one("property:og:url"), "https://git.klbr.net/");
assert_eq!(find_one("property:og:site_name"), "git.klbr.net");
assert_eq!(find_one("property:og:type"), "website");
assert_eq!(find_one("property:og:image"), "https://git.klbr.net/og.png");
assert_eq!(find_one("property:og:image:width"), "1200");
assert_eq!(find_one("property:og:image:height"), "630");
assert_eq!(
find_one("property:og:image:alt"),
"git.klbr.net — a static knot for tangled — 3 repos across 2 owners served straight from nginx (っ˘ω˘ς )"
);
assert_eq!(find_one("name:twitter:card"), "summary_large_image");
let themes: Vec<_> = tags
.iter()
.filter(|(k, _, _)| k == "name:theme-color")
.map(|(_, v, m)| (v.as_str(), m.as_deref()))
.collect();
assert_eq!(
themes,
vec![
("#fdf7fa", Some("(prefers-color-scheme: light)")),
("#1b1519", Some("(prefers-color-scheme: dark)")),
]
);
}
#[test]
fn repo_without_description_omits_desc_element() {
let sections = [PersonSection {
label: "ptr.pet".to_string(),
did: "did:plc:owner".to_string(),
repos: vec![ServedRepo {
did: "did:plc:amxsvho4opuwe53msnkbwa5j".to_string(),
name: "knot-static".to_string(),
description: None,
active_at: None,
}],
}];
let html = render(
&test_url("git.klbr.net"),
"git.klbr.net",
"gen-1",
§ions,
&page("git.klbr.net", "gen-1", §ions),
&Moment::default(),
);
assert!(!html.contains("<div class=\"desc\">"), "{html}");
}
#[test]
fn kaomoji_is_deterministic_per_generation_and_varies_across_ids() {
assert_eq!(
kaomoji_for("fixed-generation-id"),
kaomoji_for("fixed-generation-id")
);
let face = kaomoji_for("fixed-generation-id");
assert!(KAOMOJI.contains(&face));
let html_a = render(
&test_url("git.klbr.net"),
"git.klbr.net",
"fixed-generation-id",
&[],
&page("git.klbr.net", "fixed-generation-id", &[]),
&Moment::default(),
);
let html_b = render(
&test_url("git.klbr.net"),
"git.klbr.net",
"fixed-generation-id",
&[],
&page("git.klbr.net", "fixed-generation-id", &[]),
&Moment::default(),
);
assert_eq!(html_a, html_b);
assert!(html_a.contains(&format!("<span id=\"kaomoji\">{face}</span>")));
let seen: BTreeSet<&str> = (0..100).map(|n| kaomoji_for(&format!("gen-{n}"))).collect();
assert!(seen.len() > 5, "kaomoji picks across the table: {seen:?}");
}
#[tokio::test]
async fn handles_verify_through_dns_or_well_known_and_survive_outages() {
use bytes::Bytes;
use http::StatusCode;
use knot_atproto::PlcDirectory;
use knot_runtime::{
DnsTxtResolver, FakeHttp, HttpResponse, K256Signer, ManualClock, NetworkError,
SeededEntropy, Signer as _,
};
use knot_types::{KnotId, UnixMicros};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
struct StubDns {
offline: Arc<AtomicBool>,
}
impl DnsTxtResolver for StubDns {
fn lookup_txt(
&self,
name: String,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Vec<String>, NetworkError>> + Send>,
> {
let offline = Arc::clone(&self.offline);
Box::pin(async move {
if offline.load(Ordering::SeqCst) {
return Err(NetworkError::Request("dns down".into()));
}
match name.as_str() {
"_atproto.ptr.pet" => Ok(vec!["did=did:plc:verified".to_string()]),
"_atproto.impostor.pet" => Ok(vec!["did=did:plc:other".to_string()]),
"_atproto.fresh.test" => Ok(vec!["did=did:plc:fresh".to_string()]),
_ => Ok(Vec::new()),
}
})
}
}
let signer = K256Signer::generate(&SeededEntropy::new(11));
let key = knot_types::crypto::multikey(0xe7, signer.public_key().as_bytes());
let doc = move |did: &str, handle: &str| {
Bytes::from(
serde_json::to_vec(&serde_json::json!({
"id": did,
"alsoKnownAs": [format!("at://{handle}")],
"verificationMethod": [{
"id": format!("{did}#atproto"),
"type": "Multikey",
"controller": did,
"publicKeyMultibase": key,
}],
"service": [{
"id": "#atproto_pds",
"type": "AtprotoPersonalDataServer",
"serviceEndpoint": "https://pds.example.test",
}],
}))
.unwrap(),
)
};
let offline = Arc::new(AtomicBool::new(false));
let net_down = Arc::clone(&offline);
let author_lookups = Arc::new(AtomicUsize::new(0));
let requests = Arc::new(AtomicUsize::new(0));
let make_atproto = || {
let net_down = Arc::clone(&net_down);
let author_lookups = Arc::clone(&author_lookups);
let requests = Arc::clone(&requests);
let doc = doc.clone();
let http = FakeHttp::new(move |req| {
requests.fetch_add(1, Ordering::SeqCst);
if net_down.load(Ordering::SeqCst) {
return Err(NetworkError::Request("plc down".into()));
}
let reply = |status: StatusCode, body: Bytes| HttpResponse {
status,
headers: http::HeaderMap::new(),
body,
};
match req.url.as_str() {
"https://plc.directory/did:plc:verified" => {
Ok(reply(StatusCode::OK, doc("did:plc:verified", "ptr.pet")))
}
"https://plc.directory/did:plc:unverified" => Ok(reply(
StatusCode::OK,
doc("did:plc:unverified", "impostor.pet"),
)),
"https://plc.directory/did:plc:other" => {
Ok(reply(StatusCode::OK, doc("did:plc:other", "other.pet")))
}
"https://plc.directory/did:plc:author" => {
author_lookups.fetch_add(1, Ordering::SeqCst);
Ok(reply(StatusCode::OK, doc("did:plc:author", "author.test")))
}
"https://plc.directory/did:plc:fresh" => {
Ok(reply(StatusCode::OK, doc("did:plc:fresh", "fresh.test")))
}
"https://author.test/.well-known/atproto-did" => Ok(reply(
StatusCode::OK,
Bytes::from_static(b"did:plc:author\n"),
)),
_ => Ok(reply(StatusCode::NOT_FOUND, Bytes::new())),
}
});
Atproto::new(
http,
ManualClock::new(UnixMicros::new(1_000_000_000)),
KnotId::new("did:web:knot.test").unwrap(),
PlcDirectory::new(url::Url::parse("https://plc.directory/").unwrap()).unwrap(),
)
.with_dns(Arc::new(StubDns {
offline: Arc::clone(&offline),
}))
};
let root = tempfile::tempdir().unwrap();
let store = Datastore::at(root.path());
let record = store.record::<schema::OwnerHandles>(&());
let owners = BTreeSet::from(["did:plc:verified", "did:plc:unverified", "did:plc:missing"]);
let named = Named {
owners: owners.clone(),
authors: BTreeSet::from(["did:plc:author"]),
};
let no_cache = cached_owner_labels(root.path(), &owners);
assert_eq!(no_cache["did:plc:verified"], "did:plc:verified");
assert_eq!(no_cache["did:plc:unverified"], "did:plc:unverified");
assert_eq!(no_cache["did:plc:missing"], "did:plc:missing");
let avatars = root.path().join("avatars");
let cid = blob_cid(PNG);
let pds = fake_pds(
BTreeMap::from([
(
"org.tangled.actor.profile",
serde_json::json!({ "description": "hi" }),
),
("app.bsky.actor.profile", avatar_ref(&cid)),
]),
Some((StatusCode::OK, PNG.to_vec())),
);
let mut stored = StoredHandles::default();
refresh_handles_with(&make_atproto(), &pds, &avatars, &named, &mut stored, 1_000).await;
record.put(&stored).unwrap();
let with_cache = cached_owner_labels(root.path(), &owners);
assert_eq!(
with_cache["did:plc:verified"], "ptr.pet",
"through the TXT record"
);
assert_eq!(
stored.entries["did:plc:author"].handle, "author.test",
"through .well-known"
);
assert_eq!(with_cache["did:plc:unverified"], "did:plc:unverified");
assert_eq!(with_cache["did:plc:missing"], "did:plc:missing");
let verified = &stored.entries["did:plc:verified"];
assert_eq!(verified.avatar_cid.as_deref(), Some(cid.as_str()));
assert_eq!(verified.avatar_ext.as_deref(), Some("png"));
assert_eq!(
std::fs::read(avatars.join(format!("{cid}.png"))).unwrap(),
PNG
);
offline.store(true, Ordering::SeqCst);
refresh_handles_with(&make_atproto(), &pds, &avatars, &named, &mut stored, 2_000).await;
record.put(&stored).unwrap();
let after_failure = cached_owner_labels(root.path(), &owners);
assert_eq!(
after_failure["did:plc:verified"], "ptr.pet",
"cached handle is preserved when refresh transport fails"
);
assert_eq!(after_failure["did:plc:unverified"], "did:plc:unverified");
assert_eq!(
stored.entries["did:plc:verified"].avatar_cid.as_deref(),
Some(cid.as_str())
);
assert_eq!(
stored.entries["did:plc:verified"].profile,
serde_json::json!({ "description": "hi" })
.as_object()
.cloned()
);
offline.store(false, Ordering::SeqCst);
let lookups = author_lookups.load(Ordering::SeqCst);
let soon = 1_000 + AUTHOR_REVERIFY_SECS - 1;
refresh_handles_with(&make_atproto(), &pds, &avatars, &named, &mut stored, soon).await;
assert_eq!(
author_lookups.load(Ordering::SeqCst),
lookups,
"an author isn't verified again before its time"
);
let later = 1_000 + AUTHOR_REVERIFY_SECS;
refresh_handles_with(&make_atproto(), &pds, &avatars, &named, &mut stored, later).await;
assert_eq!(author_lookups.load(Ordering::SeqCst), lookups + 1);
assert_eq!(stored.entries["did:plc:author"].verified_at_unix, later);
let unnamed = Named {
owners: owners.clone(),
authors: BTreeSet::new(),
};
refresh_handles_with(
&make_atproto(),
&pds,
&avatars,
&unnamed,
&mut stored,
later,
)
.await;
assert!(!stored.entries.contains_key("did:plc:author"));
assert_eq!(stored.entries["did:plc:verified"].handle, "ptr.pet");
let seen =
BTreeSet::from(["did:plc:verified", "did:plc:fresh", "did:plc:missing"].map(String::from));
let before = requests.load(Ordering::SeqCst);
verify_unseen_with(&make_atproto(), &seen, &mut stored, later).await;
assert!(requests.load(Ordering::SeqCst) > before);
let fresh = &stored.entries["did:plc:fresh"];
assert_eq!(fresh.handle, "fresh.test");
assert_eq!(
fresh.verified_at_unix, 0,
"post-activate verifies it again with its avatar"
);
assert_eq!(stored.unverified["did:plc:missing"], later);
assert_eq!(stored.entries["did:plc:verified"].verified_at_unix, later);
let before = requests.load(Ordering::SeqCst);
verify_unseen_with(&make_atproto(), &seen, &mut stored, later + 1).await;
assert_eq!(
requests.load(Ordering::SeqCst),
before,
"accounts seen before cost a generate nothing"
);
let named = Named {
owners: owners.clone(),
authors: BTreeSet::from(["did:plc:fresh", "did:plc:missing"]),
};
refresh_handles_with(&make_atproto(), &pds, &avatars, &named, &mut stored, later).await;
let fresh = &stored.entries["did:plc:fresh"];
assert_eq!(fresh.verified_at_unix, later);
assert_eq!(fresh.avatar_cid.as_deref(), Some(cid.as_str()));
assert!(stored.unverified.contains_key("did:plc:missing"));
let only_ptr = Named {
owners: BTreeSet::from(["did:plc:verified"]),
authors: BTreeSet::new(),
};
refresh_handles_with(
&make_atproto(),
&pds,
&avatars,
&only_ptr,
&mut stored,
later,
)
.await;
assert!(stored.unverified.is_empty());
}
const PNG: &[u8] = b"\x89PNG\r\n\x1a\n an avatar";
fn blob_cid(bytes: &[u8]) -> String {
knot_types::BlobCid::from_digest(knot_types::Sha256Digest::hash(bytes)).to_string()
}
fn avatar_ref(cid: &str) -> serde_json::Value {
serde_json::json!({
"avatar": { "$type": "blob", "ref": { "$link": cid }, "mimeType": "image/png", "size": 1 },
})
}
fn fake_pds(
profiles: BTreeMap<&'static str, serde_json::Value>,
blob: Option<(http::StatusCode, Vec<u8>)>,
) -> impl knot_runtime::HttpTransport {
use http::StatusCode;
knot_runtime::FakeHttp::new(move |request| {
let query: BTreeMap<String, String> = request.url.query_pairs().into_owned().collect();
let (status, body) = match request.url.path() {
"/xrpc/com.atproto.repo.getRecord" => {
match profiles.get(query["collection"].as_str()) {
Some(value) => (
StatusCode::OK,
serde_json::json!({ "value": value })
.to_string()
.into_bytes(),
),
None => (
StatusCode::BAD_REQUEST,
br#"{"error":"RecordNotFound"}"#.to_vec(),
),
}
}
"/xrpc/com.atproto.sync.getBlob" => blob.clone().expect("the avatar is cached"),
_ => (StatusCode::NOT_FOUND, Vec::new()),
};
Ok(knot_runtime::HttpResponse {
status,
headers: http::HeaderMap::new(),
body: body.into(),
})
})
}
async fn avatar(
pds: &impl knot_runtime::HttpTransport,
avatars: &Path,
endpoint: &url::Url,
) -> Option<Option<(String, String)>> {
let found = profile_of(pds, avatars, "did:plc:verified", endpoint).await;
found.map(|found| found.avatar)
}
#[tokio::test]
async fn avatars_are_kept_only_as_the_image_their_cid_names() {
use http::StatusCode;
let root = tempfile::tempdir().unwrap();
let avatars = root.path().join("avatars");
let endpoint = url::Url::parse("https://pds.example.test/").unwrap();
let tangled = |cid: &str| BTreeMap::from([("org.tangled.actor.profile", avatar_ref(cid))]);
let cid = blob_cid(PNG);
let svg = br#"<svg xmlns="http://www.w3.org/2000/svg"/>"#;
let pds = fake_pds(
tangled(&blob_cid(svg)),
Some((StatusCode::OK, svg.to_vec())),
);
assert_eq!(avatar(&pds, &avatars, &endpoint).await, Some(None));
let pds = fake_pds(
tangled(&cid),
Some((StatusCode::OK, b"\x89PNG other".to_vec())),
);
assert_eq!(avatar(&pds, &avatars, &endpoint).await, Some(None));
let pds = fake_pds(tangled("../../etc/passwd"), None);
assert_eq!(avatar(&pds, &avatars, &endpoint).await, Some(None));
let pds = fake_pds(BTreeMap::new(), None);
assert_eq!(avatar(&pds, &avatars, &endpoint).await, Some(None));
let pds = fake_pds(tangled(&cid), Some((StatusCode::BAD_GATEWAY, Vec::new())));
assert_eq!(avatar(&pds, &avatars, &endpoint).await, None);
assert!(!avatars.exists(), "nothing rejected is written");
let fetched = Some(Some((cid.clone(), "png".to_string())));
let pds = fake_pds(tangled(&cid), Some((StatusCode::OK, PNG.to_vec())));
assert_eq!(avatar(&pds, &avatars, &endpoint).await, fetched);
let cached = fake_pds(tangled(&cid), None);
assert_eq!(avatar(&cached, &avatars, &endpoint).await, fetched);
}
/// getBlob answers with `hops` redirects before the blob host answers with `blob`
fn redirecting_pds(
profile: serde_json::Value,
hops: Vec<&'static str>,
blob: Vec<u8>,
) -> impl knot_runtime::HttpTransport {
use http::StatusCode;
knot_runtime::FakeHttp::new(move |request| {
let url = request.url.as_str().to_string();
let mut headers = http::HeaderMap::new();
let (status, body) = if request.url.path() == "/xrpc/com.atproto.repo.getRecord" {
let collection = request
.url
.query_pairs()
.find(|(key, _)| key == "collection")
.map(|(_, value)| value.into_owned());
if collection.as_deref() == Some("org.tangled.actor.profile") {
(
StatusCode::OK,
serde_json::json!({ "value": profile })
.to_string()
.into_bytes(),
)
} else {
(StatusCode::BAD_REQUEST, Vec::new())
}
} else {
let at = if request.url.path() == "/xrpc/com.atproto.sync.getBlob" {
0
} else {
hops.iter()
.position(|hop| url.ends_with(hop.trim_start_matches('.')))
.map_or(usize::MAX, |at| at + 1)
};
match hops.get(at) {
Some(next) => {
headers.insert(http::header::LOCATION, next.parse().unwrap());
(StatusCode::FOUND, Vec::new())
}
None if at == hops.len() => (StatusCode::OK, blob.clone()),
None => (StatusCode::NOT_FOUND, Vec::new()),
}
};
Ok(knot_runtime::HttpResponse {
status,
headers,
body: body.into(),
})
})
}
#[tokio::test]
async fn a_blob_the_pds_redirects_is_fetched_from_where_it_points() {
let root = tempfile::tempdir().unwrap();
let endpoint = url::Url::parse("https://pds.example.test/").unwrap();
let cid = blob_cid(PNG);
let fetched = Some(Some((cid.clone(), "png".to_string())));
let cases = [
(vec!["https://blobs.example.test/blocks/a"], fetched.clone()),
(
vec!["https://blobs.example.test/blocks/a", "./b"],
fetched.clone(),
),
(vec!["http://blobs.example.test/blocks/a"], Some(None)),
(
vec![
"https://blobs.example.test/1",
"https://blobs.example.test/2",
"https://blobs.example.test/3",
"https://blobs.example.test/4",
],
Some(None),
),
];
for (n, (hops, expected)) in cases.into_iter().enumerate() {
let avatars = root.path().join(n.to_string());
let pds = redirecting_pds(avatar_ref(&cid), hops.clone(), PNG.to_vec());
assert_eq!(
avatar(&pds, &avatars, &endpoint).await,
expected,
"{hops:?}"
);
}
}
#[tokio::test]
async fn an_avatar_past_the_lexicons_size_is_left_out_wherever_it_comes_from() {
let root = tempfile::tempdir().unwrap();
let endpoint = url::Url::parse("https://pds.example.test/").unwrap();
let mut big = PNG.to_vec();
big.resize(1_000_001, 0);
let cid = blob_cid(&big);
for hops in [vec![], vec!["https://blobs.example.test/blocks/a"]] {
let avatars = root.path().join(hops.len().to_string());
let pds = redirecting_pds(avatar_ref(&cid), hops, big.clone());
assert_eq!(avatar(&pds, &avatars, &endpoint).await, Some(None));
assert!(!avatars.exists());
}
}
#[tokio::test]
async fn a_profile_written_under_sh_tangled_is_read_too() {
use http::StatusCode;
use serde_json::json;
let root = tempfile::tempdir().unwrap();
let avatars = root.path().join("avatars");
let endpoint = url::Url::parse("https://pds.example.test/").unwrap();
let cid = blob_cid(PNG);
let mut profile = avatar_ref(&cid);
profile["$type"] = json!("sh.tangled.actor.profile");
profile["location"] = json!("Nicaragua");
profile["links"] = json!(["", "", "", "", ""]);
let pds = fake_pds(
BTreeMap::from([("sh.tangled.actor.profile", profile)]),
Some((StatusCode::OK, PNG.to_vec())),
);
let found = profile_of(&pds, &avatars, "did:plc:verified", &endpoint).await;
assert_eq!(
found,
Some(Fetched {
avatar: Some((cid, "png".to_string())),
profile: json!({ "location": "Nicaragua", "links": [] })
.as_object()
.cloned(),
})
);
}
fn encoded(image: image::DynamicImage, format: image::ImageFormat) -> Vec<u8> {
let mut bytes = Vec::new();
image
.write_to(&mut std::io::Cursor::new(&mut bytes), format)
.unwrap();
bytes
}
fn sized(avatars: &Path, cid: &str) -> Vec<(u32, u32)> {
crate::xrpc::social::AVATAR_SIZES
.iter()
.filter_map(|&size| {
let path = avatars.join(crate::xrpc::social::sized_avatar(cid, size));
let bytes = std::fs::read(path).ok()?;
assert_eq!(knot_record::attachment::sniffed(&bytes), Some("image/webp"));
let image = image::load_from_memory(&bytes).unwrap();
Some((image.width(), image.height()))
})
.collect()
}
#[tokio::test]
async fn an_avatar_is_kept_at_its_sizes_and_whole_only_up_to_the_lexicons_size() {
use http::StatusCode;
let root = tempfile::tempdir().unwrap();
let endpoint = url::Url::parse("https://pds.example.test/").unwrap();
let squares = vec![(42, 42), (96, 96), (192, 192)];
let tangled = |cid: &str| BTreeMap::from([("org.tangled.actor.profile", avatar_ref(cid))]);
let wide = image::RgbImage::from_fn(400, 300, |x, y| image::Rgb([x as u8, y as u8, 7]));
let small = encoded(wide.into(), image::ImageFormat::Jpeg);
let cid = blob_cid(&small);
let avatars = root.path().join("small");
let pds = fake_pds(tangled(&cid), Some((StatusCode::OK, small.clone())));
assert_eq!(
avatar(&pds, &avatars, &endpoint).await,
Some(Some((cid.clone(), "jpg".to_string())))
);
assert_eq!(sized(&avatars, &cid), squares);
assert!(avatars.join(format!("{cid}.jpg")).is_file());
let mut seed = 7u32;
let noise = image::RgbImage::from_fn(800, 800, |_, _| {
seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
image::Rgb((seed >> 8).to_le_bytes()[..3].try_into().unwrap())
});
let big = encoded(noise.into(), image::ImageFormat::Png);
assert!(
big.len() > 1_000_000 && big.len() < 5_000_000,
"{}",
big.len()
);
let cid = blob_cid(&big);
let avatars = root.path().join("big");
let pds = fake_pds(tangled(&cid), Some((StatusCode::OK, big)));
assert_eq!(
avatar(&pds, &avatars, &endpoint).await,
Some(Some((cid.clone(), "png".to_string())))
);
assert_eq!(sized(&avatars, &cid), squares);
assert!(
!avatars.join(format!("{cid}.png")).exists(),
"too big to serve whole"
);
let cached = fake_pds(tangled(&cid), None);
assert_eq!(
avatar(&cached, &avatars, &endpoint).await,
Some(Some((cid, "webp".to_string()))),
"its sizes stand in for it"
);
let huge = encoded(
image::GrayImage::new(6_000, 5_000).into(),
image::ImageFormat::Png,
);
assert!(huge.len() < 1_000_000);
let cid = blob_cid(&huge);
let avatars = root.path().join("huge");
let pds = fake_pds(tangled(&cid), Some((StatusCode::OK, huge)));
assert_eq!(
avatar(&pds, &avatars, &endpoint).await,
Some(Some((cid.clone(), "png".to_string()))),
"30 megapixels aren't decoded, only served whole"
);
assert_eq!(sized(&avatars, &cid), Vec::new());
let earlier = encoded(image::RgbImage::new(64, 64).into(), image::ImageFormat::Png);
let cid = blob_cid(&earlier);
let avatars = root.path().join("earlier");
std::fs::create_dir_all(&avatars).unwrap();
std::fs::write(avatars.join(format!("{cid}.png")), &earlier).unwrap();
let cached = fake_pds(tangled(&cid), None);
assert_eq!(
avatar(&cached, &avatars, &endpoint).await,
Some(Some((cid.clone(), "png".to_string())))
);
assert_eq!(
sized(&avatars, &cid),
squares,
"an avatar cached before sizes gets them"
);
}
#[tokio::test]
async fn an_avatar_cached_before_sizes_keeps_them_through_every_sync() {
let root = tempfile::tempdir().unwrap();
let avatars = root.path().join("avatars");
let endpoint = url::Url::parse("https://pds.example.test/").unwrap();
let earlier = encoded(image::RgbImage::new(64, 64).into(), image::ImageFormat::Png);
let cid = blob_cid(&earlier);
std::fs::create_dir_all(&avatars).unwrap();
std::fs::write(avatars.join(format!("{cid}.png")), &earlier).unwrap();
std::fs::write(avatars.join("bafkreigone.png"), b"an avatar nobody names").unwrap();
let mut stored = StoredHandles::default();
let cached = fake_pds(
BTreeMap::from([("org.tangled.actor.profile", avatar_ref(&cid))]),
None,
);
for sync in 0..2 {
let found = profile_of(&cached, &avatars, "did:plc:verified", &endpoint)
.await
.unwrap();
let (avatar_cid, avatar_ext) = found.avatar.unzip();
stored.entries.insert(
"did:plc:verified".into(),
StoredHandle {
handle: "ptr.pet".into(),
verified_at_unix: sync,
avatar_cid,
avatar_ext,
profile: None,
},
);
sweep_avatars(&avatars, &stored);
assert_eq!(
sized(&avatars, &cid),
vec![(42, 42), (96, 96), (192, 192)],
"sync {sync}"
);
assert!(avatars.join(format!("{cid}.png")).is_file());
assert!(!avatars.join("bafkreigone.png").exists());
}
}
#[tokio::test]
async fn the_tangled_profile_is_kept_within_the_lexicon() {
use http::StatusCode;
use serde_json::json;
let root = tempfile::tempdir().unwrap();
let avatars = root.path().join("avatars");
let endpoint = url::Url::parse("https://pds.example.test/").unwrap();
let cid = blob_cid(PNG);
let pinned = "did:plc:hqvpokkyso2k22zc76czmcj4";
let links: Vec<String> = (0..7).map(|n| format!("https://{n}.test")).collect();
let offered = [vec!["not a uri".to_string()], links.clone()].concat();
let mut tangled = avatar_ref(&cid);
tangled.as_object_mut().unwrap().extend(
json!({
"$type": "org.tangled.actor.profile",
"description": "e\u{301}".repeat(300),
"location": "x".repeat(50),
"pronouns": "it,she",
"links": offered,
"stats": ["star-count", "bogus", "repository-count", "open-issue-count"],
"pinnedRepositories": ["nope", pinned],
"isOrganization": "yes",
"preferredHandle": "ptr.pet",
"labels": { "values": [{ "val": "bot" }, { "val": "x".repeat(129) }], "extra": 1 },
"createdAt": "2026-10-13T12:55:23.571710Z",
"followers": 9,
})
.as_object()
.unwrap()
.clone(),
);
let bsky = json!({ "description": "from bsky", "displayName": "ptr" });
let pds = fake_pds(
BTreeMap::from([
("org.tangled.actor.profile", tangled),
("app.bsky.actor.profile", bsky.clone()),
]),
Some((StatusCode::OK, PNG.to_vec())),
);
let found = profile_of(&pds, &avatars, "did:plc:verified", &endpoint).await;
let expected = json!({
"description": "e\u{301}".repeat(256),
"location": "x".repeat(40),
"pronouns": "it,she",
"links": links[..5],
"stats": ["star-count", "repository-count"],
"pinnedRepositories": [pinned],
"preferredHandle": "ptr.pet",
"labels": { "$type": "com.atproto.label.defs#selfLabels", "values": [{ "val": "bot" }] },
});
assert_eq!(
found,
Some(Fetched {
avatar: Some((cid.clone(), "png".to_string())),
profile: expected.as_object().cloned(),
})
);
let family = "\u{1f469}\u{200d}\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}";
let wide = json!({ "description": family.repeat(200) });
let pds = fake_pds(BTreeMap::from([("org.tangled.actor.profile", wide)]), None);
let found = profile_of(&pds, &avatars, "did:plc:verified", &endpoint).await;
let description = found.unwrap().profile.unwrap()["description"].clone();
assert_eq!(
description,
family.repeat(102),
"25-byte graphemes, cut within 2560 bytes"
);
let pds = fake_pds(
BTreeMap::from([("app.bsky.actor.profile", avatar_ref(&cid))]),
None,
);
let found = profile_of(&pds, &avatars, "did:plc:verified", &endpoint).await;
assert_eq!(
found,
Some(Fetched {
avatar: Some((cid, "png".to_string())),
profile: None,
}),
"a bsky profile only lends its avatar"
);
let old: StoredHandle = serde_json::from_value(json!({
"handle": "ptr.pet",
"verified_at_unix": 1,
"avatar_cid": "bafkrei",
"avatar_ext": "png",
}))
.unwrap();
assert_eq!(old.profile, None, "an older cache reads as no profile yet");
}
#[tokio::test]
async fn reads_manifest_description_from_settled_chain() {
use knot_record::manifest::{ManifestMeta, ManifestRecord};
use knot_runtime::{K256Signer, SeededEntropy};
use knot_types::{MstKey, OwnerDid, RepoName, UnixSeconds};
let root = tempfile::tempdir().unwrap();
let chain_root = root.path().join("atproto");
let export_dir = root.path().join("export");
std::fs::create_dir_all(&export_dir).unwrap();
let out = crate::test_git()
.current_dir(&export_dir)
.args(["init", "-q", "--bare"])
.output()
.unwrap();
assert!(out.status.success());
let did = RepoDid::new("did:plc:abcdefghijklmnopqrstuvwx").unwrap();
let owner = OwnerDid::new("did:plc:ownerownerownerownerowne").unwrap();
let name = RepoName::new("knot-static").unwrap();
let signer = K256Signer::generate(&SeededEntropy::new(9));
assert_eq!(
published_description(&chain_root, &did).await.unwrap(),
None
);
let mut cobs = crate::atproto::CobState::default();
let manifest_record = ManifestRecord::new(
owner.clone(),
name.clone(),
ManifestMeta {
description: Some(
RepoDescription::new("a static tangled knot <script>x</script>").unwrap(),
),
..ManifestMeta::default()
},
UnixSeconds::new(1_700_000_000),
AccountDid::new(owner.as_str()).unwrap(),
)
.unwrap();
cobs.records.insert(
MstKey::new(format!("{MANIFEST_COLLECTION}/{MANIFEST_RKEY}")).unwrap(),
Some(manifest_record.body().unwrap()),
);
let source = crate::atproto::RepoSource {
did: did.clone(),
owner,
name,
export_dir,
cobs: Some(cobs),
served: crate::store::RepoStore::served(&root.path().join("served")),
verify: false,
};
std::thread::scope(|scope| {
scope
.spawn(|| crate::atproto::settle(&chain_root, &signer, &source))
.join()
.unwrap()
.unwrap();
});
assert_eq!(
published_description(&chain_root, &did)
.await
.unwrap()
.as_deref(),
Some("a static tangled knot <script>x</script>")
);
}
#[test]
fn sorts_by_most_active_then_alphabetical() {
let entries = [
repo("did:plc:r1", "did:plc:owner_a", "zebra", true),
repo("did:plc:r2", "did:plc:owner_a", "alpha", true),
repo("did:plc:r3", "did:plc:owner_a", "middle", true),
repo("did:plc:r4", "did:plc:owner_b", "newest-owner-repo", true),
];
let refs: Vec<&RepoEntry> = entries.iter().collect();
let labels = BTreeMap::from([
("did:plc:owner_a".to_string(), "ptr.pet".to_string()),
("did:plc:owner_b".to_string(), "bob.test".to_string()),
]);
let activity = BTreeMap::from([
("did:plc:r1".to_string(), 200),
("did:plc:r2".to_string(), 100),
("did:plc:r3".to_string(), 100),
("did:plc:r4".to_string(), 500),
]);
let sections = group_sections_with_activity(&refs, &labels, &BTreeMap::new(), &activity);
assert_eq!(sections[0].label, "bob.test");
assert_eq!(sections[1].label, "ptr.pet");
assert_eq!(
sections[1]
.repos
.iter()
.map(|r| r.name.as_str())
.collect::<Vec<_>>(),
["zebra", "alpha", "middle"]
);
}
#[test]
fn default_subtitle_links_yumak_and_tangled() {
let html = render(
&test_url("git.klbr.net"),
"git.klbr.net",
"gen-1",
&[],
&page("git.klbr.net", "gen-1", &[]),
&Moment::default(),
);
assert!(
html.contains(
"「a <a href=\"https://tangled.org/ptr.pet/yumak\">static knot</a> for <a href=\"https://tangled.org\">tangled</a> <span id=\"kaomoji\">"
),
"{html}"
);
}
#[test]
fn subtitle_links_escape_and_refuse_other_schemes() {
assert_eq!(
render_links("[a<b](https://x.test/?q=\"1\"&r) and [js](javascript:alert(1)) [open"),
"<a href=\"https://x.test/?q="1"&r\">a<b</a> and [js](javascript:alert(1)) [open"
);
assert_eq!(
render_links("[plain](http://x.test)"),
"<a href=\"http://x.test\">plain</a>"
);
assert_eq!(
render_links("[spaced](https://x.test/a b)"),
"[spaced](https://x.test/a b)"
);
}
#[test]
fn templates_expand_every_variable_and_keep_unknown_names() {
let vars = Vars {
hostname: "git.klbr.net",
owner: "ptr.pet",
owner_did: "did:plc:owner",
knot_did: "did:web:git.klbr.net",
repos: 92,
users: 5,
kaomoji: "(◕‿◕✿)",
generation: "0123456789abcdef",
};
assert_eq!(
vars.expand(
"{hostname} {owner} {owner_did} {knot_did} {repos}/{repo_count} {users}/{user_count}/{owners} {kaomoji} {generation} {nope} {owner"
),
"git.klbr.net ptr.pet did:plc:owner did:web:git.klbr.net 92/92 5/5/5 (◕‿◕✿) 0123456789abcdef {nope} {owner"
);
assert_eq!(vars.expand("{{hostname}}"), "{git.klbr.net}");
}
#[test]
fn configured_index_text_reaches_the_head_and_header() {
let index = IndexConfig {
title: "{owner}'s knot".to_string(),
subtitle: "[{owner}](https://tangled.org/{owner}) hosts {repos} repos".to_string(),
description: "{repos} repos on {hostname}".to_string(),
og_alt: Some("card for {hostname}".to_string()),
..IndexConfig::default()
};
let sections = [PersonSection {
label: "ptr.pet".to_string(),
did: "did:plc:owner".to_string(),
repos: vec![ServedRepo {
did: "did:plc:z1".to_string(),
name: "knot-static".to_string(),
description: None,
active_at: None,
}],
}];
let page = page_with(&index, "git.klbr.net", "gen-1", §ions);
let html = render(
&test_url("git.klbr.net"),
"git.klbr.net",
"gen-1",
§ions,
&page,
&Moment::default(),
);
let tags = scrape_head_meta(&html);
let get = |key: &str| tags.iter().find(|(k, _, _)| k == key).unwrap().1.clone();
assert!(html.contains("<title>ptr.pet's knot</title>"), "{html}");
assert_eq!(get("property:og:title"), "ptr.pet's knot");
assert_eq!(get("property:og:site_name"), "ptr.pet's knot");
assert_eq!(get("name:description"), "1 repos on git.klbr.net");
assert_eq!(get("property:og:description"), "1 repos on git.klbr.net");
assert_eq!(get("property:og:image:alt"), "card for git.klbr.net");
assert!(
html.contains("「<a href=\"https://tangled.org/ptr.pet\">ptr.pet</a> hosts 1 repos <span"),
"{html}"
);
}
#[test]
fn owners_and_repos_link_under_the_configured_base() {
let sections = [PersonSection {
label: "ptr.pet".to_string(),
did: "did:plc:owner".to_string(),
repos: vec![ServedRepo {
did: "did:plc:z1".to_string(),
name: "yumak".to_string(),
description: None,
active_at: None,
}],
}];
let html_with = |index: &IndexConfig| {
let page = page_with(index, "git.klbr.net", "gen-1", §ions);
render(
&test_url("git.klbr.net"),
"git.klbr.net",
"gen-1",
§ions,
&page,
&Moment::default(),
)
};
let tangled = html_with(&IndexConfig::default());
assert!(tangled.contains("<a href=\"https://tangled.org/ptr.pet\">@ptr.pet</a>"));
assert!(tangled.contains("<a href=\"https://tangled.org/ptr.pet/yumak\" data-t"));
let own = html_with(&IndexConfig {
links: "https://{hostname}/".to_string(),
..IndexConfig::default()
});
assert!(
own.contains("<a href=\"https://git.klbr.net/ptr.pet\">@ptr.pet</a>"),
"{own}"
);
assert!(
own.contains("<a href=\"https://git.klbr.net/ptr.pet/yumak\" data-t"),
"{own}"
);
assert!(!own.contains("https://tangled.org/ptr.pet\""));
}
#[test]
fn og_card_renders_a_1200x630_png() {
let index = IndexConfig {
og_title: "git.klbr.net".to_string(),
og_subtitle: "a static knot for *tangled*".to_string(),
og_description: "92 repos · 5 owners\nevery push compiles to files nginx serves"
.to_string(),
..IndexConfig::default()
};
let page = page_with(&index, "git.klbr.net", "gen-1", &[]);
assert_eq!(
page.og_alt,
"git.klbr.net — a static knot for tangled — 92 repos · 5 owners every push compiles to files nginx serves"
);
let bytes = og::render(&page.og_title, &page.og_subtitle, &page.og_description).unwrap();
assert!(bytes.starts_with(b"\x89PNG\r\n\x1a\n"));
assert_eq!(&bytes[12..16], b"IHDR");
let width = u32::from_be_bytes(bytes[16..20].try_into().unwrap());
let height = u32::from_be_bytes(bytes[20..24].try_into().unwrap());
assert_eq!((width, height), (OG_IMAGE_WIDTH, OG_IMAGE_HEIGHT));
assert!(bytes.ends_with(b"IEND\xae\x42\x60\x82"));
let idat_len = u32::from_be_bytes(bytes[33..37].try_into().unwrap()) as usize;
assert_eq!(&bytes[37..41], b"IDAT");
let mut raw = Vec::new();
std::io::Read::read_to_end(
&mut flate2::read::ZlibDecoder::new(&bytes[41..41 + idat_len]),
&mut raw,
)
.unwrap();
let stride = 1 + OG_IMAGE_WIDTH as usize * 3;
assert_eq!(raw.len(), stride * OG_IMAGE_HEIGHT as usize);
let pixel = |x: usize, y: usize| -> [u8; 3] {
let at = y * stride + 1 + x * 3;
raw[at..at + 3].try_into().unwrap()
};
assert_eq!(pixel(5, 5), [0x1b, 0x15, 0x19], "background");
assert_eq!(pixel(310, 433), [0xe2, 0x70, 0x9d], "yarn ball");
let row_has = |y: usize, color: [u8; 3]| (500..1200).any(|x| pixel(x, y) == color);
assert!(row_has(240, [0xf1, 0xe4, 0xea]), "title ink");
assert!(row_has(310, [0xf2, 0x8b, 0xb5]), "accent ink");
assert!(row_has(390, [0xa8, 0x91, 0x9d]), "description ink");
}
#[test]
fn a_failed_handles_publish_holds_back_only_the_same_attempt_for_a_while() {
let failed = FailedPublish {
digest: "d".into(),
generation: "g".into(),
at_unix: 1000,
};
let attempt = |digest: &str, generation: &str, at_unix| FailedPublish {
digest: digest.into(),
generation: generation.into(),
at_unix,
};
assert!(failed.holds_back(&attempt("d", "g", 1300)));
assert!(
!failed.holds_back(&attempt("e", "g", 1300)),
"the store changed"
);
assert!(
!failed.holds_back(&attempt("d", "h", 1300)),
"another generation is active"
);
assert!(
!failed.holds_back(&attempt("d", "g", 1000 + 3600)),
"an hour later"
);
}
fn person(label: &str, did: &str, repos: usize) -> PersonSection {
PersonSection {
label: label.to_string(),
did: did.to_string(),
repos: (0..repos)
.map(|idx| ServedRepo {
did: format!("did:plc:{label}-{idx}"),
name: format!("{label}-repo-{idx}"),
description: None,
active_at: Some(1_000 + idx as i64),
})
.collect(),
}
}
fn draw(sections: &[PersonSection], moment: &Moment) -> String {
render(
&test_url("git.klbr.net"),
"git.klbr.net",
"gen-1",
sections,
&page("git.klbr.net", "gen-1", sections),
moment,
)
}
#[test]
fn a_person_keeps_the_colour_svfe_gives_their_did() {
for (did, hue_var) in [
("did:plc:dfl62fgb7wtjj3fcbb72naae", "--lav"),
("did:plc:zx3ymzn7kyjvdnrfkhzplwyl", "--teal"),
("did:plc:jlplwn5pi4dqrls7i6dx2me7", "--amber"),
("did:plc:3rwz3xfw2crswgifqgc3g7zh", "--accent"),
("did:plc:4prkargawaafljpw576fztej", "--lav"),
] {
assert_eq!(hue(did), hue_var, "{did}");
}
}
#[test]
fn ages_count_in_the_largest_whole_unit() {
let now = 100_000_000;
for (ago, shown) in [
(0, "1m"),
(59 * 60, "59m"),
(3600, "1h"),
(23 * 3600, "23h"),
(86_400, "1d"),
(7 * 86_400, "1w"),
(29 * 86_400, "4w"),
(30 * 86_400, "1mo"),
(364 * 86_400, "12mo"),
(800 * 86_400, "2y"),
] {
assert_eq!(age(now, now - ago), shown, "{ago}");
}
assert_eq!(age(now, now + 50), "1m", "a clock ahead of the commit");
}
#[test]
fn owners_with_a_repo_or_two_share_the_commons() {
let sections = [
person("big", "did:plc:dfl62fgb7wtjj3fcbb72naae", 3),
person("one", "did:plc:zx3ymzn7kyjvdnrfkhzplwyl", 1),
person("two", "did:plc:jlplwn5pi4dqrls7i6dx2me7", 2),
];
let html = draw(§ions, &Moment::default());
let commons = &html[html.find("id=\"commons\"").unwrap()..];
assert!(commons.contains("<span>2 people</span>"), "{html}");
assert!(commons.contains("<span class=\"own\">@one/</span>"));
assert!(commons.contains("style=\"--line:var(--teal)\""));
assert!(commons.contains("style=\"--line:var(--amber)\""));
assert!(!html.contains("id=\"one\""));
assert!(html.contains(
"<section class=\"person\" id=\"big\" data-group style=\"--line:var(--lav);order:0\">"
));
let newest = commons.find("two-repo-1").unwrap();
assert!(newest < commons.find("two-repo-0").unwrap());
let small = [person("one", "did:plc:a", 1), person("two", "did:plc:b", 2)];
let html = draw(&small, &Moment::default());
assert!(
!html.contains("id=\"commons\""),
"nobody has a section of their own"
);
assert!(html.contains("id=\"one\"") && html.contains("id=\"two\""));
}
#[test]
fn sections_start_in_the_shorter_column() {
let sections = [
person("a", "did:plc:a", 12),
person("b", "did:plc:b", 3),
person("c", "did:plc:c", 3),
person("d", "did:plc:d", 40),
];
let refs: Vec<&PersonSection> = sections.iter().collect();
assert_eq!(columns(&refs), [vec![0], vec![1, 2, 3]]);
let html = draw(§ions, &Moment::default());
assert_eq!(html.matches("<div class=\"mc\">").count(), 2);
}
#[test]
fn a_long_section_folds_after_the_threshold_and_unwinds_without_js() {
let sections = [person("many", "did:plc:many", UNWIND_THRESHOLD + 3)];
let html = draw(§ions, &Moment::default());
assert!(html.contains("<input class=\"u-chk\" type=\"checkbox\" id=\"u-0\">"));
assert!(html.contains("<ul class=\"repos folds\">"));
assert_eq!(html.matches("<li class=\"more\"").count(), 3);
assert_eq!(html.matches("<li class=\"fold-last\"").count(), 1);
assert!(html.contains(
"<label class=\"unwind\" for=\"u-0\"><span class=\"a\">3 more ⌄</span><span class=\"b\">wind up ⌃</span></label>"
));
assert!(html.contains("<noscript><style>.search,.clone{display:none}"));
assert!(!html.contains("src=\"http"), "no external requests");
assert!(!html.contains("<link rel=\"stylesheet\""));
assert!(!html.contains("<script src"));
}
#[test]
fn ages_and_the_push_strip_come_from_the_moment() {
let sections = [person("big", "did:plc:big", 3)];
let html = draw(§ions, &Moment::default());
assert!(!html.contains("class=\"age\""), "no clock, no ages");
assert!(!html.contains("class=\"pulse\""));
let today: chrono::NaiveDate = "2026-11-10".parse().unwrap();
let moment = Moment {
now: 1_000 + 2 * 3600,
pushes: vec![(today.pred_opt().unwrap(), 0), (today, 3)],
};
let html = draw(§ions, &moment);
assert!(
html.contains("<span class=\"age\" data-at=\"1000\">2h</span>"),
"{html}"
);
assert!(
html.contains("<i class=\"z\" title=\"nov 9: 0 pushes\"></i>"),
"{html}"
);
assert!(
html.contains("<i style=\"height:16px\" title=\"nov 10: 3 pushes\"></i>"),
"{html}"
);
}
fn klbr_fonts() -> IndexFonts {
let face = |family: &str, weight, file: &str| FontFace {
family: family.to_string(),
weight,
italic: false,
url: format!("/fonts/{file}.woff2"),
};
IndexFonts {
sans: Some("Outfit".to_string()),
mono: Some("Maple Mono".to_string()),
faces: vec![
face("Outfit", 400, "outfit-400"),
face("Outfit", 500, "outfit-500"),
face("Outfit", 700, "outfit-700"),
face("Maple Mono", 400, "maple-400"),
face("Maple Mono", 700, "maple-700"),
],
}
}
fn draw_with_fonts(sections: &[PersonSection], fonts: Option<IndexFonts>) -> String {
let index = IndexConfig {
fonts,
..IndexConfig::default()
};
render(
&test_url("git.klbr.net"),
"git.klbr.net",
"gen-1",
sections,
&page_with(&index, "git.klbr.net", "gen-1", sections),
&Moment::default(),
)
}
fn preloads(html: &str) -> Vec<&str> {
html.split("<link rel=\"preload\" href=\"")
.skip(1)
.map(|rest| &rest[..rest.find('"').unwrap()])
.collect()
}
#[test]
fn a_knot_without_fonts_keeps_the_system_monospace() {
let html = draw_with_fonts(&[person("ptr.pet", "did:plc:a", 3)], None);
assert!(html.contains("font:14px/1.6 ui-monospace,"));
assert!(preloads(&html).is_empty(), "{html}");
for absent in ["@font-face", "font-size-adjust", "tabular-nums"] {
assert!(!html.contains(absent), "{absent}");
}
}
#[test]
fn configured_fonts_preload_only_the_faces_the_page_draws() {
let handles = [
person("ptr.pet", "did:plc:a", 3),
person("dawn.pet", "did:plc:b", 1),
];
let html = draw_with_fonts(&handles, Some(klbr_fonts()));
assert_eq!(
preloads(&html),
["/fonts/outfit-400.woff2", "/fonts/outfit-700.woff2"]
);
assert_eq!(html.matches("@font-face").count(), 5);
assert!(html.contains(
"@font-face{font-family:\"Maple Mono\";src:url(\"/fonts/maple-700.woff2\") format(\"woff2\");font-weight:700;font-style:normal;font-display:block}"
));
assert!(html.contains("html{font-size-adjust:ex-height .5}"));
assert!(html.contains(
"body{font-family:\"Outfit\",ui-sans-serif,system-ui,sans-serif;font-variant-numeric:tabular-nums}"
));
assert!(html.contains(".did{font-family:\"Maple Mono\",ui-monospace,"));
assert!(!html.contains("class=\"did\"") && !html.contains("own did"));
// a did names a header in bold, and a commons row in regular
let did_head = [
person("did:plc:a", "did:plc:a", 3),
person("dawn.pet", "did:plc:b", 1),
];
let html = draw_with_fonts(&did_head, Some(klbr_fonts()));
assert!(html.contains("<a class=\"did\" href=\"https://tangled.org/did:plc:a\">did:plc:a</a>"));
assert_eq!(
preloads(&html),
[
"/fonts/outfit-400.woff2",
"/fonts/outfit-700.woff2",
"/fonts/maple-700.woff2"
]
);
let did_row = [
person("ptr.pet", "did:plc:a", 3),
person("did:plc:b", "did:plc:b", 1),
];
let html = draw_with_fonts(&did_row, Some(klbr_fonts()));
assert!(html.contains("<span class=\"own did\">did:plc:b/</span>"));
assert_eq!(
preloads(&html),
[
"/fonts/outfit-400.woff2",
"/fonts/outfit-700.woff2",
"/fonts/maple-400.woff2"
]
);
}
#[test]
fn a_mono_only_knot_draws_everything_in_its_mono() {
let fonts = IndexFonts {
sans: None,
..klbr_fonts()
};
let html = draw_with_fonts(&[person("did:plc:a", "did:plc:a", 3)], Some(fonts));
assert_eq!(
preloads(&html),
["/fonts/maple-400.woff2", "/fonts/maple-700.woff2"]
);
assert!(html.contains("body{font-family:\"Maple Mono\",ui-monospace,"));
}
#[test]
fn a_missing_weight_preloads_the_face_css_would_pick() {
// 400 and 500 look upward to 500 first, lighter weights look down, heavier look up
let mut fonts = klbr_fonts();
fonts
.faces
.retain(|face| face.weight != 400 || face.family != "Outfit");
fonts
.faces
.retain(|face| face.weight != 700 || face.family != "Outfit");
fonts.faces.push(FontFace {
family: "Outfit".to_string(),
weight: 300,
italic: false,
url: "/fonts/outfit-300.woff2".to_string(),
});
fonts.faces.push(FontFace {
family: "Outfit".to_string(),
weight: 800,
italic: true,
url: "/fonts/outfit-800i.woff2".to_string(),
});
let html = draw_with_fonts(&[person("ptr.pet", "did:plc:a", 3)], Some(fonts));
assert_eq!(
preloads(&html),
["/fonts/outfit-500.woff2"],
"400 takes 500, 700 takes 500 since the only heavier face is italic"
);
}