use std::sync::Arc; use axum::body::Body; use bobbin_edge_index::{CoverageWatch, EdgeStore}; use bobbin_record_lru::{CacheCapacity, LruRecordStore}; use bobbin_resolver::RepoIdResolver; use bobbin_runtime::{RuntimeHasher, SystemClock}; use bobbin_search::{DEFAULT_WRITER_HEAP_BYTES, SearchIndex, SearchReader}; use bobbin_slingshot_client::SlingshotClient; use bobbin_xrpc::{AppState, CodeSearch, router}; use http::{Request, StatusCode}; use tower::ServiceExt; use url::Url; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; const NSID: &str = "org.tangled.temp.search.searchCode"; fn app() -> axum::Router { let base = Url::parse("http://127.0.0.1:1").unwrap(); let state = AppState::new( Arc::new(LruRecordStore::new(CacheCapacity::from_bytes(64 * 1024))), SlingshotClient::with_default_http(base.clone()).unwrap(), Arc::new(EdgeStore::new(RuntimeHasher::default())), Arc::new(CoverageWatch::new()), Arc::new(SearchIndex::new(DEFAULT_WRITER_HEAP_BYTES, Arc::new(SystemClock::new())).unwrap()) as Arc, Arc::new(RepoIdResolver::detached(RuntimeHasher::default())), Arc::new(bobbin_xrpc::default_directory()), ) .with_codesearch(Some(Arc::new(CodeSearch::new(&base).unwrap()))); router(state) } fn get(auth: Option<&str>) -> Request { let mut req = Request::builder().uri(format!("/xrpc/{NSID}?q=fn+main")); if let Some(token) = auth { req = req.header("authorization", token); } req.body(Body::empty()).unwrap() } /// Code search is gated on login, matching the appview ui it backs, so an anonymous request /// must never reach zoekt. #[tokio::test] async fn an_anonymous_request_is_rejected() { let resp = app().oneshot(get(None)).await.unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn a_malformed_authorization_header_is_rejected() { for header in ["", "Bearer", "Bearer not-a-jwt", "Basic abc"] { let resp = app().oneshot(get(Some(header))).await.unwrap(); assert_eq!( resp.status(), StatusCode::UNAUTHORIZED, "header {header:?} must not pass" ); } } /// The lexicon declares a query, so the route must not accept a POST. #[tokio::test] async fn the_nsid_is_a_query_not_a_procedure() { let resp = app() .oneshot( Request::builder() .method("POST") .uri(format!("/xrpc/{NSID}?q=fn")) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } /// The zoekt origin sits behind basic auth, so a configured credential must reach it. #[tokio::test] async fn zoekt_gets_the_configured_authorization() { let zoekt = MockServer::start().await; Mock::given(method("POST")) .and(path("/api/search")) .and(header("authorization", "Basic dXNlcjpwYXNz")) .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"Result":null}"#)) .expect(1) .mount(&zoekt) .await; let search = CodeSearch::new(&Url::parse(&zoekt.uri()).unwrap()) .unwrap() .with_authorization("Basic dXNlcjpwYXNz") .unwrap(); search.search("fn main", 0, 10).await.unwrap(); }