follow the cursor ticket search hands out
Signed-off-by: dawn <90008@klbr.net>
10 unmodified lines11121314151617181920212223242526272829232425262728293031323334353031323334353637383940414243444546248 unmodified linesExpand all2952962972982922993003013026 unmodified lines30931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835910 unmodified lines serde_json::from_slice(body).unwrap()}
fn knot() -> (u16, Nginx, tempfile::TempDir) { knot_with(1)}
fn knot_with(tickets: usize) -> (u16, Nginx, tempfile::TempDir) { let nginx_binary = PathBuf::from( std::env::var_os("NGINX_BINARY") .expect("NGINX_BINARY must name the real nginx binary for this ignored test"), ); let temp = tempfile::tempdir().unwrap(); let (config, manifest, generation) = test_fixture(temp.path()); let fixture = crate::atproto::tests::fixture("sha1"); let mut settled = crate::atproto::tests::settle_fixture(&fixture); settled .rows .social settled.rows.social.push(SocialRecord { collection: "org.tangled.track.ticket".into(), rkey: "t1".into(), uri: format!("at://{REPO}/org.tangled.track.ticket/t1"), cid: "bafyreiticket".into(), value: serde_json::json!({ "title": { "text": "hi" }, "state": "open", "createdAt": "2024-01-01T00:00:00Z", }), author: Some(ALICE.into()), editor: Some(ALICE.into()), }); .extend((1..=tickets).map(|n| SocialRecord { collection: "org.tangled.track.ticket".into(), rkey: format!("t{n}"), uri: format!("at://{REPO}/org.tangled.track.ticket/t{n}"), cid: format!("bafyreiticket{n}"), value: serde_json::json!({ "title": { "text": "hi" }, "state": "open", "createdAt": format!("2024-01-01T00:00:{n:02}Z"), }), author: Some(ALICE.into()), editor: Some(ALICE.into()), })); let avatars_dir = temp.path().join("avatars"); std::fs::create_dir_all(&avatars_dir).unwrap(); std::fs::write(avatars_dir.join(format!("{AVATAR}.png")), PNG).unwrap(); let social = Social {248 unmodified linesExpand all ); } for query in [ "&author=did%3Aplc%3Aalice", "&cursor=10", "&nsid=org.tangled.feed.comment", "&label=at%3A%2F%2Fdid%3Aplc%3Ax%2Forg.tangled.label.definition%2Fbug", "&label=&label=bug", "&since=2024-01-01T00%3A00%3A00Z",6 unmodified lines "{query}" ); }}
#[test]#[ignore = "requires nginx; set NGINX_BINARY"]fn search_follows_the_cursor_it_hands_out() { let (port, _nginx, _temp) = knot_with(25); let search = |query: &str| { let target = format!("/xrpc/org.tangled.search.query?repoDid={REPO}&kind=issue&limit=10{query}"); let (status, _, body) = http(port, "GET", &target, &[], b""); (status, json(&body)) }; let mut seen = Vec::new(); let mut cursor = String::new(); loop { let query = match cursor.as_str() { "" => String::new(), cursor => format!("&cursor={cursor}"), }; let (status, page) = search(&query); assert_eq!(status, 200, "{query}"); seen.extend( page["hits"] .as_array() .unwrap() .iter() .map(|hit| hit["uri"].as_str().unwrap().to_string()), ); match page["cursor"].as_str() { Some(next) => cursor = next.to_string(), None => break, } } let newest_first: Vec<String> = (1..=25) .rev() .map(|n| format!("at://{REPO}/org.tangled.track.ticket/t{n}")) .collect(); assert_eq!(seen, newest_first); assert_eq!(search("&offset=10").1, search("&cursor=10").1); for query in ["&offset=10&cursor=0", "&cursor=1&offset=0", "&cursor=nope"] { let (status, page) = search(query); assert_eq!( (status, page["error"].as_str()), (400, Some("InvalidRequest")), "{query}" ); }}30 unmodified lines3132333435363537383739404142190 unmodified linesExpand all2332342352362362372372382392402412428 unmodified lines251252253254255256257258259260261262246 unmodified linesExpand all30 unmodified linespub(super) const SEARCH_NSID: &str = "org.tangled.search.query";
const NO_AUTHOR: Refused = Refused::Unless("author", "");const TICKET_LIST: &[Refused] = &[NO_AUTHOR, NO_CURSOR, NO_OFFSET, NEWEST_FIRST];/// the lexicon makes offset and cursor exclusive, the source below joins both into one key.const OFFSET_AND_CURSOR: &str = "(^|&)offset=[^&].*&cursor=[^&]|(^|&)cursor=[^&].*&offset=[^&]";const SEARCH_REFUSED: [Refused; 7] = [const SEARCH_REFUSED: [Refused; 6] = [ NO_AUTHOR, NO_CURSOR, Refused::Unless("nsid", r"org\.tangled\.track\.ticket|"), Refused::Unless("label", ""), Refused::Unless("since", ""), Refused::Unless("until", ""),190 unmodified linesExpand all vec![ map_with_default("", "$arg_q|$arg_state", QUERY_STATE, queries), map_with_default("", "$arg_limit", PAGE, pages), map_with_default( "$arg_offset", "$arg_offset", "$arg_offset$arg_cursor", "$arg_offset$arg_cursor", OFFSET, vec![(String::new(), "0".into())], ), ]8 unmodified lines reroute( Block::exact(format!("/xrpc/{SEARCH_NSID}")) .directive("default_type", ["application/json"]) .blocks(SEARCH_REFUSED.map(Refused::block)) .block(reroute( Block::when("$args", "~*", OFFSET_AND_CURSOR), "/_ks_error/invalid_request", )) .block( Block::when(QUERY_STATE, "=", "").directive("return", ["400".to_string(), refused]), ) .block(empty(PAGE, "invalid_request"))246 unmodified linesExpand all