reinhardt_conf/settings/
core_settings.rs1use super::database_config::DatabaseConfig;
6use super::fragment::SettingsValidation;
7use super::profile::Profile;
8use super::security::SecuritySettings;
9use super::validation::{ValidationError, ValidationResult};
10use reinhardt_core::macros::settings;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::PathBuf;
14
15#[settings(fragment = true, section = "core", validate = false)]
20#[derive(Clone, Debug, Serialize, Deserialize)]
21pub struct CoreSettings {
22 #[serde(default = "default_base_dir")]
24 pub base_dir: PathBuf,
25 #[setting(required)]
27 pub secret_key: String,
28 #[serde(default = "default_debug")]
30 pub debug: bool,
31 #[serde(default)]
33 pub allowed_hosts: Vec<String>,
34 #[serde(default = "default_databases")]
36 pub databases: HashMap<String, DatabaseConfig>,
37 #[setting(node)]
63 #[serde(default)]
64 pub security: SecuritySettings,
65 #[serde(default)]
67 pub middleware: Vec<String>,
68 #[serde(default)]
70 pub root_urlconf: String,
71 #[serde(default)]
73 pub installed_apps: Vec<String>,
74}
75
76fn default_base_dir() -> PathBuf {
77 PathBuf::from(".")
78}
79
80fn default_debug() -> bool {
81 true
82}
83
84fn default_databases() -> HashMap<String, DatabaseConfig> {
85 let mut map = HashMap::new();
86 map.insert("default".to_string(), DatabaseConfig::default());
87 map
88}
89
90impl Default for CoreSettings {
91 fn default() -> Self {
92 Self {
93 base_dir: default_base_dir(),
94 secret_key: String::new(),
95 debug: true,
96 allowed_hosts: Vec::new(),
97 databases: default_databases(),
98 security: SecuritySettings::default(),
99 middleware: Vec::new(),
100 root_urlconf: String::new(),
101 installed_apps: Vec::new(),
102 }
103 }
104}
105
106impl SettingsValidation for CoreSettings {
107 fn validate(&self, profile: &Profile) -> ValidationResult {
108 if self.secret_key.is_empty() {
109 return Err(ValidationError::MissingRequired("secret_key".to_string()));
110 }
111
112 if profile.is_production() {
113 if self.debug {
114 return Err(ValidationError::Security(
115 "debug must be false in production".to_string(),
116 ));
117 }
118 if self.allowed_hosts.is_empty() {
119 return Err(ValidationError::MissingRequired(
120 "allowed_hosts".to_string(),
121 ));
122 }
123 }
124
125 self.security.validate(profile)?;
127
128 Ok(())
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::{CoreSettings, SecuritySettings};
135 use crate::settings::fragment::SettingsFragment;
136 use crate::settings::profile::Profile;
137 use rstest::rstest;
138
139 #[rstest]
140 fn test_core_settings_section() {
141 assert_eq!(CoreSettings::section(), "core");
143 }
144
145 #[rstest]
146 fn test_core_settings_default() {
147 let settings = CoreSettings::default();
149
150 assert!(settings.debug);
152 assert!(settings.secret_key.is_empty());
153 assert!(settings.allowed_hosts.is_empty());
154 assert!(settings.databases.contains_key("default"));
155 assert!(!settings.security.secure_ssl_redirect);
156 }
157
158 #[rstest]
159 fn test_core_settings_validate_missing_secret_key() {
160 let settings = CoreSettings::default();
162
163 let result = settings.validate(&Profile::Development);
165
166 assert!(result.is_err());
168 }
169
170 #[rstest]
171 fn test_core_settings_validate_development_ok() {
172 let settings = CoreSettings {
174 secret_key: "test-secret-key".to_string(),
175 ..Default::default()
176 };
177
178 let result = settings.validate(&Profile::Development);
180
181 assert!(result.is_ok());
183 }
184
185 #[rstest]
186 fn test_core_settings_validate_production_debug_fails() {
187 let settings = CoreSettings {
189 secret_key: "production-secret-key".to_string(),
190 debug: true,
191 ..Default::default()
192 };
193
194 let result = settings.validate(&Profile::Production);
196
197 assert!(result.is_err());
199 }
200
201 #[rstest]
202 fn test_core_settings_validate_production_ok() {
203 let settings = CoreSettings {
205 secret_key: "production-secret-key".to_string(),
206 debug: false,
207 allowed_hosts: vec!["example.com".to_string()],
208 security: SecuritySettings {
209 secure_ssl_redirect: true,
210 session_cookie_secure: true,
211 csrf_cookie_secure: true,
212 ..Default::default()
213 },
214 ..Default::default()
215 };
216
217 let result = settings.validate(&Profile::Production);
219
220 assert!(result.is_ok());
222 }
223
224 #[rstest]
225 fn test_core_settings_delegates_security_validation() {
226 let settings = CoreSettings {
228 secret_key: "production-secret-key".to_string(),
229 debug: false,
230 allowed_hosts: vec!["example.com".to_string()],
231 security: SecuritySettings::default(), ..Default::default()
233 };
234
235 let result = settings.validate(&Profile::Production);
237
238 assert!(result.is_err());
240 }
241
242 #[rstest]
243 fn test_core_settings_field_policies_secret_key_required() {
244 use crate::settings::policy::FieldRequirement;
245
246 let policies = CoreSettings::field_policies();
248
249 let secret_key_policy = policies.iter().find(|p| p.name == "secret_key");
251 assert!(
252 secret_key_policy.is_some(),
253 "secret_key must have an explicit field policy"
254 );
255 let policy = secret_key_policy.unwrap();
256 assert_eq!(
257 policy.requirement,
258 FieldRequirement::Required,
259 "secret_key must be Required"
260 );
261 assert!(
262 !policy.has_default,
263 "secret_key must not have a default value"
264 );
265 }
266
267 #[rstest]
268 fn test_core_settings_deserialize_nested_security_section() {
269 let toml_str = r#"
271secret_key = "test-secret"
272debug = false
273
274[security]
275secure_ssl_redirect = true
276session_cookie_secure = true
277csrf_cookie_secure = true
278secure_hsts_seconds = 31536000
279secure_hsts_include_subdomains = true
280secure_hsts_preload = true
281"#;
282
283 let settings: CoreSettings = toml::from_str(toml_str).expect("failed to parse TOML");
285
286 assert_eq!(settings.secret_key, "test-secret");
288 assert!(!settings.debug);
289 assert!(settings.security.secure_ssl_redirect);
290 assert!(settings.security.session_cookie_secure);
291 assert!(settings.security.csrf_cookie_secure);
292 assert_eq!(settings.security.secure_hsts_seconds, Some(31536000));
293 assert!(settings.security.secure_hsts_include_subdomains);
294 assert!(settings.security.secure_hsts_preload);
295 }
296
297 #[rstest]
298 fn test_core_settings_deserialize_omitted_security_uses_defaults() {
299 let toml_str = r#"
301secret_key = "test-secret"
302debug = true
303"#;
304
305 let settings: CoreSettings = toml::from_str(toml_str).expect("failed to parse TOML");
307
308 assert!(!settings.security.secure_ssl_redirect);
310 assert!(!settings.security.session_cookie_secure);
311 assert!(!settings.security.csrf_cookie_secure);
312 assert_eq!(settings.security.secure_hsts_seconds, None);
313 }
314
315 #[rstest]
316 fn test_core_settings_deserialize_partial_security_section() {
317 let toml_str = r#"
319secret_key = "test-secret"
320
321[security]
322secure_ssl_redirect = true
323"#;
324
325 let settings: CoreSettings = toml::from_str(toml_str).expect("failed to parse TOML");
327
328 assert!(settings.security.secure_ssl_redirect);
330 assert!(!settings.security.session_cookie_secure);
331 assert!(!settings.security.csrf_cookie_secure);
332 }
333
334 #[rstest]
335 fn test_core_settings_field_policies_other_fields_optional() {
336 use crate::settings::policy::FieldRequirement;
337
338 let policies = CoreSettings::field_policies();
340
341 let optional_fields = [
343 "base_dir",
344 "debug",
345 "allowed_hosts",
346 "databases",
347 "security",
348 "middleware",
349 "root_urlconf",
350 "installed_apps",
351 ];
352 for field_name in optional_fields {
353 let policy = policies.iter().find(|p| p.name == field_name);
354 assert!(
355 policy.is_some(),
356 "field '{field_name}' must have a field policy"
357 );
358 let policy = policy.unwrap();
359 assert_eq!(
360 policy.requirement,
361 FieldRequirement::Optional,
362 "field '{field_name}' must be Optional"
363 );
364 assert!(
365 policy.has_default,
366 "field '{field_name}' must have a default value"
367 );
368 }
369 }
370}