@ -13,7 +13,7 @@ fn require_test_db() -> bool {
std ::env ::var ( "TEST_DATABASE_URL" ) . is_ok ( )
}
// ── Auth requirement ─────────────────────────────────────────────────────
// -- Auth requirement ---------------------------------------------------------
#[ tokio::test ]
async fn get_settings_without_auth_returns_401 ( ) {
@ -42,17 +42,11 @@ async fn put_settings_without_auth_returns_401() {
let app = common ::TestApp ::new ( ) . await ;
let body = serde_json ::json ! ( {
"theme" : "Test" ,
"max_age_days" : 7 ,
"categories" : [ "Cat" ] ,
"max_items_per_category" : 4 ,
"max_articles_per_source" : 3 ,
"max_links_per_source" : 8 ,
"use_brave_search" : false ,
"article_history_days" : 90 ,
"batch_size" : 5 ,
"summary_length" : 3 ,
"source_extraction_window" : 3 ,
"search_agent_behavior" : "" ,
"ai_provider" : "" ,
@ -79,7 +73,7 @@ async fn put_settings_without_auth_returns_401() {
) ;
}
// ── Default settings ─────────────────────────────────────────────────────
// -- Default settings ---------------------------------------------------------
#[ tokio::test ]
async fn get_settings_returns_defaults_on_first_access ( ) {
@ -97,25 +91,12 @@ async fn get_settings_returns_defaults_on_first_access() {
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"
body [ "max_articles_per_source" ] , 3 ,
"Default max_articles_per_source should be 3"
) ;
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 ──────────────────────────────────────────────────────
// -- Update settings ----------------------------------------------------------
#[ tokio::test ]
async fn put_settings_with_valid_data_returns_200 ( ) {
@ -130,17 +111,11 @@ async fn put_settings_with_valid_data_returns_200() {
. await ;
let update = serde_json ::json ! ( {
"theme" : "Cybersecurite" ,
"max_age_days" : 14 ,
"categories" : [ "Vulnerabilites" , "Patch Tuesday" , "Threat Intel" ] ,
"max_items_per_category" : 6 ,
"max_articles_per_source" : 3 ,
"max_links_per_source" : 8 ,
"use_brave_search" : false ,
"article_history_days" : 90 ,
"batch_size" : 5 ,
"summary_length" : 3 ,
"source_extraction_window" : 3 ,
"search_agent_behavior" : "Focus on CVEs" ,
"ai_provider" : "" ,
@ -159,16 +134,7 @@ async fn put_settings_with_valid_data_returns_200() {
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 ]
@ -189,17 +155,11 @@ async fn put_then_get_returns_updated_data() {
// Update
let update = serde_json ::json ! ( {
"theme" : "Economie" ,
"max_age_days" : 30 ,
"categories" : [ "Macro" , "Finance" ] ,
"max_items_per_category" : 10 ,
"max_articles_per_source" : 3 ,
"max_links_per_source" : 8 ,
"use_brave_search" : false ,
"article_history_days" : 90 ,
"batch_size" : 5 ,
"summary_length" : 3 ,
"source_extraction_window" : 3 ,
"search_agent_behavior" : "Francophone sources" ,
"ai_provider" : "" ,
@ -213,271 +173,13 @@ async fn put_then_get_returns_updated_data() {
. await ;
assert_eq! ( put_status , StatusCode ::OK ) ;
// GET again — should reflect the update
// 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 ,
"max_articles_per_source" : 3 ,
"max_links_per_source" : 8 ,
"use_brave_search" : false ,
"article_history_days" : 90 ,
"batch_size" : 5 ,
"summary_length" : 3 ,
"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
} ) ;
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 ,
"max_articles_per_source" : 3 ,
"max_links_per_source" : 8 ,
"use_brave_search" : false ,
"article_history_days" : 90 ,
"batch_size" : 5 ,
"summary_length" : 3 ,
"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
} ) ;
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 ,
"max_articles_per_source" : 3 ,
"max_links_per_source" : 8 ,
"use_brave_search" : false ,
"article_history_days" : 90 ,
"batch_size" : 5 ,
"summary_length" : 3 ,
"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
} ) ;
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 ,
"max_articles_per_source" : 3 ,
"max_links_per_source" : 8 ,
"use_brave_search" : false ,
"article_history_days" : 90 ,
"batch_size" : 5 ,
"summary_length" : 3 ,
"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
} ) ;
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 ,
"max_articles_per_source" : 3 ,
"max_links_per_source" : 8 ,
"use_brave_search" : false ,
"article_history_days" : 90 ,
"batch_size" : 5 ,
"summary_length" : 3 ,
"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
} ) ;
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 ,
"max_articles_per_source" : 3 ,
"max_links_per_source" : 8 ,
"use_brave_search" : false ,
"article_history_days" : 90 ,
"batch_size" : 5 ,
"summary_length" : 3 ,
"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
} ) ;
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 ───────────────────────────────────────────────────
// -- Per-user isolation -------------------------------------------------------
#[ tokio::test ]
async fn settings_are_per_user_isolated ( ) {
@ -498,17 +200,11 @@ async fn settings_are_per_user_isolated() {
// 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 ,
"max_articles_per_source" : 3 ,
"max_links_per_source" : 8 ,
"use_brave_search" : false ,
"article_history_days" : 90 ,
"batch_size" : 5 ,
"summary_length" : 3 ,
"source_extraction_window" : 3 ,
"search_agent_behavior" : "User A behavior" ,
"ai_provider" : "" ,
@ -524,17 +220,11 @@ async fn settings_are_per_user_isolated() {
// 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 ,
"max_articles_per_source" : 3 ,
"max_links_per_source" : 8 ,
"use_brave_search" : false ,
"article_history_days" : 90 ,
"batch_size" : 5 ,
"summary_length" : 3 ,
"source_extraction_window" : 3 ,
"search_agent_behavior" : "User B behavior" ,
"ai_provider" : "" ,
@ -550,22 +240,14 @@ async fn settings_are_per_user_isolated() {
// 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" ) ;
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 [ "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" ) ;
assert_eq! ( body_b [ "search_agent_behavior" ] , "User B behavior" ) ;
}
// ── Boundary values ─────────────────────────────────────────────────────
// -- Boundary values ----------------------------------------------------------
#[ tokio::test ]
async fn put_settings_boundary_values_succeed ( ) {
@ -581,17 +263,11 @@ async fn put_settings_boundary_values_succeed() {
// Minimum valid values
let update_min = serde_json ::json ! ( {
"theme" : "A" ,
"max_age_days" : 1 ,
"categories" : [ "C" ] ,
"max_items_per_category" : 1 ,
"max_articles_per_source" : 3 ,
"max_links_per_source" : 8 ,
"max_articles_per_source" : 1 ,
"max_links_per_source" : 1 ,
"use_brave_search" : false ,
"article_history_days" : 90 ,
"batch_size" : 5 ,
"summary_length" : 1 ,
"article_history_days" : 0 ,
"batch_size" : 1 ,
"source_extraction_window" : 1 ,
"search_agent_behavior" : "" ,
"ai_provider" : "" ,
@ -606,19 +282,12 @@ async fn put_settings_boundary_values_succeed() {
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 ,
"max_articles_per_source" : 3 ,
"max_links_per_source" : 8 ,
"use_brave_search" : false ,
"article_history_days" : 90 ,
"batch_size" : 5 ,
"summary_length" : 3 ,
"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" : "" ,