use axum::body::Body;
use axum::http::StatusCode;
use axum::http::{Method, Request};
use tower::ServiceExt;
use crate::service::routes::build_router;
use crate::service::tests::make_state;
#[tokio::test]
async fn review_endpoint_requires_index_id() {
let (state, _tmp) = make_state();
let app = build_router(state);
let diff = "+++ b/src/foo.rs\n@@ -0,0 +1,2 @@\n+/// doc\n+fn f() {}\n";
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/review")
.header("content-type", "text/x-patch")
.body(Body::from(diff))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn review_endpoint_surfaces_search_failure_as_502() {
let (state, _tmp) = make_state();
let app = build_router(state);
let diff = "+++ b/src/foo.rs\n@@ -0,0 +1,2 @@\n+/// doc\n+fn f() {}\n";
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/review?index_id=my-idx")
.header("content-type", "text/x-patch")
.body(Body::from(diff))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_GATEWAY);
}
#[tokio::test]
async fn review_endpoint_rejects_malformed_diff() {
let (state, _tmp) = make_state();
let app = build_router(state);
let diff = "+++ b/x.rs\n@@ totally bogus @@\n+fn x() {}\n";
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/review?index_id=my-idx")
.header("content-type", "text/x-patch")
.body(Body::from(diff))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn deep_endpoint_requires_index_id() {
let (state, _tmp) = make_state();
let state = state.with_api_key(Some("test-key".into()));
let app = build_router(state);
let body = serde_json::json!({ "index_id": "" }).to_string();
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/analyze/deep")
.header("content-type", "application/json")
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn deep_endpoint_requires_api_key() {
let (state, _tmp) = make_state();
let state = state.with_api_key(None);
let app = build_router(state);
let body = serde_json::json!({ "index_id": "my-idx" }).to_string();
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/analyze/deep")
.header("content-type", "application/json")
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn synthesise_review_from_chunks_groups_by_file() {
use crate::core::review::ReviewSource;
use crate::types::CodeChunk;
let chunks = vec![
CodeChunk {
id: "a:1:5".into(),
file: "src/a.rs".into(),
start_line: 1,
end_line: 5,
content: "fn a() {}".into(),
..Default::default()
},
CodeChunk {
id: "a:10:20".into(),
file: "src/a.rs".into(),
start_line: 10,
end_line: 20,
content: "fn aa() {}".into(),
..Default::default()
},
CodeChunk {
id: "b:1:3".into(),
file: "src/b.rs".into(),
start_line: 1,
end_line: 3,
content: "fn b() {}".into(),
..Default::default()
},
];
let report = crate::service::handlers::deep::synthesise_review_from_chunks(&chunks);
assert_eq!(report.files.len(), 2);
let paths: Vec<&str> = report.files.iter().map(|f| f.path.as_str()).collect();
assert!(paths.contains(&"src/a.rs"));
assert!(paths.contains(&"src/b.rs"));
for f in &report.files {
assert_eq!(f.source, ReviewSource::NewFile);
assert!(f.recommendations.is_empty());
}
}
#[test]
fn synthesise_review_from_chunks_empty_corpus_is_grade_a() {
let report = crate::service::handlers::deep::synthesise_review_from_chunks(&[]);
assert!(report.files.is_empty());
assert_eq!(report.overall_grade, crate::types::ComplexityGrade::A);
assert_eq!(report.smell_count, 0);
}
#[test]
fn lookup_frameworks_reads_stored_facts() {
use crate::core::facts::new_fact;
let (state, _tmp) = make_state();
for fw in ["React", "Next.js"] {
let f = new_fact(
"my-idx".to_string(),
"uses_framework".to_string(),
fw.to_string(),
"my-idx".to_string(),
);
state.facts.upsert(f).unwrap();
}
let mut got = crate::service::handlers::deep::lookup_frameworks(&state, "my-idx");
got.sort();
assert_eq!(got, vec!["Next.js".to_string(), "React".to_string()]);
}
#[tokio::test]
async fn webhook_ignores_non_pr_event() {
let (state, _tmp) = make_state();
let app = build_router(state);
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/webhooks/github")
.header("X-GitHub-Event", "push")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::ACCEPTED);
}
#[tokio::test]
async fn webhook_ignores_non_actionable_pr_action() {
let (state, _tmp) = make_state();
let app = build_router(state);
let body = serde_json::json!({ "action": "closed" }).to_string();
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/webhooks/github")
.header("X-GitHub-Event", "pull_request")
.header("content-type", "application/json")
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::ACCEPTED);
}
#[tokio::test]
async fn webhook_rejects_bad_signature() {
let (state, _tmp) = make_state();
let state = state.with_webhook_secret(Some("test-secret".to_string()));
let app = build_router(state);
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/webhooks/github")
.header("X-GitHub-Event", "pull_request")
.header("X-Hub-Signature-256", "sha256=deadbeef")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn webhook_accepts_valid_signature() {
use hmac::{Hmac, Mac};
use sha2::Sha256;
let (state, _tmp) = make_state();
let state = state.with_webhook_secret(Some("test-secret".to_string()));
let app = build_router(state);
let body = serde_json::json!({ "action": "closed" }).to_string();
let mut mac = Hmac::<Sha256>::new_from_slice(b"test-secret").unwrap();
mac.update(body.as_bytes());
let sig = format!("sha256={}", hex::encode(mac.finalize().into_bytes()));
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/webhooks/github")
.header("X-GitHub-Event", "pull_request")
.header("X-Hub-Signature-256", &sig)
.header("content-type", "application/json")
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::ACCEPTED);
}
#[tokio::test]
async fn webhook_rejects_malformed_pr_payload() {
let (state, _tmp) = make_state();
let app = build_router(state);
let body = serde_json::json!({ "action": "opened" }).to_string();
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/webhooks/github")
.header("X-GitHub-Event", "pull_request")
.header("content-type", "application/json")
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}