use super::*;
#[test]
fn emitted_responses_have_the_shapes_hydrant_reads() {
let fixture = fixture("sha1");
let settled = settle_fixture(&fixture);
let generation = fixture._root.path().join("generation");
emit(&generation, std::slice::from_ref(&settled)).unwrap();
let dir = generation.join(ATPROTO_DIR);
let read = |path: String| -> serde_json::Value {
serde_json::from_slice(&std::fs::read(dir.join(path)).unwrap()).unwrap()
};
let name = settled.dir_name();
assert_eq!(
std::fs::read(dir.join(&name).join("repo.car")).unwrap(),
settled.car()
);
let latest = read(format!("{name}/latest.json"));
assert_eq!(latest["cid"], settled.commit.to_string());
assert_eq!(latest["rev"], settled.rev);
let status = read(format!("{name}/status.json"));
assert_eq!(status["did"], fixture.source.did.as_str());
assert_eq!(status["active"], true);
assert_eq!(status["rev"], settled.rev);
let described = read(format!("{name}/describe.json"));
let collections = described["collections"].as_array().unwrap();
for wanted in ["org.tangled.git.refs", "org.tangled.repo.manifest"] {
assert!(collections.iter().any(|c| c == wanted), "{wanted} listed");
}
let listed = read("list/1000-0.json".to_string());
assert_eq!(listed["repos"][0]["did"], fixture.source.did.as_str());
assert_eq!(listed["repos"][0]["head"], settled.commit.to_string());
assert_eq!(listed["repos"][0]["rev"], settled.rev);
assert_eq!(listed["repos"][0]["active"], true);
assert!(listed.get("cursor").is_none(), "one page needs no cursor");
let maps = std::fs::read_to_string(generation.join("nginx/maps").join(MAPS_FILE)).unwrap();
assert!(maps.contains(&format!("{} {name};", fixture.source.did)));
assert!(maps.contains("did%3Aplc%3Aabcdefghijklmnopqrstuvwx "));
assert!(maps.contains("1000| list/1000-0.json;"));
}
#[test]
fn listing_pages_chain_through_cursors() {
let fixture = fixture("sha1");
let settled = settle_fixture(&fixture);
let pools = fixture._root.path().join("pools");
let many: Vec<SettledRepo> = (0..120)
.map(|n| {
let did = RepoDid::new(format!("did:plc:r{n:03}")).unwrap();
let pool = pools.join(repo_token(did.as_str()));
std::fs::create_dir_all(pool.join(settled.commit.to_string())).unwrap();
SettledRepo {
did,
commit: settled.commit,
rev: settled.rev.clone(),
entry_key: String::new(),
pool,
blobs: BTreeSet::new(),
rows: Rows::default(),
}
})
.collect();
let generation = fixture._root.path().join("generation");
emit(&generation, &many).unwrap();
let page = |file: &str| -> serde_json::Value {
serde_json::from_slice(&std::fs::read(generation.join(ATPROTO_DIR).join(file)).unwrap())
.unwrap()
};
let first = page("list/50-0.json");
assert_eq!(first["repos"].as_array().unwrap().len(), 50);
assert_eq!(first["cursor"], "50");
assert_eq!(
page("list/50-100.json")["repos"].as_array().unwrap().len(),
20
);
assert!(page("list/50-100.json").get("cursor").is_none());
assert_eq!(
page("list/1000-0.json")["repos"].as_array().unwrap().len(),
120
);
let page_rows = list_page_rows();
let find = |rows: &[(String, String)], key: &str| {
rows.iter()
.find(|(r, _)| r == key)
.map(|(_, val)| val.clone())
};
assert_eq!(find(&page_rows, "").as_deref(), Some("500"));
assert_eq!(find(&page_rows, "1").as_deref(), Some("1"));
assert_eq!(find(&page_rows, "7").as_deref(), Some("1"));
assert_eq!(find(&page_rows, "49").as_deref(), Some("10"));
assert_eq!(find(&page_rows, "500").as_deref(), Some("500"));
assert_eq!(find(&page_rows, "999").as_deref(), Some("500"));
assert_eq!(find(&page_rows, "1000").as_deref(), Some("1000"));
for bad in ["0", "-1", "1001", "5000", "abc"] {
assert_eq!(find(&page_rows, bad), None, "{bad}");
}
let maps = std::fs::read_to_string(generation.join("nginx/maps").join(MAPS_FILE)).unwrap();
let resolve = |limit: &str, cursor: &str| -> Option<serde_json::Value> {
let size = find(&page_rows, limit)?;
let c = if cursor.is_empty() { "0" } else { cursor };
let grid = format!("list/{size}-{c}.json");
let one = format!("list/1-{c}.json");
let file = if maps.contains(&format!("{size}|{cursor} {grid};")) {
grid
} else if maps.contains(&format!("{cursor} {one};")) {
one
} else if cursor == "120" && maps.contains(&format!("120 {EMPTY_REPOS};")) {
EMPTY_REPOS.into()
} else {
return None;
};
Some(page(&file))
};
let want: Vec<String> = (0..120).map(|n| format!("did:plc:r{n:03}")).collect();
for limit in ["", "1", "7", "10", "49", "50", "100", "500", "1000"] {
let cap = if limit.is_empty() {
500
} else {
limit.parse().unwrap()
};
let mut seen = Vec::new();
let mut cursor = String::new();
loop {
let body = resolve(limit, &cursor).unwrap();
let dids: Vec<String> = body["repos"]
.as_array()
.unwrap()
.iter()
.map(|r| r["did"].as_str().unwrap().into())
.collect();
assert!(!dids.is_empty() && dids.len() <= cap);
seen.extend(dids);
match body["cursor"].as_str() {
Some(next) => cursor = next.to_string(),
None => break,
}
}
assert_eq!(seen, want, "limit={limit}");
}
assert_eq!(
resolve("50", "120").unwrap()["repos"]
.as_array()
.unwrap()
.len(),
0
);
assert!(resolve("50", "121").is_none());
}
#[test]
fn locations_check_required_params_before_repo_and_record_lookup() {
let blocks = locations("/gen", &crate::config::ServeLimits::default());
let rendered: String = blocks.iter().map(ToString::to_string).collect();
for (nsid, param, key) in [
("sync.getRepo", "$arg_did", "$ks_atp_did"),
("sync.getLatestCommit", "$arg_did", "$ks_atp_did"),
("sync.getRepoStatus", "$arg_did", "$ks_atp_did"),
("repo.describeRepo", "$arg_repo", "$ks_atp_repo"),
("repo.getRecord", "$arg_repo", "$ks_atp_repo"),
("repo.getRecord", "$arg_collection", "$ks_atp_repo"),
("repo.getRecord", "$arg_rkey", "$ks_atp_repo"),
("repo.listRecords", "$arg_repo", "$ks_atp_repo"),
] {
let block = blocks
.iter()
.map(ToString::to_string)
.find(|text| text.contains(&format!("location = /xrpc/com.atproto.{nsid} ")))
.unwrap_or_else(|| panic!("missing {nsid} in {rendered}"));
let param_pos = block
.find(&format!(
"if ({param} = \"\") {{\n rewrite ^ /_ks_error/invalid_request last;"
))
.unwrap_or_else(|| panic!("{nsid} missing {param} check:\n{block}"));
let key_pos = block
.find(&format!(
"if ({key} = \"\") {{\n rewrite ^ /_ks_error/atp_repo_not_found last;"
))
.unwrap_or_else(|| panic!("{nsid} missing {key} check:\n{block}"));
assert!(
param_pos < key_pos,
"{nsid}: {param} must precede {key}:\n{block}"
);
}
let get_repo = blocks
.iter()
.map(ToString::to_string)
.find(|text| text.contains("location = /xrpc/com.atproto.sync.getRepo "))
.unwrap();
let since_pos = get_repo
.find("if ($ks_atp_since = \"\") {\n rewrite ^ /_ks_error/invalid_request last;")
.unwrap();
let did_pos = get_repo
.find("if ($ks_atp_did = \"\") {\n rewrite ^ /_ks_error/atp_repo_not_found last;")
.unwrap();
assert!(
since_pos < did_pos,
"getRepo checks since before repo lookup:\n{get_repo}"
);
let regex = pcre2::bytes::Regex::new(SINCE_TID_SHAPE.strip_prefix('~').unwrap()).unwrap();
assert!(regex.is_match(b"3lubrptx57d22").unwrap());
for bad in [
&b"nope"[..],
b"3lubrptx57d2",
b"3lubrptx57d222",
b"0lubrptx57d22",
b"3LUBRPTX57D22",
] {
assert!(
!regex.is_match(bad).unwrap(),
"{:?}",
std::str::from_utf8(bad)
);
}
}