You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

142 lines
5.5 KiB
Rust

//! Integration tests for the LLM logs endpoint (GAP-2).
//!
//! Tests:
//! - GET /api/v1/llm-logs/:job_id
//!
//! The handler first checks that a synthesis with the given job_id exists and
//! belongs to the authenticated user, then returns the associated log entries.
//! A random/unknown job_id therefore returns 404 (not an empty array).
//!
//! Requires a running Postgres instance. Set `TEST_DATABASE_URL` to run.
mod common;
use axum::http::StatusCode;
fn require_test_db() -> bool {
std::env::var("TEST_DATABASE_URL").is_ok()
}
// ═══════════════════════════════════════════════════════════════════════════
// Auth (1 test)
// ═══════════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn get_llm_logs_without_auth_returns_401() {
if !require_test_db() {
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
return;
}
let app = common::TestApp::new().await;
let fake_job_id = uuid::Uuid::new_v4();
let (status, body) = app
.get_with_session(
&format!("/api/v1/llm-logs/{}", fake_job_id),
"invalid-session-token",
)
.await;
assert_eq!(
status,
StatusCode::UNAUTHORIZED,
"GET /llm-logs/:job_id without auth should return 401"
);
assert_eq!(body["error"], "unauthorized");
}
// ═══════════════════════════════════════════════════════════════════════════
// Not found (1 test)
// ═══════════════════════════════════════════════════════════════════════════
/// The handler first verifies the job_id maps to a synthesis owned by the
/// authenticated user. A random UUID that has no matching synthesis in the DB
/// returns 404. This is intentional — it prevents enumeration of job IDs.
#[tokio::test]
async fn get_llm_logs_returns_404_for_unknown_job() {
if !require_test_db() {
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
return;
}
let app = common::TestApp::new().await;
let (_user_id, session) = app
.create_authenticated_user("llm-logs-404@example.com")
.await;
let fake_job_id = uuid::Uuid::new_v4();
let (status, body) = app
.get_with_session(&format!("/api/v1/llm-logs/{}", fake_job_id), &session)
.await;
assert_eq!(
status,
StatusCode::NOT_FOUND,
"GET /llm-logs/:job_id for an unknown job_id should return 404"
);
assert_eq!(body["error"], "not_found");
}
// ═══════════════════════════════════════════════════════════════════════════
// Happy path (1 test)
// ═══════════════════════════════════════════════════════════════════════════
/// Verify that when a synthesis exists for the given job_id, the endpoint
/// returns 200 with a JSON array (the log entries, which may be empty if no
/// LLM calls were recorded for the synthesis created directly via helper).
#[tokio::test]
async fn get_llm_logs_returns_array_for_known_job() {
if !require_test_db() {
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
return;
}
let app = common::TestApp::new().await;
let (user_id, session) = app
.create_authenticated_user("llm-logs-array@example.com")
.await;
// Insert a test synthesis with a known job_id directly into the database.
// The `insert_test_synthesis` helper uses a random job_id internally; we
// need to insert our own to control the job_id used for the log lookup.
let job_id = uuid::Uuid::new_v4();
let sections = serde_json::json!([{
"title": "AI News",
"items": [{"title": "Article 1", "url": "https://example.com/1", "summary": "Summary"}]
}]);
let synthesis_id: (uuid::Uuid,) = sqlx::query_as(
"INSERT INTO syntheses (user_id, week, sections, status, job_id)
VALUES ($1, $2, $3, 'completed', $4)
RETURNING id",
)
.bind(user_id)
.bind("2026-W13")
.bind(&sections)
.bind(job_id)
.fetch_one(&app.pool)
.await
.expect("Failed to insert test synthesis");
let _ = synthesis_id; // verify it was inserted; we only need the job_id for the request
let (status, body) = app
.get_with_session(&format!("/api/v1/llm-logs/{}", job_id), &session)
.await;
assert_eq!(
status,
StatusCode::OK,
"GET /llm-logs/:job_id for a known synthesis should return 200"
);
assert!(
body.as_array().is_some(),
"Response should be a JSON array, got: {}", body
);
// No LLM calls were made for this synthesis (inserted directly), so the
// array is empty — but the important thing is it's a valid array.
assert!(
body.as_array().unwrap().is_empty(),
"Log array should be empty for a synthesis with no recorded LLM calls"
);
}