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