|
|
|
|
@ -13,6 +13,11 @@ pub struct UserSettings {
|
|
|
|
|
pub categories: Vec<String>,
|
|
|
|
|
pub max_items_per_category: i32,
|
|
|
|
|
pub search_agent_behavior: String,
|
|
|
|
|
pub ai_provider: String,
|
|
|
|
|
pub ai_model: String,
|
|
|
|
|
pub ai_model_writing: String,
|
|
|
|
|
pub rate_limit_max_requests: Option<i32>,
|
|
|
|
|
pub rate_limit_time_window_seconds: Option<i32>,
|
|
|
|
|
pub updated_at: DateTime<Utc>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -24,6 +29,11 @@ pub struct SettingsResponse {
|
|
|
|
|
pub categories: Vec<String>,
|
|
|
|
|
pub max_items_per_category: i32,
|
|
|
|
|
pub search_agent_behavior: String,
|
|
|
|
|
pub ai_provider: String,
|
|
|
|
|
pub ai_model: String,
|
|
|
|
|
pub ai_model_writing: String,
|
|
|
|
|
pub rate_limit_max_requests: Option<i32>,
|
|
|
|
|
pub rate_limit_time_window_seconds: Option<i32>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<UserSettings> for SettingsResponse {
|
|
|
|
|
@ -34,6 +44,11 @@ impl From<UserSettings> for SettingsResponse {
|
|
|
|
|
categories: s.categories,
|
|
|
|
|
max_items_per_category: s.max_items_per_category,
|
|
|
|
|
search_agent_behavior: s.search_agent_behavior,
|
|
|
|
|
ai_provider: s.ai_provider,
|
|
|
|
|
ai_model: s.ai_model,
|
|
|
|
|
ai_model_writing: s.ai_model_writing,
|
|
|
|
|
rate_limit_max_requests: s.rate_limit_max_requests,
|
|
|
|
|
rate_limit_time_window_seconds: s.rate_limit_time_window_seconds,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -46,6 +61,11 @@ pub struct UpdateSettingsRequest {
|
|
|
|
|
pub categories: Vec<String>,
|
|
|
|
|
pub max_items_per_category: i32,
|
|
|
|
|
pub search_agent_behavior: String,
|
|
|
|
|
pub ai_provider: String,
|
|
|
|
|
pub ai_model: String,
|
|
|
|
|
pub ai_model_writing: String,
|
|
|
|
|
pub rate_limit_max_requests: Option<i32>,
|
|
|
|
|
pub rate_limit_time_window_seconds: Option<i32>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl UpdateSettingsRequest {
|
|
|
|
|
@ -86,6 +106,25 @@ impl UpdateSettingsRequest {
|
|
|
|
|
if self.search_agent_behavior.len() > 2000 {
|
|
|
|
|
return Err("search_agent_behavior must be at most 2000 characters".into());
|
|
|
|
|
}
|
|
|
|
|
if self.ai_provider.len() > 100 {
|
|
|
|
|
return Err("ai_provider must be at most 100 characters".into());
|
|
|
|
|
}
|
|
|
|
|
if self.ai_model.len() > 100 {
|
|
|
|
|
return Err("ai_model must be at most 100 characters".into());
|
|
|
|
|
}
|
|
|
|
|
if self.ai_model_writing.len() > 100 {
|
|
|
|
|
return Err("ai_model_writing must be at most 100 characters".into());
|
|
|
|
|
}
|
|
|
|
|
if let Some(max_req) = self.rate_limit_max_requests {
|
|
|
|
|
if max_req < 1 {
|
|
|
|
|
return Err("rate_limit_max_requests must be >= 1".into());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(window) = self.rate_limit_time_window_seconds {
|
|
|
|
|
if window < 1 {
|
|
|
|
|
return Err("rate_limit_time_window_seconds must be >= 1".into());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -106,6 +145,11 @@ impl Default for UserSettings {
|
|
|
|
|
],
|
|
|
|
|
max_items_per_category: 4,
|
|
|
|
|
search_agent_behavior: String::new(),
|
|
|
|
|
ai_provider: String::new(),
|
|
|
|
|
ai_model: String::new(),
|
|
|
|
|
ai_model_writing: String::new(),
|
|
|
|
|
rate_limit_max_requests: None,
|
|
|
|
|
rate_limit_time_window_seconds: None,
|
|
|
|
|
updated_at: Utc::now(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -115,15 +159,25 @@ impl Default for UserSettings {
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_valid_settings() {
|
|
|
|
|
let req = UpdateSettingsRequest {
|
|
|
|
|
/// Helper to create a valid request with all new fields populated.
|
|
|
|
|
fn valid_request() -> UpdateSettingsRequest {
|
|
|
|
|
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(),
|
|
|
|
|
};
|
|
|
|
|
ai_provider: String::new(),
|
|
|
|
|
ai_model: String::new(),
|
|
|
|
|
ai_model_writing: String::new(),
|
|
|
|
|
rate_limit_max_requests: None,
|
|
|
|
|
rate_limit_time_window_seconds: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_valid_settings() {
|
|
|
|
|
let req = valid_request();
|
|
|
|
|
assert!(req.validate().is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -131,10 +185,7 @@ mod tests {
|
|
|
|
|
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(),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
let err = req.validate().unwrap_err();
|
|
|
|
|
assert!(err.contains("Theme"));
|
|
|
|
|
@ -144,10 +195,7 @@ mod tests {
|
|
|
|
|
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(),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
assert!(req.validate().is_err());
|
|
|
|
|
}
|
|
|
|
|
@ -155,11 +203,8 @@ mod tests {
|
|
|
|
|
#[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(),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
let err = req.validate().unwrap_err();
|
|
|
|
|
assert!(err.contains("max_age_days"));
|
|
|
|
|
@ -168,11 +213,8 @@ mod tests {
|
|
|
|
|
#[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(),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
assert!(req.validate().is_err());
|
|
|
|
|
}
|
|
|
|
|
@ -180,11 +222,8 @@ mod tests {
|
|
|
|
|
#[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(),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
let err = req.validate().unwrap_err();
|
|
|
|
|
assert!(err.contains("Categories"));
|
|
|
|
|
@ -194,11 +233,8 @@ mod tests {
|
|
|
|
|
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(),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
let err = req.validate().unwrap_err();
|
|
|
|
|
assert!(err.contains("20"));
|
|
|
|
|
@ -207,11 +243,8 @@ mod tests {
|
|
|
|
|
#[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(),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
let err = req.validate().unwrap_err();
|
|
|
|
|
assert!(err.contains("index 1"));
|
|
|
|
|
@ -220,11 +253,8 @@ mod tests {
|
|
|
|
|
#[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(),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
assert!(req.validate().is_err());
|
|
|
|
|
}
|
|
|
|
|
@ -232,11 +262,8 @@ mod tests {
|
|
|
|
|
#[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(),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
assert!(req.validate().is_err());
|
|
|
|
|
}
|
|
|
|
|
@ -244,11 +271,8 @@ mod tests {
|
|
|
|
|
#[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),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
assert!(req.validate().is_err());
|
|
|
|
|
}
|
|
|
|
|
@ -261,6 +285,7 @@ mod tests {
|
|
|
|
|
categories: vec!["Cat".into()],
|
|
|
|
|
max_items_per_category: 1,
|
|
|
|
|
search_agent_behavior: String::new(),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
assert!(req.validate().is_ok());
|
|
|
|
|
|
|
|
|
|
@ -270,7 +295,79 @@ mod tests {
|
|
|
|
|
categories: (0..20).map(|i| format!("Cat {}", i)).collect(),
|
|
|
|
|
max_items_per_category: 50,
|
|
|
|
|
search_agent_behavior: "a".repeat(2000),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
assert!(req.validate().is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_validate_with_model_fields() {
|
|
|
|
|
let req = UpdateSettingsRequest {
|
|
|
|
|
ai_provider: "google".into(),
|
|
|
|
|
ai_model: "gemini-2.5-pro".into(),
|
|
|
|
|
ai_model_writing: "gemini-2.5-flash".into(),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
assert!(req.validate().is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_validate_rate_limit_zero_rejected() {
|
|
|
|
|
let req = UpdateSettingsRequest {
|
|
|
|
|
rate_limit_max_requests: Some(0),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
let err = req.validate().unwrap_err();
|
|
|
|
|
assert!(err.contains("rate_limit_max_requests"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_validate_rate_limit_zero_window_rejected() {
|
|
|
|
|
let req = UpdateSettingsRequest {
|
|
|
|
|
rate_limit_time_window_seconds: Some(0),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
let err = req.validate().unwrap_err();
|
|
|
|
|
assert!(err.contains("rate_limit_time_window_seconds"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_validate_rate_limit_valid() {
|
|
|
|
|
let req = UpdateSettingsRequest {
|
|
|
|
|
rate_limit_max_requests: Some(29),
|
|
|
|
|
rate_limit_time_window_seconds: Some(60),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
assert!(req.validate().is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_validate_ai_provider_too_long_rejected() {
|
|
|
|
|
let req = UpdateSettingsRequest {
|
|
|
|
|
ai_provider: "a".repeat(101),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
let err = req.validate().unwrap_err();
|
|
|
|
|
assert!(err.contains("ai_provider"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_validate_ai_model_too_long_rejected() {
|
|
|
|
|
let req = UpdateSettingsRequest {
|
|
|
|
|
ai_model: "a".repeat(101),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
let err = req.validate().unwrap_err();
|
|
|
|
|
assert!(err.contains("ai_model"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_validate_ai_model_writing_too_long_rejected() {
|
|
|
|
|
let req = UpdateSettingsRequest {
|
|
|
|
|
ai_model_writing: "a".repeat(101),
|
|
|
|
|
..valid_request()
|
|
|
|
|
};
|
|
|
|
|
let err = req.validate().unwrap_err();
|
|
|
|
|
assert!(err.contains("ai_model_writing"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|