use std::collections::HashMap;
use axum::body::{to_bytes, Body};
use axum::http::StatusCode;
use axum::http::{Method, Request};
use tempfile::TempDir;
use tower::ServiceExt;
use crate::core::{FactStore, TrustySearchClient};
use crate::service::events::{AnalyzerAppState, AnalyzerEvent};
use crate::service::routes::build_router;
use axum::Router;
pub(crate) fn make_state() -> (AnalyzerAppState, TempDir) {
let tmp = TempDir::new().unwrap();
let facts = FactStore::open(&tmp.path().join("facts.redb")).unwrap();
let search = TrustySearchClient::new("http://127.0.0.1:1");
(AnalyzerAppState::new(search, facts), tmp)
}
pub(crate) async fn json_get(app: Router, uri: &str) -> (StatusCode, serde_json::Value) {
let resp = app
.oneshot(
Request::builder()
.method(Method::GET)
.uri(uri)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let status = resp.status();
let bytes = to_bytes(resp.into_body(), 1024 * 1024).await.unwrap();
let value = if bytes.is_empty() {
serde_json::Value::Null
} else {
serde_json::from_slice(&bytes).unwrap()
};
(status, value)
}
#[tokio::test]
async fn health_degraded_when_search_unreachable() {
let (state, _tmp) = make_state();
let app = build_router(state);
let (status, body) = json_get(app, "/health").await;
assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(body["status"], "degraded");
assert_eq!(body["search_reachable"], false);
}
#[tokio::test]
async fn health_response_includes_version() {
let (state, _tmp) = make_state();
let app = build_router(state);
let (_status, body) = json_get(app, "/health").await;
assert!(body["version"].is_string());
assert!(!body["version"].as_str().unwrap().is_empty());
}
#[tokio::test]
async fn sse_subscriber_receives_emitted_event() {
let (state, _tmp) = make_state();
let mut rx = state.events.subscribe();
state.emit(AnalyzerEvent::FactUpserted {
subject: "fn auth".into(),
predicate: "uses".into(),
});
let evt = rx
.recv()
.await
.expect("subscriber should receive emitted event");
match evt {
AnalyzerEvent::FactUpserted { subject, predicate } => {
assert_eq!(subject, "fn auth");
assert_eq!(predicate, "uses");
}
other => panic!("unexpected event: {other:?}"),
}
}
#[tokio::test]
async fn sse_route_returns_event_stream_content_type() {
let (state, _tmp) = make_state();
let app = build_router(state);
let resp = app
.oneshot(
Request::builder()
.method(Method::GET)
.uri("/sse")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let ct = resp
.headers()
.get(axum::http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
assert!(ct.starts_with("text/event-stream"), "got {ct}");
}
#[test]
fn run_diagnostics_blocking_skips_unknown_languages() {
let mut by_file = HashMap::new();
by_file.insert("notes.txt".to_string(), "hello world".to_string());
let diags = crate::service::handlers::analysis::run_diagnostics_blocking(by_file, None, None);
assert!(diags.is_empty());
}
#[test]
fn run_diagnostics_blocking_respects_language_filter() {
let mut by_file = HashMap::new();
by_file.insert("main.rs".to_string(), "fn main() {}".to_string());
let diags = crate::service::handlers::analysis::run_diagnostics_blocking(
by_file,
Some("python".to_string()),
None,
);
assert!(diags.is_empty());
}
#[tokio::test]
async fn diagnostics_endpoint_surfaces_search_failure_as_502() {
let (state, _tmp) = make_state();
let app = build_router(state);
let (status, _body) = json_get(app, "/indexes/demo/diagnostics").await;
assert_eq!(status, StatusCode::BAD_GATEWAY);
}
#[tokio::test]
async fn upsert_then_list_facts_round_trip() {
let (state, _tmp) = make_state();
let app = build_router(state);
let body = serde_json::json!({
"subject": "fn search",
"predicate": "implements",
"object": "trait Searcher",
"index_id": "test"
});
let resp = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/facts")
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let (status, listing) = json_get(app, "/facts").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(listing["count"], 1);
}
#[tokio::test]
async fn scip_ingest_accepts_valid_index_and_stores_overlay() {
use protobuf::{EnumOrUnknown, Message};
use scip::types::{
symbol_information::Kind as ScipKind, Document, Index, Occurrence, SymbolInformation,
};
let (state, _tmp) = make_state();
let overlays = state.scip_overlays.clone();
let app = build_router(state);
let mut sym = SymbolInformation::new();
sym.symbol = "rust . . hello().".into();
sym.kind = EnumOrUnknown::new(ScipKind::Function);
sym.display_name = "hello".into();
let mut occ = Occurrence::new();
occ.symbol = sym.symbol.clone();
occ.symbol_roles = 0x1;
occ.range = vec![1, 0, 5];
let mut doc = Document::new();
doc.relative_path = "src/lib.rs".into();
doc.language = "rust".into();
doc.symbols.push(sym);
doc.occurrences.push(occ);
let mut index = Index::new();
index.documents.push(doc);
let bytes = index.write_to_bytes().expect("encode scip index");
let resp = app
.clone()
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/indexes/myidx/scip")
.header("content-type", "application/octet-stream")
.body(Body::from(bytes))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = to_bytes(resp.into_body(), 1 << 20).await.unwrap();
let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(parsed["index_id"], "myidx");
assert_eq!(parsed["documents"], 1);
assert_eq!(parsed["kg_nodes"], 1);
let overlays = overlays.read().await;
let g = overlays.get("myidx").expect("overlay stored");
assert_eq!(g.node_count(), 1);
assert_eq!(g.nodes[0].name, "hello");
}
#[tokio::test]
async fn scip_ingest_rejects_garbage_bytes() {
let (state, _tmp) = make_state();
let app = build_router(state);
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/indexes/x/scip")
.header("content-type", "application/octet-stream")
.body(Body::from(vec![0xFF, 0xFF, 0xFF, 0xFF]))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn list_indexes_proxies_failure_to_502() {
let (state, _tmp) = make_state();
let app = build_router(state);
let (status, _) = json_get(app, "/indexes").await;
assert_eq!(status, StatusCode::BAD_GATEWAY);
}