//! Integration tests for the settings endpoints. //! //! Tests GET and PUT /api/v1/settings, including authentication, //! validation, defaults, and per-user isolation. //! //! 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 requirement --------------------------------------------------------- #[tokio::test] async fn get_settings_without_auth_returns_401() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let (status, body) = app.get_with_session("/api/v1/settings", "invalid-session-token").await; assert_eq!( status, StatusCode::UNAUTHORIZED, "GET /settings without auth should return 401" ); assert_eq!(body["error"], "unauthorized"); } #[tokio::test] async fn put_settings_without_auth_returns_401() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; let body = serde_json::json!({ "max_articles_per_source": 3, "max_links_per_source": 8, "use_brave_search": false, "article_history_days": 90, "batch_size": 5, "source_extraction_window": 3, "search_agent_behavior": "", "ai_provider": "", "ai_model": "", "ai_model_websearch": "", "rate_limit_max_requests": null, "rate_limit_time_window_seconds": null }); // PUT without a session cookie let req = axum::http::Request::builder() .method(axum::http::Method::PUT) .uri("/api/v1/settings") .header("Content-Type", "application/json") .header("X-Requested-With", "XMLHttpRequest") .body(axum::body::Body::from(serde_json::to_vec(&body).unwrap())) .unwrap(); let response = app.raw_request(req).await; assert_eq!( response.status(), StatusCode::UNAUTHORIZED, "PUT /settings without auth should return 401" ); } // -- Default settings --------------------------------------------------------- #[tokio::test] async fn get_settings_returns_defaults_on_first_access() { 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("settings-default@example.com") .await; let (status, body) = app.get_with_session("/api/v1/settings", &session).await; assert_eq!(status, StatusCode::OK, "GET /settings should return 200"); assert_eq!( body["max_articles_per_source"], 3, "Default max_articles_per_source should be 3" ); } // -- Update settings ---------------------------------------------------------- #[tokio::test] async fn put_settings_with_valid_data_returns_200() { 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("settings-update@example.com") .await; let update = serde_json::json!({ "max_articles_per_source": 3, "max_links_per_source": 8, "use_brave_search": false, "article_history_days": 90, "batch_size": 5, "source_extraction_window": 3, "search_agent_behavior": "Focus on CVEs", "ai_provider": "", "ai_model": "", "ai_model_websearch": "", "rate_limit_max_requests": null, "rate_limit_time_window_seconds": null }); let (status, body) = app .put_with_session("/api/v1/settings", &update, &session) .await; assert_eq!( status, StatusCode::OK, "PUT /settings with valid data should return 200" ); assert_eq!(body["search_agent_behavior"], "Focus on CVEs"); } #[tokio::test] async fn put_then_get_returns_updated_data() { 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("settings-roundtrip@example.com") .await; // First, trigger default creation let (status, _) = app.get_with_session("/api/v1/settings", &session).await; assert_eq!(status, StatusCode::OK); // Update let update = serde_json::json!({ "max_articles_per_source": 3, "max_links_per_source": 8, "use_brave_search": false, "article_history_days": 90, "batch_size": 5, "source_extraction_window": 3, "search_agent_behavior": "Francophone sources", "ai_provider": "", "ai_model": "", "ai_model_websearch": "", "rate_limit_max_requests": null, "rate_limit_time_window_seconds": null }); let (put_status, _) = app .put_with_session("/api/v1/settings", &update, &session) .await; assert_eq!(put_status, StatusCode::OK); // GET again -- should reflect the update let (get_status, body) = app.get_with_session("/api/v1/settings", &session).await; assert_eq!(get_status, StatusCode::OK); assert_eq!(body["search_agent_behavior"], "Francophone sources"); } // -- Per-user isolation ------------------------------------------------------- #[tokio::test] async fn settings_are_per_user_isolated() { if !require_test_db() { eprintln!("SKIPPED: TEST_DATABASE_URL not set"); return; } let app = common::TestApp::new().await; // Create two users with sessions let (_user_a_id, session_a) = app .create_authenticated_user("user-a-settings@example.com") .await; let (_user_b_id, session_b) = app .create_authenticated_user("user-b-settings@example.com") .await; // User A updates their settings let update_a = serde_json::json!({ "max_articles_per_source": 3, "max_links_per_source": 8, "use_brave_search": false, "article_history_days": 90, "batch_size": 5, "source_extraction_window": 3, "search_agent_behavior": "User A behavior", "ai_provider": "", "ai_model": "", "ai_model_websearch": "", "rate_limit_max_requests": null, "rate_limit_time_window_seconds": null }); let (status_a, _) = app .put_with_session("/api/v1/settings", &update_a, &session_a) .await; assert_eq!(status_a, StatusCode::OK); // User B updates their settings differently let update_b = serde_json::json!({ "max_articles_per_source": 3, "max_links_per_source": 8, "use_brave_search": false, "article_history_days": 90, "batch_size": 5, "source_extraction_window": 3, "search_agent_behavior": "User B behavior", "ai_provider": "", "ai_model": "", "ai_model_websearch": "", "rate_limit_max_requests": null, "rate_limit_time_window_seconds": null }); let (status_b, _) = app .put_with_session("/api/v1/settings", &update_b, &session_b) .await; assert_eq!(status_b, StatusCode::OK); // Verify User A sees only their settings let (_, body_a) = app.get_with_session("/api/v1/settings", &session_a).await; assert_eq!(body_a["search_agent_behavior"], "User A behavior"); // Verify User B sees only their settings let (_, body_b) = app.get_with_session("/api/v1/settings", &session_b).await; assert_eq!(body_b["search_agent_behavior"], "User B behavior"); } // -- Boundary values ---------------------------------------------------------- #[tokio::test] async fn put_settings_boundary_values_succeed() { 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("settings-boundary@example.com") .await; // Minimum valid values let update_min = serde_json::json!({ "max_articles_per_source": 1, "max_links_per_source": 1, "use_brave_search": false, "article_history_days": 0, "batch_size": 1, "source_extraction_window": 1, "search_agent_behavior": "", "ai_provider": "", "ai_model": "", "ai_model_websearch": "", "rate_limit_max_requests": null, "rate_limit_time_window_seconds": null }); let (status, _) = app .put_with_session("/api/v1/settings", &update_min, &session) .await; assert_eq!(status, StatusCode::OK, "Minimum boundary values should be accepted"); // Maximum valid values let update_max = serde_json::json!({ "max_articles_per_source": 10, "max_links_per_source": 30, "use_brave_search": true, "article_history_days": 365, "batch_size": 20, "source_extraction_window": 10, "search_agent_behavior": "a".repeat(2000), "ai_provider": "", "ai_model": "", "ai_model_websearch": "", "rate_limit_max_requests": null, "rate_limit_time_window_seconds": null }); let (status2, _) = app .put_with_session("/api/v1/settings", &update_max, &session) .await; assert_eq!(status2, StatusCode::OK, "Maximum boundary values should be accepted"); }