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.

277 lines
8.3 KiB
Rust

//! User settings model.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// User settings record from the database.
#[derive(Debug, Clone, Serialize)]
pub struct UserSettings {
pub user_id: Uuid,
pub theme: String,
pub max_age_days: i32,
pub categories: Vec<String>,
pub max_items_per_category: i32,
pub search_agent_behavior: String,
pub updated_at: DateTime<Utc>,
}
/// Response shape for `GET /api/v1/settings`.
#[derive(Debug, Serialize)]
pub struct SettingsResponse {
pub theme: String,
pub max_age_days: i32,
pub categories: Vec<String>,
pub max_items_per_category: i32,
pub search_agent_behavior: String,
}
impl From<UserSettings> for SettingsResponse {
fn from(s: UserSettings) -> Self {
Self {
theme: s.theme,
max_age_days: s.max_age_days,
categories: s.categories,
max_items_per_category: s.max_items_per_category,
search_agent_behavior: s.search_agent_behavior,
}
}
}
/// Request body for `PUT /api/v1/settings`.
#[derive(Debug, Deserialize)]
pub struct UpdateSettingsRequest {
pub theme: String,
pub max_age_days: i32,
pub categories: Vec<String>,
pub max_items_per_category: i32,
pub search_agent_behavior: String,
}
impl UpdateSettingsRequest {
/// Validate the settings update request.
///
/// Returns `Ok(())` if all fields are within acceptable bounds,
/// or `Err(message)` describing the first validation failure.
pub fn validate(&self) -> Result<(), String> {
if self.theme.trim().is_empty() {
return Err("Theme cannot be empty".into());
}
if self.theme.len() > 200 {
return Err("Theme must be at most 200 characters".into());
}
if !(1..=365).contains(&self.max_age_days) {
return Err("max_age_days must be between 1 and 365".into());
}
if self.categories.is_empty() {
return Err("Categories cannot be empty".into());
}
if self.categories.len() > 20 {
return Err("At most 20 categories are allowed".into());
}
for (i, cat) in self.categories.iter().enumerate() {
if cat.trim().is_empty() {
return Err(format!("Category at index {} cannot be empty", i));
}
if cat.len() > 200 {
return Err(format!(
"Category at index {} must be at most 200 characters",
i
));
}
}
if !(1..=50).contains(&self.max_items_per_category) {
return Err("max_items_per_category must be between 1 and 50".into());
}
if self.search_agent_behavior.len() > 2000 {
return Err("search_agent_behavior must be at most 2000 characters".into());
}
Ok(())
}
}
/// Default settings values used when a user has no saved settings.
impl Default for UserSettings {
fn default() -> Self {
Self {
user_id: Uuid::nil(),
theme: "Intelligence Artificielle".to_string(),
max_age_days: 7,
categories: vec![
"Annonces majeures".to_string(),
"Recherche et innovation".to_string(),
"Industrie et entreprises".to_string(),
"Secteur public".to_string(),
"Opinions et analyses".to_string(),
],
max_items_per_category: 4,
search_agent_behavior: String::new(),
updated_at: Utc::now(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_settings() {
let req = UpdateSettingsRequest {
theme: "Intelligence Artificielle".into(),
max_age_days: 7,
categories: vec!["Category 1".into(), "Category 2".into()],
max_items_per_category: 4,
search_agent_behavior: String::new(),
};
assert!(req.validate().is_ok());
}
#[test]
fn test_empty_theme() {
let req = UpdateSettingsRequest {
theme: " ".into(),
max_age_days: 7,
categories: vec!["Cat".into()],
max_items_per_category: 4,
search_agent_behavior: String::new(),
};
let err = req.validate().unwrap_err();
assert!(err.contains("Theme"));
}
#[test]
fn test_theme_too_long() {
let req = UpdateSettingsRequest {
theme: "a".repeat(201),
max_age_days: 7,
categories: vec!["Cat".into()],
max_items_per_category: 4,
search_agent_behavior: String::new(),
};
assert!(req.validate().is_err());
}
#[test]
fn test_max_age_days_below_range() {
let req = UpdateSettingsRequest {
theme: "AI".into(),
max_age_days: 0,
categories: vec!["Cat".into()],
max_items_per_category: 4,
search_agent_behavior: String::new(),
};
let err = req.validate().unwrap_err();
assert!(err.contains("max_age_days"));
}
#[test]
fn test_max_age_days_above_range() {
let req = UpdateSettingsRequest {
theme: "AI".into(),
max_age_days: 366,
categories: vec!["Cat".into()],
max_items_per_category: 4,
search_agent_behavior: String::new(),
};
assert!(req.validate().is_err());
}
#[test]
fn test_empty_categories() {
let req = UpdateSettingsRequest {
theme: "AI".into(),
max_age_days: 7,
categories: vec![],
max_items_per_category: 4,
search_agent_behavior: String::new(),
};
let err = req.validate().unwrap_err();
assert!(err.contains("Categories"));
}
#[test]
fn test_too_many_categories() {
let cats: Vec<String> = (0..21).map(|i| format!("Cat {}", i)).collect();
let req = UpdateSettingsRequest {
theme: "AI".into(),
max_age_days: 7,
categories: cats,
max_items_per_category: 4,
search_agent_behavior: String::new(),
};
let err = req.validate().unwrap_err();
assert!(err.contains("20"));
}
#[test]
fn test_empty_category_item() {
let req = UpdateSettingsRequest {
theme: "AI".into(),
max_age_days: 7,
categories: vec!["Good".into(), " ".into()],
max_items_per_category: 4,
search_agent_behavior: String::new(),
};
let err = req.validate().unwrap_err();
assert!(err.contains("index 1"));
}
#[test]
fn test_max_items_below_range() {
let req = UpdateSettingsRequest {
theme: "AI".into(),
max_age_days: 7,
categories: vec!["Cat".into()],
max_items_per_category: 0,
search_agent_behavior: String::new(),
};
assert!(req.validate().is_err());
}
#[test]
fn test_max_items_above_range() {
let req = UpdateSettingsRequest {
theme: "AI".into(),
max_age_days: 7,
categories: vec!["Cat".into()],
max_items_per_category: 51,
search_agent_behavior: String::new(),
};
assert!(req.validate().is_err());
}
#[test]
fn test_search_agent_behavior_too_long() {
let req = UpdateSettingsRequest {
theme: "AI".into(),
max_age_days: 7,
categories: vec!["Cat".into()],
max_items_per_category: 4,
search_agent_behavior: "a".repeat(2001),
};
assert!(req.validate().is_err());
}
#[test]
fn test_boundary_values_valid() {
let req = UpdateSettingsRequest {
theme: "A".into(),
max_age_days: 1,
categories: vec!["Cat".into()],
max_items_per_category: 1,
search_agent_behavior: String::new(),
};
assert!(req.validate().is_ok());
let req = UpdateSettingsRequest {
theme: "A".into(),
max_age_days: 365,
categories: (0..20).map(|i| format!("Cat {}", i)).collect(),
max_items_per_category: 50,
search_agent_behavior: "a".repeat(2000),
};
assert!(req.validate().is_ok());
}
}