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.
466 lines
14 KiB
Rust
466 lines
14 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("/api/v1/settings").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!({
|
|
"theme": "Test",
|
|
"max_age_days": 7,
|
|
"categories": ["Cat"],
|
|
"max_items_per_category": 4,
|
|
"search_agent_behavior": ""
|
|
});
|
|
|
|
// 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["theme"], "Intelligence Artificielle",
|
|
"Default theme should be 'Intelligence Artificielle'"
|
|
);
|
|
assert_eq!(
|
|
body["max_age_days"], 7,
|
|
"Default max_age_days should be 7"
|
|
);
|
|
assert_eq!(
|
|
body["max_items_per_category"], 4,
|
|
"Default max_items_per_category should be 4"
|
|
);
|
|
|
|
// Check default categories
|
|
let categories = body["categories"].as_array().expect("categories should be an array");
|
|
assert_eq!(categories.len(), 5, "Default should have 5 categories");
|
|
assert_eq!(categories[0], "Annonces majeures");
|
|
}
|
|
|
|
// ── 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!({
|
|
"theme": "Cybersecurite",
|
|
"max_age_days": 14,
|
|
"categories": ["Vulnerabilites", "Patch Tuesday", "Threat Intel"],
|
|
"max_items_per_category": 6,
|
|
"search_agent_behavior": "Focus on CVEs"
|
|
});
|
|
|
|
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["theme"], "Cybersecurite");
|
|
assert_eq!(body["max_age_days"], 14);
|
|
assert_eq!(body["max_items_per_category"], 6);
|
|
assert_eq!(body["search_agent_behavior"], "Focus on CVEs");
|
|
|
|
let categories = body["categories"].as_array().expect("categories array");
|
|
assert_eq!(categories.len(), 3);
|
|
assert_eq!(categories[0], "Vulnerabilites");
|
|
assert_eq!(categories[1], "Patch Tuesday");
|
|
assert_eq!(categories[2], "Threat Intel");
|
|
}
|
|
|
|
#[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!({
|
|
"theme": "Economie",
|
|
"max_age_days": 30,
|
|
"categories": ["Macro", "Finance"],
|
|
"max_items_per_category": 10,
|
|
"search_agent_behavior": "Francophone sources"
|
|
});
|
|
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["theme"], "Economie");
|
|
assert_eq!(body["max_age_days"], 30);
|
|
assert_eq!(body["max_items_per_category"], 10);
|
|
assert_eq!(body["search_agent_behavior"], "Francophone sources");
|
|
|
|
let categories = body["categories"].as_array().expect("categories array");
|
|
assert_eq!(categories.len(), 2);
|
|
assert_eq!(categories[0], "Macro");
|
|
assert_eq!(categories[1], "Finance");
|
|
}
|
|
|
|
// ── Validation errors ────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn put_settings_empty_theme_returns_422() {
|
|
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-val-theme@example.com")
|
|
.await;
|
|
|
|
let update = serde_json::json!({
|
|
"theme": " ",
|
|
"max_age_days": 7,
|
|
"categories": ["Cat"],
|
|
"max_items_per_category": 4,
|
|
"search_agent_behavior": ""
|
|
});
|
|
let (status, body) = app
|
|
.put_with_session("/api/v1/settings", &update, &session)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
status,
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"Empty theme should return 422"
|
|
);
|
|
assert_eq!(body["error"], "validation_error");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn put_settings_too_many_categories_returns_422() {
|
|
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-val-cats@example.com")
|
|
.await;
|
|
|
|
let categories: Vec<String> = (0..21).map(|i| format!("Cat {}", i)).collect();
|
|
let update = serde_json::json!({
|
|
"theme": "AI",
|
|
"max_age_days": 7,
|
|
"categories": categories,
|
|
"max_items_per_category": 4,
|
|
"search_agent_behavior": ""
|
|
});
|
|
let (status, body) = app
|
|
.put_with_session("/api/v1/settings", &update, &session)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
status,
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"More than 20 categories should return 422"
|
|
);
|
|
assert_eq!(body["error"], "validation_error");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn put_settings_empty_categories_returns_422() {
|
|
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-val-empty-cats@example.com")
|
|
.await;
|
|
|
|
let update = serde_json::json!({
|
|
"theme": "AI",
|
|
"max_age_days": 7,
|
|
"categories": [],
|
|
"max_items_per_category": 4,
|
|
"search_agent_behavior": ""
|
|
});
|
|
let (status, body) = app
|
|
.put_with_session("/api/v1/settings", &update, &session)
|
|
.await;
|
|
|
|
assert_eq!(
|
|
status,
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"Empty categories array should return 422"
|
|
);
|
|
assert_eq!(body["error"], "validation_error");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn put_settings_max_age_days_out_of_range_returns_422() {
|
|
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-val-age@example.com")
|
|
.await;
|
|
|
|
// Below range
|
|
let update = serde_json::json!({
|
|
"theme": "AI",
|
|
"max_age_days": 0,
|
|
"categories": ["Cat"],
|
|
"max_items_per_category": 4,
|
|
"search_agent_behavior": ""
|
|
});
|
|
let (status, _) = app
|
|
.put_with_session("/api/v1/settings", &update, &session)
|
|
.await;
|
|
assert_eq!(
|
|
status,
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"max_age_days=0 should return 422"
|
|
);
|
|
|
|
// Above range
|
|
let update2 = serde_json::json!({
|
|
"theme": "AI",
|
|
"max_age_days": 366,
|
|
"categories": ["Cat"],
|
|
"max_items_per_category": 4,
|
|
"search_agent_behavior": ""
|
|
});
|
|
let (status2, _) = app
|
|
.put_with_session("/api/v1/settings", &update2, &session)
|
|
.await;
|
|
assert_eq!(
|
|
status2,
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"max_age_days=366 should return 422"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn put_settings_max_items_out_of_range_returns_422() {
|
|
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-val-items@example.com")
|
|
.await;
|
|
|
|
let update = serde_json::json!({
|
|
"theme": "AI",
|
|
"max_age_days": 7,
|
|
"categories": ["Cat"],
|
|
"max_items_per_category": 51,
|
|
"search_agent_behavior": ""
|
|
});
|
|
let (status, _) = app
|
|
.put_with_session("/api/v1/settings", &update, &session)
|
|
.await;
|
|
assert_eq!(
|
|
status,
|
|
StatusCode::UNPROCESSABLE_ENTITY,
|
|
"max_items_per_category=51 should return 422"
|
|
);
|
|
}
|
|
|
|
// ── 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!({
|
|
"theme": "User A Theme",
|
|
"max_age_days": 3,
|
|
"categories": ["A-Category"],
|
|
"max_items_per_category": 2,
|
|
"search_agent_behavior": "User A behavior"
|
|
});
|
|
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!({
|
|
"theme": "User B Theme",
|
|
"max_age_days": 14,
|
|
"categories": ["B-Category-1", "B-Category-2"],
|
|
"max_items_per_category": 8,
|
|
"search_agent_behavior": "User B behavior"
|
|
});
|
|
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["theme"], "User A Theme");
|
|
assert_eq!(body_a["max_age_days"], 3);
|
|
let cats_a = body_a["categories"].as_array().unwrap();
|
|
assert_eq!(cats_a.len(), 1);
|
|
assert_eq!(cats_a[0], "A-Category");
|
|
|
|
// Verify User B sees only their settings
|
|
let (_, body_b) = app.get_with_session("/api/v1/settings", &session_b).await;
|
|
assert_eq!(body_b["theme"], "User B Theme");
|
|
assert_eq!(body_b["max_age_days"], 14);
|
|
let cats_b = body_b["categories"].as_array().unwrap();
|
|
assert_eq!(cats_b.len(), 2);
|
|
assert_eq!(cats_b[0], "B-Category-1");
|
|
}
|
|
|
|
// ── 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!({
|
|
"theme": "A",
|
|
"max_age_days": 1,
|
|
"categories": ["C"],
|
|
"max_items_per_category": 1,
|
|
"search_agent_behavior": ""
|
|
});
|
|
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 categories_max: Vec<String> = (0..20).map(|i| format!("Cat {}", i)).collect();
|
|
let update_max = serde_json::json!({
|
|
"theme": "a".repeat(200),
|
|
"max_age_days": 365,
|
|
"categories": categories_max,
|
|
"max_items_per_category": 50,
|
|
"search_agent_behavior": "a".repeat(2000)
|
|
});
|
|
let (status2, _) = app
|
|
.put_with_session("/api/v1/settings", &update_max, &session)
|
|
.await;
|
|
assert_eq!(status2, StatusCode::OK, "Maximum boundary values should be accepted");
|
|
}
|