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.

304 lines
9.4 KiB
Rust

//! 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");
}