diff --git a/crates/api/src/admin_search.rs b/crates/api/src/admin_search.rs index a232db3..2245f9c 100644 --- a/crates/api/src/admin_search.rs +++ b/crates/api/src/admin_search.rs @@ -79,6 +79,8 @@ pub(crate) async fn search_objects( })); } + // Search uses a tighter default/cap (20, max 50) than the shared `Pagination` + // (default 50, max 200): result pages are slower to scan than a raw object list. let offset = params.offset.unwrap_or(0).max(0) as usize; let limit = params.limit.unwrap_or(20).clamp(1, 50) as usize; diff --git a/crates/api/tests/admin_search.rs b/crates/api/tests/admin_search.rs index c2541e7..e552599 100644 --- a/crates/api/tests/admin_search.rs +++ b/crates/api/tests/admin_search.rs @@ -189,6 +189,98 @@ async fn search_returns_results_and_validates_params(pool: PgPool) { assert_eq!(bad.status(), StatusCode::BAD_REQUEST); } +#[sqlx::test(migrations = "../db/migrations")] +async fn search_visibility_filter_narrows_results(pool: PgPool) { + migrate_sessions(&db::Db::from_pool(pool.clone())) + .await + .unwrap(); + + seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await; + + let (url, key) = meili(); + let search = SearchClient::connect(&url, &key, &unique_index()).unwrap(); + + search.ensure_index().await.unwrap(); + + let app = build_app(state(pool.clone(), Some(search))); + let cookie = login(&app, "ed@example.com", "pw-editor-123").await; + + let create_internal = app + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/admin/objects") + .header(header::COOKIE, &cookie) + .header(header::CONTENT_TYPE, "application/json") + .body(Body::from( + r#"{"object_number":"R-2","object_name":"astrolabe-internal","number_of_objects":1,"visibility":"internal"}"#, + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(create_internal.status(), StatusCode::CREATED); + + let create_draft = app + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/admin/objects") + .header(header::COOKIE, &cookie) + .header(header::CONTENT_TYPE, "application/json") + .body(Body::from( + r#"{"object_number":"R-3","object_name":"astrolabe-draft","number_of_objects":1,"visibility":"draft"}"#, + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(create_draft.status(), StatusCode::CREATED); + + let all = app + .clone() + .oneshot( + Request::builder() + .uri("/api/admin/search?q=astrolabe") + .header(header::COOKIE, &cookie) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(all.status(), StatusCode::OK); + + let all_body: serde_json::Value = + serde_json::from_slice(&all.into_body().collect().await.unwrap().to_bytes()).unwrap(); + + assert_eq!(all_body["estimated_total"], 2); + + let filtered = app + .clone() + .oneshot( + Request::builder() + .uri("/api/admin/search?q=astrolabe&visibility=internal") + .header(header::COOKIE, &cookie) + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(filtered.status(), StatusCode::OK); + + let filtered_body: serde_json::Value = + serde_json::from_slice(&filtered.into_body().collect().await.unwrap().to_bytes()).unwrap(); + + assert_eq!(filtered_body["estimated_total"], 1); + assert_eq!(filtered_body["hits"][0]["visibility"], "internal"); +} + #[sqlx::test(migrations = "../db/migrations")] async fn search_unavailable_when_not_configured(pool: PgPool) { migrate_sessions(&db::Db::from_pool(pool.clone()))