mod common; use std::future::Future; use std::pin::Pin; use std::time::{Duration, Instant}; use futures::StreamExt; use futures::stream; use http::{HeaderMap, StatusCode, header}; use tokio_tungstenite::tungstenite; use knot_events::{EventCursor, GitRefUpdate}; use knot_types::{AccountDid, ObjectFormat, Oid, OwnerDid, RepoDid}; use knot_xrpc::{ArchiveLimit, ResponseLimit}; use common::{ OWNER, World, archive_full, assert_immutable_round_trip, assert_post_rejected, assert_warming, commit_file, empty_repo, get, get_error, get_json, get_with_headers, git_run, post_authed, post_json, ref_names, repo_dids, seeded, seeded_feature_branch, seeded_with_format, sh_git, sh_git_at, }; #[tokio::test] async fn the_seeded_read_surface_renders_each_wire_shape_once() { let world = World::new(); let (did, work) = seeded(&world, "coral"); let head = sh_git(work.path(), &["rev-parse", "HEAD"]); let parent = sh_git(work.path(), &["rev-parse", "HEAD~1"]); let tag_object = sh_git(work.path(), &["rev-parse", "v1.0.0"]); let tagged_commit = sh_git(work.path(), &["rev-parse", "v1.0.0^{commit}"]); let tree = get_json( &world, &format!("/xrpc/sh.tangled.repo.tree?repo={did}&ref=main"), ) .await; assert_eq!(tree["ref"], "main"); assert!(tree.get("parent").is_none()); assert!(tree.get("dotdot").is_none()); let files = tree["files"].as_array().unwrap(); let names: Vec<&str> = files .iter() .map(|file| file["name"].as_str().unwrap()) .collect(); assert_eq!(names, vec!["README.md", "logo.png", "src"]); let readme_entry = &files[0]; assert_eq!(readme_entry["mode"], "0100644"); assert_eq!( readme_entry["size"].as_i64().unwrap(), b"# coral\n\nhello reef\n".len() as i64 ); assert_eq!( readme_entry["last_commit"]["hash"].as_str().unwrap(), head, "README was last touched by the head commit" ); assert_eq!(readme_entry["last_commit"]["message"], "update readme"); assert_eq!(files[2]["mode"], "0040000"); assert_eq!(tree["readme"]["filename"], "README.md"); assert_eq!(tree["readme"]["contents"], "# coral\n\nhello reef\n"); assert_eq!(tree["lastCommit"]["hash"], head.as_str()); assert_eq!(tree["lastCommit"]["author"]["name"], "nel"); assert_eq!(tree["lastCommit"]["author"]["when"], ""); let sub = get_json( &world, &format!("/xrpc/sh.tangled.repo.tree?repo={did}&ref=main&path=src"), ) .await; assert_eq!(sub["parent"], "src"); assert!(sub.get("dotdot").is_none()); assert_eq!(sub["files"][0]["name"], "main.rs"); assert_eq!( get_error( &world, &format!("/xrpc/sh.tangled.repo.tree?repo={did}&ref=main&path=nope"), ) .await, (StatusCode::NOT_FOUND, "PathNotFound".to_string()) ); let log = get_json( &world, &format!("/xrpc/sh.tangled.repo.log?repo={did}&ref=main"), ) .await; assert_eq!(log["total"].as_i64(), Some(4)); assert_eq!(log["page"].as_i64(), Some(1)); assert_eq!(log["per_page"].as_i64(), Some(50)); assert_eq!(log["log"], true); assert_eq!(log["ref"], "main"); let commits = log["commits"].as_array().unwrap(); assert_eq!(commits.len(), 4); let first = &commits[0]; let hash_bytes: Vec = first["hash"] .as_array() .unwrap() .iter() .map(|byte| byte.as_u64().unwrap() as u8) .collect(); assert_eq!( hash_bytes, (0..head.len()) .step_by(2) .map(|index| u8::from_str_radix(&head[index..index + 2], 16).unwrap()) .collect::>(), "commit hash rides as a byte array" ); assert_eq!(first["this"], head.as_str()); assert_eq!(first["parent"], parent.as_str()); assert_eq!(first["author"]["Name"], "nel"); assert_eq!(first["author"]["Email"], "nel@oyster.cafe"); assert_eq!(first["author"]["When"], "2026-06-01T12:33:00+02:00"); assert_eq!(first["message"], "update readme\n"); assert!(first["tree"].as_str().unwrap().len() == 40); let paged = get_json( &world, &format!("/xrpc/sh.tangled.repo.log?repo={did}&ref=main&limit=2&cursor=2"), ) .await; assert_eq!(paged["commits"].as_array().unwrap().len(), 2); assert_eq!(paged["page"].as_i64(), Some(2)); assert_eq!(paged["per_page"].as_i64(), Some(2)); let branches = get_json( &world, &format!("/xrpc/sh.tangled.repo.branches?repo={did}"), ) .await; let listed = branches["branches"].as_array().unwrap(); assert_eq!(listed.len(), 1); assert_eq!(listed[0]["reference"]["name"], "main"); assert_eq!(listed[0]["reference"]["hash"], head.as_str()); assert_eq!(listed[0]["is_default"], true); assert_eq!(listed[0]["commit"]["Author"]["Name"], "nel"); assert!(listed[0]["commit"]["Hash"].is_array()); assert_eq!(listed[0]["commit"]["ExtraHeaders"], serde_json::Value::Null); assert_eq!(listed[0]["commit"]["Message"], "update readme"); let branch = get_json( &world, &format!("/xrpc/sh.tangled.repo.branch?repo={did}&name=main"), ) .await; assert_eq!(branch["name"], "main"); assert_eq!(branch["hash"], head.as_str()); assert_eq!(branch["shortHash"], head[..7].to_string().as_str()); assert_eq!(branch["isDefault"], true); assert_eq!(branch["author"]["name"], "nel"); assert_eq!(branch["when"], "2026-06-01T12:33:00+02:00"); assert_eq!( get_error( &world, &format!("/xrpc/sh.tangled.repo.branch?repo={did}&name=mangrove"), ) .await, (StatusCode::NOT_FOUND, "BranchNotFound".to_string()) ); let tags = get_json(&world, &format!("/xrpc/sh.tangled.repo.tags?repo={did}")).await; let tag_list = tags["tags"].as_array().unwrap(); assert_eq!(tag_list.len(), 2); let annotated = tag_list.iter().find(|tag| tag["name"] == "v1.0.0").unwrap(); assert_eq!(annotated["hash"], tag_object.as_str()); assert_eq!(annotated["message"], "release one"); assert_eq!(annotated["tag"]["TargetType"].as_i64(), Some(4)); assert_eq!(annotated["tag"]["Tagger"]["Name"], "nel"); let target_bytes = annotated["tag"]["Target"].as_array().unwrap(); assert_eq!(target_bytes.len(), 20); assert_eq!( target_bytes[0].as_u64().unwrap() as u8, u8::from_str_radix(&tagged_commit[..2], 16).unwrap() ); let lightweight = tag_list .iter() .find(|tag| tag["name"] == "lightweight") .unwrap(); assert!(lightweight.get("tag").is_none()); assert_eq!(lightweight["hash"], tagged_commit.as_str()); assert_eq!(lightweight["message"], "add logo"); let single = get_json( &world, &format!("/xrpc/sh.tangled.repo.tag?repo={did}&tag=v1.0.0"), ) .await; assert_eq!(single["tag"]["name"], "v1.0.0"); assert_eq!( get_error( &world, &format!("/xrpc/sh.tangled.repo.tag?repo={did}&tag=v9.9.9"), ) .await, (StatusCode::BAD_REQUEST, "TagNotFound".to_string()) ); let text = get_json( &world, &format!("/xrpc/sh.tangled.repo.blob?repo={did}&ref=main&path=README.md"), ) .await; assert_eq!(text["encoding"], "utf-8"); assert_eq!(text["isBinary"], false); assert_eq!(text["content"], "# coral\n\nhello reef\n"); assert_eq!(text["mimeType"], "text/plain; charset=utf-8"); assert_eq!(text["lastCommit"]["message"], "update readme"); let binary = get_json( &world, &format!("/xrpc/sh.tangled.repo.blob?repo={did}&ref=main&path=logo.png"), ) .await; assert_eq!(binary["encoding"], "base64"); assert_eq!(binary["isBinary"], true); assert_eq!(binary["mimeType"], "image/png"); let (status, headers, body) = get( &world, &format!("/xrpc/sh.tangled.repo.blob?repo={did}&ref=main&path=logo.png&raw=true"), ) .await; assert_eq!(status, StatusCode::OK); assert_eq!(headers.get(header::CONTENT_TYPE).unwrap(), "image/png"); assert_eq!( headers.get(header::X_CONTENT_TYPE_OPTIONS).unwrap(), "nosniff" ); assert_eq!( headers.get(header::CONTENT_SECURITY_POLICY).unwrap(), "default-src 'none'; style-src 'unsafe-inline'; sandbox" ); let etag = headers .get(header::ETAG) .unwrap() .to_str() .unwrap() .to_string(); assert!(body.starts_with(b"\x89PNG")); let mut cached = HeaderMap::new(); cached.insert( header::IF_NONE_MATCH, http::HeaderValue::from_str(&etag).unwrap(), ); let (status, _, _) = get_with_headers( &world, &format!("/xrpc/sh.tangled.repo.blob?repo={did}&ref=main&path=logo.png&raw=true"), cached, ) .await; assert_eq!(status, StatusCode::NOT_MODIFIED); let mut weak = HeaderMap::new(); weak.insert( header::IF_NONE_MATCH, http::HeaderValue::from_str(&format!("W/{etag}")).unwrap(), ); let (status, _, _) = get_with_headers( &world, &format!("/xrpc/sh.tangled.repo.blob?repo={did}&ref=main&path=logo.png&raw=true"), weak, ) .await; assert_eq!( status, StatusCode::NOT_MODIFIED, "weak validator must revalidate too" ); assert_eq!( get_error( &world, &format!("/xrpc/sh.tangled.repo.blob?repo={did}&ref=main&path=ghost.txt"), ) .await, (StatusCode::NOT_FOUND, "FileNotFound".to_string()) ); } #[tokio::test] async fn tree_directory_last_commit_is_the_newest_touching_commit() { let world = World::new(); let (did, bare, work) = empty_repo(&world, "periwinkle"); let work = work.path(); commit_file( work, "src/a.rs", b"fn a() {}\n", "add a", "2026-06-01T12:30:00+02:00", ); let older = sh_git(work, &["rev-parse", "HEAD"]); commit_file( work, "src/b.rs", b"fn b() {}\n", "add b", "2026-06-01T12:31:00+02:00", ); let newer = sh_git(work, &["rev-parse", "HEAD"]); commit_file( work, "README.md", b"# periwinkle\n", "doc", "2026-06-01T12:33:00+02:00", ); let head = sh_git(work, &["rev-parse", "HEAD"]); sh_git(work, &["push", "-q", &bare, "main"]); let value = get_json( &world, &format!("/xrpc/sh.tangled.repo.tree?repo={did}&ref=main"), ) .await; let files = value["files"].as_array().unwrap(); let src = files .iter() .find(|file| file["name"] == "src") .expect("src directory is listed"); let reported = src["last_commit"]["hash"].as_str().unwrap(); assert_eq!( reported, newer, "the newest commit that touches the subtree is reported, not the oldest" ); assert_ne!(reported, older, "not the first commit that created subtree"); assert_ne!( reported, head, "head commit only touched README, never the src subtree" ); } #[tokio::test] async fn languages_timeout_yields_a_partial_answer_not_an_error() { let world = World::new(); let (did, work) = seeded(&world, "scallop"); let head = Oid::from_hex(&sh_git(work.path(), &["rev-parse", "HEAD"])).unwrap(); let repo = world.layout.open(&did).unwrap(); let full = knot_langs::analyze(&repo, head, Some(Instant::now() + Duration::from_secs(60))).unwrap(); assert!( full.values().any(|size| size.get() > 0), "generous budget detects code" ); let expired = Instant::now() .checked_sub(Duration::from_secs(1)) .unwrap_or_else(Instant::now); let partial = knot_langs::analyze(&repo, head, Some(expired)).unwrap(); assert!( partial.is_empty(), "exhausted budget breaks the walk and returns the partial map gathered so far, never an error" ); } #[tokio::test] async fn compare_format_patch_keeps_a_non_ascii_author_raw() { let world = World::new(); let (did, bare, work) = empty_repo(&world, "mussel"); let work = work.path(); commit_file( work, "README.md", b"# mussel\n", "first", "2026-06-01T12:30:00+02:00", ); let base = sh_git(work, &["rev-parse", "HEAD"]); std::fs::write(work.join("src.rs"), b"fn main() {}\n").unwrap(); let author = ("Lýna Þórsdóttir", "lyna@nel.pet"); git_run(work, "2026-06-01T12:31:00+02:00", author, &["add", "-A"]); git_run( work, "2026-06-01T12:31:00+02:00", author, &["commit", "-q", "-m", "café changes"], ); let head = sh_git(work, &["rev-parse", "HEAD"]); sh_git(work, &["push", "-q", &bare, "main"]); let value = get_json( &world, &format!("/xrpc/sh.tangled.repo.compare?repo={did}&rev1={base}&rev2={head}"), ) .await; let entry = &value["format_patch"][0]; assert_eq!( entry["Author"]["Name"], "Lýna Þórsdóttir", "structured author the appview renders keeps the raw unicode" ); assert_eq!(entry["Author"]["Email"], "lyna@nel.pet"); assert_eq!( entry["Title"], "café changes", "structured subject the appview renders keeps the raw unicode" ); assert_eq!(entry["RawHeaders"]["Subject"][0], "[PATCH] café changes"); assert_eq!( entry["RawHeaders"]["From"][0], "Lýna Þórsdóttir " ); let raw = entry["Raw"].as_str().unwrap(); assert!( raw.contains("From: Lýna Þórsdóttir "), "knot emits the raw UTF-8 author instead of RFC2047 Q-encoding real format-patch uses" ); assert!( !raw.contains("=?UTF-8?") && !raw.contains("=?utf-8?"), "no MIME word-encoding headers" ); assert!(raw.contains("Subject: [PATCH] café changes")); assert!( raw.ends_with("-- \nknot"), "knot signs the patch w/ its own trailer" ); } #[tokio::test] async fn diff_reports_structured_fragments_and_stats() { let world = World::new(); let (did, work) = seeded(&world, "whelk"); let head = sh_git(work.path(), &["rev-parse", "HEAD"]); let value = get_json( &world, &format!("/xrpc/sh.tangled.repo.diff?repo={did}&ref={head}"), ) .await; assert_eq!(value["ref"], head.as_str()); let diff = &value["diff"]; assert_eq!(diff["stat"]["files_changed"].as_i64(), Some(1)); assert_eq!(diff["stat"]["insertions"].as_i64(), Some(1)); assert_eq!(diff["stat"]["deletions"].as_i64(), Some(1)); let file = &diff["diff"][0]; assert_eq!(file["name"]["new"], "README.md"); assert_eq!(file["is_new"], false); let fragment = &file["text_fragments"][0]; assert_eq!(fragment["OldPosition"].as_i64(), Some(1)); assert_eq!(fragment["Comment"], ""); let lines = fragment["Lines"].as_array().unwrap(); assert!( lines .iter() .any(|line| line["Op"].as_i64() == Some(1) && line["Line"] == "hello\n") ); assert!( lines .iter() .any(|line| line["Op"].as_i64() == Some(2) && line["Line"] == "hello reef\n") ); assert_eq!(diff["commit"]["this"], head.as_str()); } #[tokio::test] async fn compare_produces_format_patches_and_a_combined_patch() { let world = World::new(); let (did, work) = seeded(&world, "conch"); let head = sh_git(work.path(), &["rev-parse", "HEAD"]); let base = sh_git(work.path(), &["rev-parse", "HEAD~3"]); let value = get_json( &world, &format!("/xrpc/sh.tangled.repo.compare?repo={did}&rev1={base}&rev2={head}"), ) .await; assert_eq!(value["rev1"], base.as_str()); assert_eq!(value["rev2"], head.as_str()); let patches = value["format_patch"].as_array().unwrap(); assert_eq!(patches.len(), 3, "three commits separate base from head"); let first = &patches[0]; assert_eq!(first["Title"], "add main"); assert_eq!(first["SubjectPrefix"], "[PATCH] "); assert_eq!(first["Committer"], serde_json::Value::Null); assert_eq!(first["CommitterDate"], "0001-01-01T00:00:00Z"); assert_eq!(first["Author"]["Name"], "nel"); assert_eq!(first["AuthorDate"], "2026-06-01T12:31:00+02:00"); assert_eq!(first["RawHeaders"]["Subject"][0], "[PATCH] add main"); let raw = first["Raw"].as_str().unwrap(); assert!(raw.starts_with(&format!( "From {} Mon Sep 17 00:00:00 2001\n", sh_git(work.path(), &["rev-parse", "HEAD~2"]) ))); assert!(raw.contains("Subject: [PATCH] add main")); assert!(raw.contains("diff --git a/src/main.rs b/src/main.rs")); assert!(raw.contains("new file mode 100644")); assert!(first["Files"][0]["NewName"] == "src/main.rs"); assert!(first["Files"][0]["IsNew"] == true); assert!(value["patch"].as_str().unwrap().contains("add logo")); let combined = value["combined_patch"].as_array().unwrap(); assert!(combined.iter().any(|file| file["NewName"] == "README.md")); assert!( value["combined_patch_raw"] .as_str() .unwrap() .contains("diff --git a/README.md b/README.md") ); assert_eq!( get_error( &world, &format!("/xrpc/sh.tangled.repo.compare?repo={did}&rev1=nope&rev2={head}"), ) .await, (StatusCode::BAD_REQUEST, "RevisionNotFound".to_string()) ); } #[tokio::test] async fn archive_conditional_and_range_semantics() { let world = World::new(); let (did, _work) = seeded(&world, "nautilus"); let path = format!("/xrpc/sh.tangled.repo.archive?repo={did}&ref=main"); let (status, headers, full) = get(&world, &path).await; assert_eq!(status, StatusCode::OK); assert_eq!( headers.get(header::CONTENT_TYPE).unwrap(), "application/gzip" ); assert_eq!( headers .get(header::CONTENT_DISPOSITION) .unwrap() .to_str() .unwrap(), format!("attachment; filename=\"{did}-main.tar.gz\"") ); let link = headers.get(header::LINK).unwrap().to_str().unwrap(); assert!(link.contains("rel=\"immutable\"")); assert!(link.contains("/xrpc/sh.tangled.repo.archive?format=tar.gz")); assert_eq!(headers.get(header::ACCEPT_RANGES).unwrap(), "bytes"); let last_modified = headers .get(header::LAST_MODIFIED) .expect("a pinned modification time backs date revalidation") .to_str() .unwrap() .to_string(); let etag = headers .get(header::ETAG) .unwrap() .to_str() .unwrap() .to_string(); assert!( etag.starts_with('"') && etag.ends_with('"'), "a strong etag is quoted" ); assert_eq!(&full[..2], &[0x1f, 0x8b]); let mut range = HeaderMap::new(); range.insert(header::RANGE, "bytes=0-3".parse().unwrap()); let (status, range_headers, partial) = get_with_headers(&world, &path, range).await; assert_eq!(status, StatusCode::PARTIAL_CONTENT); assert_eq!( range_headers .get(header::CONTENT_RANGE) .unwrap() .to_str() .unwrap(), format!("bytes 0-3/{}", full.len()) ); assert_eq!( partial.as_ref(), &full[..4], "a resumed range regenerates byte for byte" ); let mut conditional = HeaderMap::new(); conditional.insert(header::IF_NONE_MATCH, etag.parse().unwrap()); let (status, cond_headers, conditional_body) = get_with_headers(&world, &path, conditional).await; assert_eq!(status, StatusCode::NOT_MODIFIED); assert_eq!( cond_headers.get(header::ETAG).unwrap().to_str().unwrap(), etag ); assert!(conditional_body.is_empty(), "a 304 has no body"); let (status, _) = get_error( &world, &format!("/xrpc/sh.tangled.repo.archive?repo={did}&ref=main&format=tar.bz2"), ) .await; assert_eq!(status, StatusCode::BAD_REQUEST); let mut etag_match = HeaderMap::new(); etag_match.insert(header::RANGE, "bytes=0-3".parse().unwrap()); etag_match.insert(header::IF_RANGE, etag.parse().unwrap()); let (status, if_range_headers, partial) = get_with_headers(&world, &path, etag_match).await; assert_eq!( status, StatusCode::PARTIAL_CONTENT, "a matching content etag resumes the range" ); assert_eq!( if_range_headers .get(header::CONTENT_RANGE) .unwrap() .to_str() .unwrap(), format!("bytes 0-3/{}", full.len()) ); assert_eq!(partial.as_ref(), &full[..4]); let mut etag_stale = HeaderMap::new(); etag_stale.insert(header::RANGE, "bytes=0-3".parse().unwrap()); etag_stale.insert(header::IF_RANGE, "\"0000\"".parse().unwrap()); let (status, _, body) = get_with_headers(&world, &path, etag_stale).await; assert_eq!( status, StatusCode::OK, "a stale content etag falls back to the full body" ); assert_eq!(body, full, "the full archive comes back byte for byte"); let mut weak = HeaderMap::new(); weak.insert(header::RANGE, "bytes=0-3".parse().unwrap()); weak.insert(header::IF_RANGE, format!("W/{etag}").parse().unwrap()); let (status, _, body) = get_with_headers(&world, &path, weak).await; assert_eq!( status, StatusCode::OK, "a weak validator never serves a range, per strong-comparison rules" ); assert_eq!(body, full); let mut date_match = HeaderMap::new(); date_match.insert(header::RANGE, "bytes=0-3".parse().unwrap()); date_match.insert(header::IF_RANGE, last_modified.parse().unwrap()); let (status, date_headers, partial) = get_with_headers(&world, &path, date_match).await; assert_eq!( status, StatusCode::PARTIAL_CONTENT, "a date matching the pinned last-modified resumes the range" ); assert_eq!( date_headers .get(header::CONTENT_RANGE) .unwrap() .to_str() .unwrap(), format!("bytes 0-3/{}", full.len()) ); assert_eq!(partial.as_ref(), &full[..4]); let mut date_stale = HeaderMap::new(); date_stale.insert(header::RANGE, "bytes=0-3".parse().unwrap()); date_stale.insert( header::IF_RANGE, "Wed, 21 Oct 2015 07:28:00 GMT".parse().unwrap(), ); let (status, _, body) = get_with_headers(&world, &path, date_stale).await; assert_eq!( status, StatusCode::OK, "a date that doesn't match the pinned last-modified falls back to the full body" ); assert_eq!(body, full); assert_immutable_round_trip(&world, &headers, &full, &etag).await; } #[tokio::test] async fn archive_etag_distinguishes_refs_that_share_a_commit() { let world = World::new(); let (did, work) = seeded(&world, "scallop"); let bare = world.layout.repo_path(&did).unwrap(); sh_git(work.path(), &["branch", "release", "main"]); sh_git( work.path(), &["push", "-q", bare.to_str().unwrap(), "refs/heads/release"], ); let (main_etag, _main_last_modified, main_body) = archive_full(&world, &did).await; let (status, release_headers, release_body) = get( &world, &format!("/xrpc/sh.tangled.repo.archive?repo={did}&ref=release"), ) .await; assert_eq!(status, StatusCode::OK); let release_etag = release_headers .get(header::ETAG) .unwrap() .to_str() .unwrap() .to_string(); assert_ne!( main_etag, release_etag, "two refs at one commit name different archive prefixes, so the strong etag must differ" ); assert_ne!( main_body, release_body, "the archives use different top-level directories and differ byte for byte" ); let mut conditional = HeaderMap::new(); conditional.insert(header::IF_NONE_MATCH, main_etag.parse().unwrap()); let (status, _, _) = get_with_headers( &world, &format!("/xrpc/sh.tangled.repo.archive?repo={did}&ref=release"), conditional, ) .await; assert_eq!( status, StatusCode::OK, "main's etag mustn't satisfy a conditional request for the release archive" ); } #[tokio::test] async fn archive_serves_a_sha256_repo_with_a_stable_etag() { let world = World::sha256(); let (did, _work) = seeded_with_format(&world, "nautilus", ObjectFormat::SHA256); let (status, headers, full) = get( &world, &format!("/xrpc/sh.tangled.repo.archive?repo={did}&ref=main"), ) .await; assert_eq!(status, StatusCode::OK); assert_eq!( headers.get(header::CONTENT_TYPE).unwrap(), "application/gzip" ); assert_eq!(&full[..2], &[0x1f, 0x8b]); let etag = headers .get(header::ETAG) .unwrap() .to_str() .unwrap() .to_string(); assert_immutable_round_trip(&world, &headers, &full, &etag).await; let mut conditional = HeaderMap::new(); conditional.insert(header::IF_NONE_MATCH, etag.parse().unwrap()); let (status, _, body) = get_with_headers( &world, &format!("/xrpc/sh.tangled.repo.archive?repo={did}&ref=main"), conditional, ) .await; assert_eq!( status, StatusCode::NOT_MODIFIED, "conditional revalidation works under sha256" ); assert!(body.is_empty()); } #[tokio::test] async fn languages_detect_rust_and_markdown_stays_out() { let world = World::new(); let (did, _work) = seeded(&world, "uni"); let value = get_json( &world, &format!("/xrpc/sh.tangled.repo.languages?repo={did}&ref=main"), ) .await; let languages = value["languages"].as_array().unwrap(); assert_eq!( languages.len(), 1, "only Rust counts: markdown is prose, png is binary" ); assert_eq!(languages[0]["name"], "Rust"); assert_eq!(languages[0]["percentage"].as_i64(), Some(100)); assert!(languages[0]["size"].as_i64().unwrap() > 0); assert_eq!(value["totalFiles"].as_i64(), Some(1)); } #[tokio::test] async fn repo_metadata_resolves_and_fails_closed() { let world = World::new(); let (did, _work) = seeded(&world, "cuttle"); let value = get_json( &world, &format!("/xrpc/sh.tangled.repo.getDefaultBranch?repo={did}"), ) .await; assert_eq!(value["name"], "main"); assert_eq!(value["hash"], ""); assert_eq!(value["when"], "1970-01-01T00:00:00Z"); let described = get_json( &world, &format!("/xrpc/sh.tangled.repo.describeRepo?repoDid={did}"), ) .await; assert_eq!(described["repoDid"], did.as_str()); assert_eq!(described["ownerDid"], OWNER); assert_eq!(described["rkey"], "cuttle"); assert_eq!( get_error( &world, "/xrpc/sh.tangled.repo.describeRepo?repoDid=did:plc:doesnotexist", ) .await, (StatusCode::NOT_FOUND, "RepoNotFound".to_string()) ); let by_owner = get_json( &world, &format!("/xrpc/sh.tangled.repo.getDefaultBranch?repo={OWNER}/cuttle"), ) .await; assert_eq!(by_owner["name"], "main"); assert_eq!( get_error( &world, "/xrpc/sh.tangled.repo.getDefaultBranch?repo=did:plc:unregistered", ) .await, (StatusCode::NOT_FOUND, "RepoNotFound".to_string()) ); let (status, _) = get_error(&world, "/xrpc/sh.tangled.repo.getDefaultBranch?repo=oyster").await; assert_eq!(status, StatusCode::BAD_REQUEST); assert_eq!( get_error( &world, &format!("/xrpc/sh.tangled.repo.log?repo={did}&ref=mangrove"), ) .await, (StatusCode::NOT_FOUND, "RefNotFound".to_string()) ); let (status, error) = get_error(&world, "/xrpc/sh.tangled.repo.getDefaultBranch").await; assert_eq!(status, StatusCode::BAD_REQUEST); assert_eq!( error, "InvalidRequest", "a structurally unsound query gets the lexicon error shape instead of the runtime's default plaintext" ); } #[tokio::test] async fn list_refs_reports_paginates_and_drains() { let world = World::new(); let (did, work) = seeded(&world, "whelk"); let head = sh_git(work.path(), &["rev-parse", "HEAD"]); let value = get_json(&world, &format!("/xrpc/sh.tangled.git.listRefs?repo={did}")).await; assert_eq!( ref_names(&value, "refs"), vec![ "refs/heads/main", "refs/tags/lightweight", "refs/tags/v1.0.0" ] ); let main = value["refs"] .as_array() .unwrap() .iter() .find(|entry| entry["ref"] == "refs/heads/main") .unwrap(); assert_eq!(main["sha"], head); assert_eq!(value["defaultBranch"]["ref"], "refs/heads/main"); assert_eq!(value["defaultBranch"]["head"], head); assert!(value["cursor"].is_null()); let first = get_json( &world, &format!("/xrpc/sh.tangled.git.listRefs?repo={did}&limit=2"), ) .await; assert_eq!(ref_names(&first, "refs").len(), 2); let cursor = first["cursor"].as_str().unwrap().to_string(); let second = get_json( &world, &format!("/xrpc/sh.tangled.git.listRefs?repo={did}&limit=2&cursor={cursor}"), ) .await; assert_eq!(ref_names(&second, "refs").len(), 1); assert!(second["cursor"].is_null()); let refs = get_json( &world, &format!( "/xrpc/sh.tangled.git.listRefs?repo={did}&cursor={}", usize::MAX ), ) .await; assert!(ref_names(&refs, "refs").is_empty()); assert!(refs["cursor"].is_null()); let repos = get_json( &world, &format!("/xrpc/sh.tangled.sync.listRepos?cursor={}", usize::MAX), ) .await; assert!(repo_dids(&repos).is_empty()); assert!(repos["cursor"].is_null()); } #[tokio::test] async fn a_hidden_staging_ref_resolves_for_fork_comparison_reads() { let world = World::new(); let (did, work) = seeded(&world, "limpet"); let bare = world.layout.repo_path(&did).unwrap(); sh_git(work.path(), &["checkout", "-q", "-b", "upstream"]); commit_file( work.path(), "upstream.txt", b"upstream\n", "upstream moved", "2026-06-01T12:50:00+02:00", ); let upstream = sh_git(work.path(), &["rev-parse", "HEAD"]); sh_git( work.path(), &[ "push", "-q", bare.to_str().unwrap(), "HEAD:refs/hidden/main/main", ], ); sh_git(work.path(), &["checkout", "-q", "main"]); commit_file( work.path(), "ours.txt", b"ours\n", "fork work", "2026-06-01T12:55:00+02:00", ); sh_git( work.path(), &["push", "-q", bare.to_str().unwrap(), "HEAD:refs/heads/main"], ); let comparison = get_json( &world, &format!("/xrpc/sh.tangled.repo.compare?repo={did}&rev1=hidden/main/main&rev2=main"), ) .await; assert_eq!(comparison["rev1"].as_str().unwrap(), upstream); assert!(!comparison["format_patch"].as_array().unwrap().is_empty()); let log = get_json( &world, &format!("/xrpc/sh.tangled.repo.log?repo={did}&ref=refs/hidden/main/main"), ) .await; assert!(!log["commits"].as_array().unwrap().is_empty()); let (status, _) = get_error( &world, &format!("/xrpc/sh.tangled.repo.log?repo={did}&ref={upstream}"), ) .await; assert_eq!( status, StatusCode::NOT_FOUND, "a raw oid reachable only through the hidden ref mustn't resolve" ); let (status, error) = get_error( &world, &format!("/xrpc/sh.tangled.repo.compare?repo={did}&rev1={upstream}&rev2=main"), ) .await; assert_eq!(status, StatusCode::BAD_REQUEST); assert_eq!(error, "RevisionNotFound"); } #[tokio::test] async fn the_cob_ref_namespace_is_invisible_across_every_read() { let world = World::new(); let (did, work) = seeded(&world, "anemone"); let head = sh_git(work.path(), &["rev-parse", "HEAD"]); let bare = world.layout.repo_path(&did).unwrap(); let cob = "refs/cobs/sh.tangled.repo.collaborator/x"; sh_git(bare.as_path(), &["update-ref", cob, &head]); let value = get_json(&world, &format!("/xrpc/sh.tangled.git.listRefs?repo={did}")).await; assert!( ref_names(&value, "refs") .iter() .all(|name| !name.starts_with("refs/cobs/")), "reserved cob ref leaked into listRefs" ); let w = &world; let d = &did; let named_routes: &[(&str, &str)] = &[ ("log", ""), ("tree", ""), ("blob", "&path=README.md"), ("diff", ""), ("archive", ""), ("languages", ""), ]; stream::iter(named_routes) .for_each(|&(route, suffix)| async move { let (status, _) = get_error( w, &format!("/xrpc/sh.tangled.repo.{route}?repo={d}&ref={cob}{suffix}"), ) .await; assert_eq!( status, StatusCode::NOT_FOUND, "{route} mustn't resolve reserved cobs ref" ); }) .await; let (status, _) = get_error( &world, &format!("/xrpc/sh.tangled.repo.log?repo={did}&ref=cobs/sh.tangled.repo.collaborator/x"), ) .await; assert_eq!( status, StatusCode::NOT_FOUND, "reserved namespace shorthand mustn't resolve either" ); let (status, _) = get_error( &world, &format!("/xrpc/sh.tangled.repo.compare?repo={did}&rev1={cob}&rev2={head}"), ) .await; assert_eq!(status, StatusCode::BAD_REQUEST); commit_file( work.path(), "secret.txt", b"hidden\n", "secret", "2026-06-01T12:40:00+02:00", ); let hidden = sh_git(work.path(), &["rev-parse", "HEAD"]); sh_git( work.path(), &[ "push", "-q", bare.to_str().unwrap(), "HEAD:refs/cobs/sh.tangled.repo.collaborator/secret", ], ); let hidden_ref = &hidden; let hidden_routes: &[(&str, &str)] = &[ ("log", ""), ("tree", "&path=secret.txt"), ("diff", ""), ("archive", ""), ("languages", ""), ]; stream::iter(hidden_routes) .for_each(|&(route, suffix)| async move { let (status, _) = get_error( w, &format!("/xrpc/sh.tangled.repo.{route}?repo={d}&ref={hidden_ref}{suffix}"), ) .await; assert_eq!( status, StatusCode::NOT_FOUND, "{route} mustn't serve a commit reachable only through cob ref" ); }) .await; let (status, _) = get_error( &world, &format!("/xrpc/sh.tangled.repo.blob?repo={did}&ref={hidden}&path=secret.txt"), ) .await; assert_eq!(status, StatusCode::NOT_FOUND); let (status, error) = get_error( &world, &format!("/xrpc/sh.tangled.repo.compare?repo={did}&rev1={head}&rev2={hidden}"), ) .await; assert_eq!(status, StatusCode::BAD_REQUEST); assert_eq!(error, "RevisionNotFound"); let still_public = get_json( &world, &format!("/xrpc/sh.tangled.repo.log?repo={did}&ref={head}"), ) .await; assert_eq!(still_public["total"].as_i64(), Some(4)); } #[tokio::test] async fn the_list_reads_reject_malformed_paging_params() { let world = World::new(); let (did, _work) = seeded(&world, "barnacle"); let queries: Vec = vec![ format!("/xrpc/sh.tangled.git.listRefs?repo={did}&limit=abc"), format!("/xrpc/sh.tangled.git.listRefs?repo={did}&cursor=notanint"), "/xrpc/sh.tangled.sync.listRepos?limit=abc".to_string(), "/xrpc/sh.tangled.sync.listRepos?cursor=notanint".to_string(), "/xrpc/sh.tangled.sync.listRepos?order=sideways".to_string(), ]; let w = &world; stream::iter(queries.iter()) .for_each(|query| async move { let (status, error) = get_error(w, query).await; assert_eq!(status, StatusCode::BAD_REQUEST, "query {query}"); assert_eq!(error, "InvalidRequest", "query {query}"); }) .await; let clamped = get_json( &world, &format!("/xrpc/sh.tangled.git.listRefs?repo={did}&limit=5000"), ) .await; assert_eq!( ref_names(&clamped, "refs").len(), 3, "an oversize limit clamps to the max instead of erroring" ); } #[tokio::test] async fn list_repos_lists_hosted_repos_with_order_and_pagination() { let world = World::new(); let (mussel, _a) = seeded(&world, "mussel"); let (nautilus, _b) = seeded(&world, "nautilus"); let (scallop, _c) = seeded(&world, "scallop"); let desc = get_json(&world, "/xrpc/sh.tangled.sync.listRepos").await; assert_eq!( repo_dids(&desc), vec![ scallop.as_str().to_string(), nautilus.as_str().to_string(), mussel.as_str().to_string(), ] ); assert_eq!(desc["repos"][0]["status"], "active"); assert_eq!(desc["repos"][0]["defaultBranch"]["ref"], "refs/heads/main"); let asc = get_json(&world, "/xrpc/sh.tangled.sync.listRepos?order=asc").await; assert_eq!( repo_dids(&asc), vec![ mussel.as_str().to_string(), nautilus.as_str().to_string(), scallop.as_str().to_string(), ] ); let page = get_json(&world, "/xrpc/sh.tangled.sync.listRepos?order=asc&limit=2").await; assert_eq!(page["repos"].as_array().unwrap().len(), 2); let cursor = page["cursor"].as_str().unwrap().to_string(); let rest = get_json( &world, &format!("/xrpc/sh.tangled.sync.listRepos?order=asc&limit=2&cursor={cursor}"), ) .await; assert_eq!(repo_dids(&rest), vec![scallop.as_str().to_string()]); assert!(rest["cursor"].is_null()); } #[tokio::test] async fn every_projection_read_fails_closed_while_warming() { let world = World::warming(); let did = RepoDid::new("did:plc:limpetfixture").unwrap(); let cases: Vec<(String, Option<&str>)> = vec![ ( "/xrpc/sh.tangled.sync.listRepos".to_string(), Some("ProjectionWarming"), ), ( format!("/xrpc/sh.tangled.repo.getDefaultBranch?repo={did}"), Some("ProjectionWarming"), ), ( format!("/xrpc/sh.tangled.repo.describeRepo?repoDid={did}"), Some("ProjectionWarming"), ), ( "/xrpc/sh.tangled.knot.listMembers?subject=did:web:knot.nel.pet".to_string(), None, ), ( "/xrpc/sh.tangled.repo.listCollaborators?subject=did:plc:squid".to_string(), None, ), ]; let w = &world; stream::iter(cases.iter()) .for_each(|(path, expected)| async move { assert_warming(w, path, *expected).await; }) .await; } #[tokio::test] async fn branch_tips_render_edge_shapes() { let world = World::new(); let (did, work) = seeded(&world, "trochus"); let bare = world.layout.repo_path(&did).unwrap(); let bare_str = bare.to_str().unwrap().to_string(); sh_git(work.path(), &["checkout", "-q", "-b", "side", "HEAD~1"]); commit_file( work.path(), "side.txt", b"side\n", "side work", "2026-06-01T12:34:00+02:00", ); sh_git(work.path(), &["checkout", "-q", "main"]); sh_git_at( work.path(), "2026-06-01T12:35:00+02:00", &["merge", "-q", "--no-ff", "-m", "merge side", "side"], ); sh_git(work.path(), &["push", "-q", &bare_str, "main"]); let first_parent = sh_git(work.path(), &["rev-parse", "HEAD^1"]); let second_parent = sh_git(work.path(), &["rev-parse", "HEAD^2"]); let tag_object = sh_git(work.path(), &["rev-parse", "v1.0.0"]); std::fs::write(bare.join("refs/heads/tagtip"), format!("{tag_object}\n")).unwrap(); let root_commit = sh_git(work.path(), &["rev-list", "--max-parents=0", "HEAD"]); std::fs::write(bare.join("refs/heads/roottip"), format!("{root_commit}\n")).unwrap(); let value = get_json( &world, &format!("/xrpc/sh.tangled.repo.branches?repo={did}"), ) .await; let branches = value["branches"].as_array().unwrap(); assert_eq!(branches.len(), 3); let branch = |name: &str| { branches .iter() .find(|branch| branch["reference"]["name"] == name) .unwrap() }; let parents = |branch: &serde_json::Value| -> Vec { branch["commit"]["ParentHashes"] .as_array() .unwrap() .iter() .map(|parent| { parent .as_array() .unwrap() .iter() .map(|byte| format!("{:02x}", byte.as_u64().unwrap())) .collect() }) .collect() }; let main = branch("main"); assert_eq!( parents(main), vec![first_parent, second_parent], "merge tip must report both parents in order" ); assert_eq!(main["commit"]["Author"]["Name"], "nel"); assert!( parents(branch("roottip")).is_empty(), "root tip must report no parents" ); let tagtip = branch("tagtip"); assert!( parents(tagtip).is_empty(), "a non-commit tip must report no parents" ); assert_eq!(tagtip["reference"]["hash"], tag_object.as_str()); assert_eq!(tagtip["commit"]["Author"]["Name"], ""); assert_eq!(tagtip["commit"]["Author"]["When"], "0001-01-01T00:00:00Z"); assert_eq!(tagtip["commit"]["Message"], "release one"); assert!( tagtip["commit"]["TreeHash"] .as_array() .unwrap() .iter() .all(|byte| byte.as_u64() == Some(0)), "opaque tip has the zero tree hash" ); } #[tokio::test] async fn a_submodule_path_in_the_tree_is_path_not_found() { let world = World::new(); let (did, work) = seeded(&world, "razorclam"); let bare = world .layout .repo_path(&did) .unwrap() .to_str() .unwrap() .to_string(); let head = sh_git(work.path(), &["rev-parse", "HEAD"]); sh_git( work.path(), &[ "update-index", "--add", "--cacheinfo", &format!("160000,{head},vendor/dep"), ], ); sh_git(work.path(), &["commit", "-q", "-m", "add gitlink"]); sh_git(work.path(), &["push", "-q", &bare, "main"]); assert_eq!( get_error( &world, &format!("/xrpc/sh.tangled.repo.tree?repo={did}&ref=main&path=vendor/dep"), ) .await, (StatusCode::NOT_FOUND, "PathNotFound".to_string()) ); } #[tokio::test] async fn a_blob_past_the_derived_serving_limit_is_a_named_error() { let world = World::with_response_limit(ResponseLimit::new(1024)); let (did, bare, work) = empty_repo(&world, "auger"); commit_file( work.path(), "big.txt", "a".repeat(2_000).as_bytes(), "big file", "2026-06-01T12:30:00+02:00", ); sh_git(work.path(), &["push", "-q", &bare, "main"]); let (status, error) = get_error( &world, &format!("/xrpc/sh.tangled.repo.blob?repo={did}&ref=main&path=big.txt"), ) .await; assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE); assert_eq!( error, "BlobTooLarge", "blob limit is reached before the generic response limit" ); let (status, _, body) = get( &world, &format!("/xrpc/sh.tangled.repo.blob?repo={did}&ref=main&path=big.txt&raw=true"), ) .await; assert_eq!(status, StatusCode::OK, "raw serving keeps full limit"); assert_eq!(body.len(), 2_000); } #[tokio::test] async fn an_oversized_readme_is_omitted_from_the_tree() { let world = World::with_response_limit(ResponseLimit::new(2_048)); let (did, bare, work) = empty_repo(&world, "cowrie"); commit_file( work.path(), "README.md", format!("# reef\n\n{}\n", "r".repeat(1_000)).as_bytes(), "huge readme", "2026-06-01T12:30:00+02:00", ); sh_git(work.path(), &["push", "-q", &bare, "main"]); let value = get_json( &world, &format!("/xrpc/sh.tangled.repo.tree?repo={did}&ref=main"), ) .await; assert_eq!(value["files"][0]["name"], "README.md"); assert_eq!( value["readme"]["contents"], "", "readme past the serving limit is omitted instead of failing the whole tree" ); } #[tokio::test] async fn a_comparison_spanning_too_many_commits_is_refused() { let world = World::new(); let (did, work) = seeded(&world, "abalone"); let bare = world .layout .repo_path(&did) .unwrap() .to_str() .unwrap() .to_string(); let base = sh_git(work.path(), &["rev-parse", "HEAD"]); (0..501).for_each(|index| { sh_git( work.path(), &["commit", "-q", "--allow-empty", "-m", &format!("c{index}")], ); }); sh_git(work.path(), &["push", "-q", &bare, "main"]); let head = sh_git(work.path(), &["rev-parse", "HEAD"]); let (status, error) = get_error( &world, &format!("/xrpc/sh.tangled.repo.compare?repo={did}&rev1={base}&rev2={head}"), ) .await; assert_eq!(status, StatusCode::BAD_REQUEST); assert_eq!(error, "CompareError"); } #[tokio::test] async fn archive_rejects_traversal_prefixes_and_sanitizes_the_filename() { let world = World::new(); let (did, work) = seeded(&world, "cockle"); let bare = world .layout .repo_path(&did) .unwrap() .to_str() .unwrap() .to_string(); let (status, error) = get_error( &world, &format!("/xrpc/sh.tangled.repo.archive?repo={did}&ref=main&prefix=../evil"), ) .await; assert_eq!(status, StatusCode::BAD_REQUEST); assert_eq!(error, "InvalidRequest"); sh_git(work.path(), &["branch", "a\"b"]); sh_git(work.path(), &["push", "-q", &bare, "refs/heads/a\"b"]); let (status, headers, _) = get( &world, &format!("/xrpc/sh.tangled.repo.archive?repo={did}&ref=a%22b"), ) .await; assert_eq!(status, StatusCode::OK); let disposition = headers .get(header::CONTENT_DISPOSITION) .unwrap() .to_str() .unwrap(); assert_eq!( disposition, format!("attachment; filename=\"{did}-a-b.tar.gz\""), "quote in the ref name mustn't break the header quoting" ); } #[tokio::test] async fn an_archive_larger_than_the_configured_limit_is_refused() { let world = World::with_archive_limit(ArchiveLimit::new(64)); let (did, _work) = seeded(&world, "murex"); let (status, error) = get_error( &world, &format!("/xrpc/sh.tangled.repo.archive?repo={did}&ref=main"), ) .await; assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE); assert_eq!(error, "RequestTooLarge"); } #[tokio::test] async fn an_oversized_read_response_is_refused() { let world = World::with_response_limit(ResponseLimit::new(256)); let (did, _work) = seeded(&world, "clam"); let (status, error) = get_error( &world, &format!("/xrpc/sh.tangled.repo.log?repo={did}&ref=main"), ) .await; assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE); assert_eq!(error, "RequestTooLarge"); let small = get_json( &world, &format!("/xrpc/sh.tangled.repo.getDefaultBranch?repo={did}"), ) .await; assert_eq!(small["name"], "main"); } #[tokio::test] async fn languages_omit_files_with_zero_size() { let world = World::new(); let (did, bare, work) = empty_repo(&world, "topshell"); commit_file( work.path(), "lib.rs", b"", "empty rust file", "2026-06-01T12:30:00+02:00", ); sh_git(work.path(), &["push", "-q", &bare, "main"]); let value = get_json( &world, &format!("/xrpc/sh.tangled.repo.languages?repo={did}&ref=main"), ) .await; assert!( value["languages"].is_null(), "zero-byte file mustn't surface as a language with a NaN percentage" ); assert!(value.get("totalSize").is_none()); assert!(value.get("totalFiles").is_none()); } #[tokio::test] async fn a_compare_patch_round_trips_through_merge_check() { let world = World::new(); let (_did, main_sha, feature_sha) = seeded_feature_branch(&world, "periwinkle"); let registered = RepoDid::new("did:plc:periwinklefixture").unwrap(); let compared = get_json( &world, &format!( "/xrpc/sh.tangled.repo.compare?repo={registered}&rev1={main_sha}&rev2={feature_sha}" ), ) .await; let patch = compared["patch"].as_str().unwrap(); let (status, check) = post_json( &world, "/xrpc/sh.tangled.repo.mergeCheck", serde_json::json!({ "did": OWNER, "name": "periwinkle", "branch": "main", "patch": patch, }), ) .await; assert_eq!(status, StatusCode::OK); assert_eq!( check["is_conflicted"], serde_json::Value::Bool(false), "knot's own compare output must pass its own merge check: {check}" ); let (status, stale) = post_json( &world, "/xrpc/sh.tangled.repo.mergeCheck", serde_json::json!({ "did": OWNER, "name": "periwinkle", "branch": "feature", "patch": patch, }), ) .await; assert_eq!(status, StatusCode::OK); assert_eq!( stale["is_conflicted"], serde_json::Value::Bool(true), "re-applying an already-landed patch must conflict: {stale}" ); } #[tokio::test] async fn list_members_pages_in_the_wire_shape() { let world = World::new(); world.add_member("did:plc:limpet", OWNER, 1_000); world.add_member("did:plc:scallop", OWNER, 2_000); world.add_member("did:plc:whelk", OWNER, 3_000); let page = get_json( &world, "/xrpc/sh.tangled.knot.listMembers?subject=did:web:knot.nel.pet&limit=2", ) .await; let items = page["items"].as_array().unwrap(); assert_eq!(items.len(), 2, "default order is createdAt descending"); assert_eq!(items[0]["subject"], "did:plc:whelk"); assert_eq!(items[0]["addedBy"], OWNER); assert_eq!(items[0]["createdAt"], "1970-01-01T00:50:00Z"); assert!( items[0].get("uri").is_none() && items[0].get("cid").is_none(), "knot-owned member has no backing record, so uri and cid are omitted" ); assert_eq!(items[1]["subject"], "did:plc:scallop"); let cursor = page["cursor"].as_str().unwrap(); let next = get_json( &world, &format!( "/xrpc/sh.tangled.knot.listMembers?subject=did:web:knot.nel.pet&limit=2&cursor={cursor}" ), ) .await; let items = next["items"].as_array().unwrap(); assert_eq!(items.len(), 1); assert_eq!(items[0]["subject"], "did:plc:limpet"); assert!(next.get("cursor").is_none(), "drained list has no cursor"); let ascending = get_json( &world, "/xrpc/sh.tangled.knot.listMembers?subject=did:web:knot.nel.pet&order=asc&limit=1", ) .await; assert_eq!(ascending["items"][0]["subject"], "did:plc:limpet"); } #[tokio::test] async fn list_members_rejects_malformed_params_and_clamps_the_limit() { let world = World::new(); world.add_member("did:plc:limpet", OWNER, 1_000); let queries: &[&str] = &[ "limit=abc&subject=did:web:knot.nel.pet", "cursor=notanint&subject=did:web:knot.nel.pet", "order=ascending&subject=did:web:knot.nel.pet", "limit=2", "subject=knot.nel.pet", ]; let w = &world; stream::iter(queries.iter().copied()) .for_each(|query| async move { let (status, _) = get_error(w, &format!("/xrpc/sh.tangled.knot.listMembers?{query}")).await; assert_eq!(status, StatusCode::BAD_REQUEST, "query {query}"); }) .await; let clamped = get_json( &world, "/xrpc/sh.tangled.knot.listMembers?subject=did:web:knot.nel.pet&limit=5000", ) .await; assert_eq!(clamped["items"].as_array().unwrap().len(), 1); } #[tokio::test] async fn list_collaborators_is_scoped_to_the_repo() { let world = World::new(); let squid = RepoDid::new("did:plc:squid").unwrap(); let clam = RepoDid::new("did:plc:clam").unwrap(); world.layout.create(&squid).unwrap(); world.layout.create(&clam).unwrap(); world.register(&squid, "squid"); world.register(&clam, "clam"); world.add_collaborator(&squid, "did:plc:lyna", OWNER, 1_000); world.add_collaborator(&clam, "did:plc:bailey", OWNER, 2_000); let page = get_json( &world, "/xrpc/sh.tangled.repo.listCollaborators?subject=did:plc:squid", ) .await; let items = page["items"].as_array().unwrap(); assert_eq!(items.len(), 1); assert_eq!(items[0]["subject"], "did:plc:lyna"); assert_eq!(items[0]["addedBy"], OWNER); assert_eq!(items[0]["createdAt"], "1970-01-01T00:16:40Z"); assert!(items[0].get("uri").is_none() && items[0].get("cid").is_none()); let unknown = get_json( &world, "/xrpc/sh.tangled.repo.listCollaborators?subject=did:plc:unhosted", ) .await; assert!( unknown["items"].as_array().unwrap().is_empty(), "unhosted repo has no collaborators, the answer is an empty list" ); let (status, _) = get_error( &world, "/xrpc/sh.tangled.repo.listCollaborators?subject=notadid", ) .await; assert_eq!(status, StatusCode::BAD_REQUEST); } #[tokio::test] async fn the_subject_tie_break_stays_ascending_in_both_directions() { let world = World::new(); world.add_member("did:plc:whelk", OWNER, 1_000); world.add_member("did:plc:limpet", OWNER, 1_000); let subjects = |page: &serde_json::Value| -> Vec { page["items"] .as_array() .unwrap() .iter() .map(|item| item["subject"].as_str().unwrap().to_string()) .collect() }; let asc = get_json( &world, "/xrpc/sh.tangled.knot.listMembers?subject=did:web:knot.nel.pet&order=asc", ) .await; let desc = get_json( &world, "/xrpc/sh.tangled.knot.listMembers?subject=did:web:knot.nel.pet&order=desc", ) .await; assert_eq!(subjects(&asc), vec!["did:plc:limpet", "did:plc:whelk"]); assert_eq!( subjects(&desc), vec!["did:plc:limpet", "did:plc:whelk"], "equal-createdAt entries keep an ascending subject tie-break regardless of sort direction" ); } #[tokio::test] async fn repo_error_outranks_paging_in_any_param_order() { let world = World::new(); let orders: &[&str] = &[ "/xrpc/sh.tangled.repo.listCollaborators?subject=notadid&limit=abc", "/xrpc/sh.tangled.repo.listCollaborators?limit=abc&subject=notadid", ]; let w = &world; stream::iter(orders.iter().copied()) .for_each(|query| async move { let (_, error) = get_error(w, query).await; assert_eq!( error, "InvalidRepo", "the repo extractor runs before paging by signature position for {query}" ); }) .await; } #[tokio::test] async fn service_metadata_endpoints_answer() { let world = World::new(); let wire = get_json(&world, "/xrpc/sh.tangled.knot.version").await; assert_eq!(wire["version"], "v1.15.0"); assert_eq!(wire["capabilities"], serde_json::json!(["knot-acl"])); let owner = get_json(&world, "/xrpc/sh.tangled.owner").await; assert_eq!(owner["owner"], OWNER); } fn publish_update(world: &World, repo: &str) -> EventCursor { world.state.events.publish(&GitRefUpdate::new( RepoDid::new(repo).unwrap(), Some(OwnerDid::new(OWNER).unwrap()), AccountDid::new("did:plc:nel").unwrap(), )) } async fn serve_events(world: &World) -> std::net::SocketAddr { let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let router = world.router.clone(); tokio::spawn(async move { axum::serve( listener, router.into_make_service_with_connect_info::(), ) .await .unwrap(); }); addr } type Ws = tokio_tungstenite::WebSocketStream>; fn next_event(ws: &mut Ws) -> Pin + '_>> { Box::pin(async move { let received = tokio::time::timeout(std::time::Duration::from_secs(5), ws.next()) .await .expect("an event arrives within the timeout") .expect("the stream stays open") .expect("the frame is readable"); match received { tungstenite::Message::Text(text) => serde_json::from_str(text.as_str()).unwrap(), _ => next_event(ws).await, } }) } #[tokio::test] async fn the_events_stream_replays_resumes_and_rejects_bad_cursors() { let world = World::new(); let first = publish_update(&world, "did:plc:squid"); publish_update(&world, "did:plc:anemone"); let addr = serve_events(&world).await; let (mut ws, _) = tokio_tungstenite::connect_async(format!("ws://{addr}/events")) .await .unwrap(); let replayed_first = next_event(&mut ws).await; let replayed_second = next_event(&mut ws).await; assert_eq!(replayed_first["nsid"], "sh.tangled.git.refUpdate"); assert_eq!(replayed_first["event"]["repo"], "did:plc:squid"); assert_eq!(replayed_first["event"]["ownerDid"], OWNER); assert_eq!(replayed_first["event"]["committerDid"], "did:plc:nel"); assert_eq!(replayed_first["rkey"].as_str().unwrap().len(), 13); assert_eq!(replayed_second["event"]["repo"], "did:plc:anemone"); assert!( replayed_first["created"].as_i64().unwrap() < replayed_second["created"].as_i64().unwrap() ); publish_update(&world, "did:plc:whelk"); let live = next_event(&mut ws).await; assert_eq!(live["event"]["repo"], "did:plc:whelk"); let (mut resumed, _) = tokio_tungstenite::connect_async(format!("ws://{addr}/events?cursor={}", first.get())) .await .unwrap(); let resumed_event = next_event(&mut resumed).await; assert_eq!( resumed_event["event"]["repo"], "did:plc:anemone", "a cursor resumes past the event it names" ); let (mut garbled, _) = tokio_tungstenite::connect_async(format!("ws://{addr}/events?cursor=banana")) .await .unwrap(); let replayed = next_event(&mut garbled).await; assert_eq!( replayed["event"]["repo"], "did:plc:squid", "a garbled cursor replays from the start" ); } fn refused(result: Result) { match result { Err(tungstenite::Error::Http(response)) => { assert_eq!(response.status().as_u16(), 503); } Err(other) => panic!("expected an http refusal: {other}"), Ok(_) => panic!("a subscriber past the limit connected"), } } #[tokio::test] async fn a_subscriber_beyond_the_events_limit_is_refused() { let world = World::new(); let addr = serve_events(&world).await; let saturated: Vec<_> = (0..16u8) .map(|octet| { world .state .subscriber_gate .try_admit(std::net::IpAddr::V4(std::net::Ipv4Addr::new( 10, 0, 0, octet, ))) .expect("distinct peers fill the global limit") }) .collect(); refused(tokio_tungstenite::connect_async(format!("ws://{addr}/events")).await); drop(saturated); } #[tokio::test] async fn a_single_peer_cannot_monopolize_the_events_stream() { let world = World::new(); let addr = serve_events(&world).await; let held: Vec<_> = stream::iter(0..4) .then(|_| async { tokio_tungstenite::connect_async(format!("ws://{addr}/events")) .await .expect("a connection within the per-peer limit is admitted") .0 }) .collect() .await; refused(tokio_tungstenite::connect_async(format!("ws://{addr}/events")).await); drop(held); } #[tokio::test] async fn set_default_branch_resolves_an_at_uri_repo_and_an_existing_branch() { let world = World::new(); let (did, work) = seeded(&world, "coral"); let bare = world.layout.repo_path(&did).unwrap(); sh_git(work.path(), &["branch", "release", "main"]); sh_git( work.path(), &["push", "-q", bare.to_str().unwrap(), "refs/heads/release"], ); let (status, _) = post_authed( &world, "/xrpc/sh.tangled.repo.setDefaultBranch", OWNER, serde_json::json!({ "repo": format!("at://{OWNER}/sh.tangled.repo/coral"), "defaultBranch": "release", }), ) .await; assert_eq!(status, StatusCode::OK); let repo = world.layout.open(&did).unwrap(); assert_eq!( repo.default_branch().unwrap().as_str(), "refs/heads/release", "the default head moved to the requested branch" ); } #[tokio::test] async fn delete_branch_removes_a_non_default_branch_then_reports_it_gone() { let world = World::new(); let (did, work) = seeded(&world, "kelp"); let bare = world.layout.repo_path(&did).unwrap(); sh_git(work.path(), &["branch", "feature", "main"]); sh_git( work.path(), &["push", "-q", bare.to_str().unwrap(), "refs/heads/feature"], ); let at = format!("at://{OWNER}/sh.tangled.repo/kelp"); let (status, _) = post_authed( &world, "/xrpc/sh.tangled.repo.deleteBranch", OWNER, serde_json::json!({ "repo": at, "branch": "feature" }), ) .await; assert_eq!(status, StatusCode::OK); let (status, body) = post_authed( &world, "/xrpc/sh.tangled.repo.deleteBranch", OWNER, serde_json::json!({ "repo": at, "branch": "feature" }), ) .await; assert_eq!(status, StatusCode::NOT_FOUND, "second delete: {body}"); } #[tokio::test] async fn bad_post_bodies_are_invalid_request() { let world = World::new(); let (_kelp, _wk) = seeded(&world, "kelp"); let (_barnacle, _wb) = seeded(&world, "barnacle"); let cases: &[(&str, serde_json::Value)] = &[ ( "/xrpc/sh.tangled.repo.setDefaultBranch", serde_json::json!({ "repo": "not-an-at-uri", "defaultBranch": "main" }), ), ( "/xrpc/sh.tangled.repo.deleteBranch", serde_json::json!({ "repo": format!("at://{OWNER}/sh.tangled.repo/kelp"), "branch": "bad branch", }), ), ( "/xrpc/sh.tangled.repo.forkSync", serde_json::json!({ "did": OWNER, "name": "barnacle", "branch": "bad branch" }), ), ( "/xrpc/sh.tangled.repo.hiddenRef", serde_json::json!({ "repo": "nope", "forkRef": "feature", "remoteRef": "main" }), ), ]; let w = &world; stream::iter(cases) .for_each(|(path, value)| async move { assert_post_rejected(w, path, OWNER, value.clone()).await; }) .await; } #[tokio::test] async fn merge_applies_a_plain_patch_under_the_supplied_author() { let world = World::new(); let (_did, main_sha, feature_sha) = seeded_feature_branch(&world, "mussel"); let registered = RepoDid::new("did:plc:musselfixture").unwrap(); let compared = get_json( &world, &format!( "/xrpc/sh.tangled.repo.compare?repo={registered}&rev1={main_sha}&rev2={feature_sha}" ), ) .await; let patch = compared["combined_patch_raw"].as_str().unwrap().to_string(); let (status, body) = post_authed( &world, "/xrpc/sh.tangled.repo.merge", OWNER, serde_json::json!({ "did": OWNER, "name": "mussel", "branch": "main", "patch": patch, "authorName": "Teq", "authorEmail": "teq@nel.pet", "commitMessage": "merged kelp", }), ) .await; assert_eq!(status, StatusCode::OK, "merge failed: {body}"); let log = get_json( &world, &format!("/xrpc/sh.tangled.repo.log?repo={registered}&ref=main"), ) .await; let top = &log["commits"][0]; assert_eq!( top["author"]["Name"], "Teq", "the supplied author rode through" ); assert!( top["message"].as_str().unwrap().contains("merged kelp"), "the supplied commit message rode through: {}", top["message"] ); } #[tokio::test] async fn create_mints_a_did_plc_repo_with_the_requested_default_branch() { let world = World::new(); world.add_member(OWNER, OWNER, 1_000); let (status, body) = post_authed( &world, "/xrpc/sh.tangled.repo.create", OWNER, serde_json::json!({ "rkey": "squidkey", "name": "squid", "defaultBranch": "trunk" }), ) .await; assert_eq!(status, StatusCode::OK, "create failed: {body}"); let repo_did = body["repoDid"].as_str().unwrap(); assert!( repo_did.starts_with("did:plc:"), "minted a did:plc: {repo_did}" ); let did = RepoDid::new(repo_did).unwrap(); let repo = world.layout.open(&did).unwrap(); assert_eq!( repo.default_branch().unwrap().as_str(), "refs/heads/trunk", "the requested default branch became HEAD" ); }