fabro/lib/crates/fabro-api/tests/pagination.rs
Bryan Helmkamp 075812478b Extract fabro-interview crate from fabro-workflows
The interviewer module (trait + 7 implementations for human-in-the-loop
interactions) had zero dependencies on fabro-workflows internals, making
it a clean extraction. Consumers (fabro-api, fabro-slack) now depend on
fabro-interview directly instead of reaching through fabro-workflows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:53:36 -04:00

144 lines
4.4 KiB
Rust

//! Tests that paginated list endpoints return `{ data, meta: { has_more } }`.
use std::sync::Arc;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use fabro_api::jwt_auth::AuthMode;
use fabro_api::server::{build_router, create_app_state};
use fabro_interview::Interviewer;
use fabro_workflows::handler::exit::ExitHandler;
use fabro_workflows::handler::start::StartHandler;
use fabro_workflows::handler::HandlerRegistry;
use tower::ServiceExt;
fn test_registry(_interviewer: Arc<dyn Interviewer>) -> HandlerRegistry {
let mut registry = HandlerRegistry::new(Box::new(StartHandler));
registry.register("start", Box::new(StartHandler));
registry.register("exit", Box::new(ExitHandler));
registry
}
async fn test_db() -> sqlx::SqlitePool {
let pool = fabro_db::connect_memory().await.unwrap();
fabro_db::initialize_db(&pool).await.unwrap();
pool
}
async fn get_json(app: axum::Router, uri: &str) -> serde_json::Value {
let req = Request::builder()
.method("GET")
.uri(uri)
.header("x-fabro-demo", "1")
.body(Body::empty())
.unwrap();
let response = app.clone().oneshot(req).await.unwrap();
assert_eq!(response.status(), StatusCode::OK, "GET {uri} failed");
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
serde_json::from_slice(&body).unwrap()
}
/// Assert that a value has the paginated shape: `{ data: [...], meta: { has_more: bool } }`
fn assert_paginated_shape(json: &serde_json::Value, context: &str) {
assert!(json.get("data").is_some(), "{context}: missing 'data' key");
assert!(json["data"].is_array(), "{context}: 'data' is not an array");
assert!(json.get("meta").is_some(), "{context}: missing 'meta' key");
assert!(
json["meta"].get("has_more").is_some(),
"{context}: missing 'meta.has_more'"
);
assert!(
json["meta"]["has_more"].is_boolean(),
"{context}: 'meta.has_more' is not boolean"
);
}
struct PaginatedEndpoint {
path: &'static str,
name: &'static str,
}
const ENDPOINTS: &[PaginatedEndpoint] = &[
PaginatedEndpoint {
path: "/workflows",
name: "listWorkflows",
},
PaginatedEndpoint {
path: "/workflows/implement/runs",
name: "listWorkflowRuns",
},
PaginatedEndpoint {
path: "/retros",
name: "listRetros",
},
PaginatedEndpoint {
path: "/sessions",
name: "listSessions",
},
PaginatedEndpoint {
path: "/insights/queries",
name: "listSavedQueries",
},
PaginatedEndpoint {
path: "/insights/history",
name: "listQueryHistory",
},
PaginatedEndpoint {
path: "/models",
name: "listModels",
},
PaginatedEndpoint {
path: "/runs/run-1/stages/detect-drift/turns",
name: "listStageTurns",
},
PaginatedEndpoint {
path: "/runs/run-1/questions",
name: "listRunQuestions",
},
PaginatedEndpoint {
path: "/runs/run-1/stages",
name: "listRunStages",
},
PaginatedEndpoint {
path: "/runs/run-1/verification",
name: "retrieveRunVerification",
},
];
#[tokio::test]
async fn paginated_endpoints_return_correct_shape() {
let state = create_app_state(test_db().await, test_registry);
let app = build_router(state, AuthMode::Disabled);
for ep in ENDPOINTS {
// Default request: paginated shape, has_more = false (fixtures fit in default page)
let json = get_json(app.clone(), ep.path).await;
assert_paginated_shape(&json, ep.name);
assert_eq!(
json["meta"]["has_more"], false,
"{}: default request should have has_more=false",
ep.name
);
// limit=1: at most 1 item, has_more = true (all fixtures have >1 item)
let uri = if ep.path.contains('?') {
format!("{}&page[limit]=1", ep.path)
} else {
format!("{}?page[limit]=1", ep.path)
};
let json = get_json(app.clone(), &uri).await;
assert_paginated_shape(&json, &format!("{} limit=1", ep.name));
assert!(
json["data"].as_array().unwrap().len() <= 1,
"{}: limit=1 returned more than 1 item",
ep.name
);
assert_eq!(
json["meta"]["has_more"], true,
"{}: limit=1 should have has_more=true",
ep.name
);
}
}