//! 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(§ions) .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" ); }