#![allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "test code — panics are acceptable failures"
)]
mod support;
use axum::{body::Body, http::Request};
use cognee_search::types::SearchType;
use serde_json::json;
use std::sync::Arc;
use tower::ServiceExt;
use support::{StubRetriever, body_json, build_orchestrator, build_p4_state, build_search_db};
async fn build_search_app() -> axum::Router {
let db = build_search_db().await;
let retriever = Arc::new(StubRetriever::text_for(SearchType::GraphCompletion, "ans"));
let orchestrator = build_orchestrator(db, retriever).await;
let state = build_p4_state(Some(orchestrator), None, None).await;
cognee_http_server::build_router(state)
.await
.expect("router")
}
fn assert_keys_have_no_underscore(value: &serde_json::Value, context: &str) {
if let Some(map) = value.as_object() {
for k in map.keys() {
assert!(
!k.contains('_'),
"{context}: response key `{k}` is snake_case; expected camelCase per Decision 10. Full body: {value}"
);
}
}
}
#[tokio::test]
async fn post_search_accepts_snake_case_body() {
let app = build_search_app().await;
let req = Request::builder()
.method("POST")
.uri("/api/v1/search")
.header("content-type", "application/json")
.body(Body::from(
json!({
"search_type": "GRAPH_COMPLETION",
"dataset_ids": [],
"system_prompt": "sys",
"node_name": ["x"],
"top_k": 5,
"only_context": false,
"query": "hi"
})
.to_string(),
))
.unwrap();
let resp = app.oneshot(req).await.expect("resp");
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn post_search_accepts_camelcase_body() {
let app = build_search_app().await;
let req = Request::builder()
.method("POST")
.uri("/api/v1/search")
.header("content-type", "application/json")
.body(Body::from(
json!({
"searchType": "GRAPH_COMPLETION",
"datasetIds": [],
"systemPrompt": "sys",
"nodeName": ["x"],
"topK": 5,
"onlyContext": false,
"query": "hi"
})
.to_string(),
))
.unwrap();
let resp = app.oneshot(req).await.expect("resp");
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn post_search_response_keys_are_camelcase() {
let app = build_search_app().await;
let req = Request::builder()
.method("POST")
.uri("/api/v1/search")
.header("content-type", "application/json")
.body(Body::from(json!({"query": "hi"}).to_string()))
.unwrap();
let resp = app.oneshot(req).await.expect("resp");
assert_eq!(resp.status(), 200);
let body = body_json(resp).await;
let arr = body.as_array().expect("array of SearchResultDTO");
for entry in arr {
assert_keys_have_no_underscore(entry, "post_search response item");
}
}
#[tokio::test]
async fn get_search_history_response_keys_are_camelcase() {
let app = build_search_app().await;
let post = Request::builder()
.method("POST")
.uri("/api/v1/search")
.header("content-type", "application/json")
.body(Body::from(json!({"query": "hi"}).to_string()))
.unwrap();
let _ = app.clone().oneshot(post).await.expect("seed");
let get = Request::builder()
.method("GET")
.uri("/api/v1/search")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(get).await.expect("resp");
assert_eq!(resp.status(), 200);
let body = body_json(resp).await;
let arr = body.as_array().expect("array of SearchHistoryItemDTO");
assert!(!arr.is_empty(), "history should have at least one row");
for entry in arr {
assert_keys_have_no_underscore(entry, "search history item");
}
}
#[tokio::test]
async fn post_recall_accepts_snake_case_body() {
let app = build_search_app().await;
let req = Request::builder()
.method("POST")
.uri("/api/v1/recall")
.header("content-type", "application/json")
.body(Body::from(
json!({
"search_type": "GRAPH_COMPLETION",
"dataset_ids": [],
"top_k": 3,
"only_context": false,
"query": "hi"
})
.to_string(),
))
.unwrap();
let resp = app.oneshot(req).await.expect("resp");
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn post_recall_accepts_camelcase_body() {
let app = build_search_app().await;
let req = Request::builder()
.method("POST")
.uri("/api/v1/recall")
.header("content-type", "application/json")
.body(Body::from(
json!({
"searchType": "GRAPH_COMPLETION",
"datasetIds": [],
"topK": 3,
"onlyContext": false,
"query": "hi"
})
.to_string(),
))
.unwrap();
let resp = app.oneshot(req).await.expect("resp");
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn post_recall_response_keys_match_python_shape() {
let app = build_search_app().await;
let req = Request::builder()
.method("POST")
.uri("/api/v1/recall")
.header("content-type", "application/json")
.body(Body::from(json!({"query": "hi"}).to_string()))
.unwrap();
let resp = app.oneshot(req).await.expect("resp");
assert_eq!(resp.status(), 200);
let body = body_json(resp).await;
let arr = body.as_array().expect("array");
for entry in arr {
assert!(
entry["_source"].is_string(),
"every recall item must have a string `_source`: {entry}"
);
}
}
#[tokio::test]
async fn select_tenant_dto_accepts_both_casings() {
use cognee_http_server::dto::permissions::SelectTenantDTO;
let snake: SelectTenantDTO =
serde_json::from_str(r#"{"tenant_id": null}"#).expect("snake parse");
assert!(snake.tenant_id.is_none());
let camel: SelectTenantDTO =
serde_json::from_str(r#"{"tenantId": null}"#).expect("camel parse");
assert!(camel.tenant_id.is_none());
}