1use serde::Deserialize;
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Deserialize, Default)]
15struct FileConfig {
16 server: Option<ServerSection>,
17 auth: Option<AuthSection>,
18 tls: Option<TlsSection>,
19 cors: Option<CorsSection>,
20}
21
22#[derive(Debug, Deserialize, Default)]
23struct ServerSection {
24 host: Option<String>,
25 port: Option<u16>,
26 data_dir: Option<String>,
27 shutdown_timeout_secs: Option<u64>,
28 rate_limit: Option<u32>,
29}
30
31#[derive(Debug, Deserialize, Default)]
32struct AuthSection {
33 api_keys: Option<Vec<String>>,
34}
35
36#[derive(Debug, Deserialize, Default)]
37struct TlsSection {
38 cert: Option<String>,
39 key: Option<String>,
40}
41
42#[derive(Debug, Deserialize, Default)]
43struct CorsSection {
44 allowed_origins: Option<Vec<String>>,
45 allowed_methods: Option<Vec<String>>,
46 allowed_headers: Option<Vec<String>>,
47 allow_credentials: Option<bool>,
48 max_age_secs: Option<u64>,
49}
50
51#[derive(Debug, Clone, PartialEq, Default)]
60pub struct TlsConfig {
61 pub cert: Option<String>,
63 pub key: Option<String>,
65}
66
67impl TlsConfig {
68 pub fn is_enabled(&self) -> bool {
70 self.cert.is_some() && self.key.is_some()
71 }
72}
73
74#[derive(Debug, Clone, PartialEq)]
76pub struct ServerConfig {
77 pub host: String,
78 pub port: u16,
79 pub data_dir: String,
80 pub api_keys: Vec<String>,
81 pub tls: TlsConfig,
83 pub shutdown_timeout_secs: u64,
84 pub rate_limit: u32,
86 pub cors: CorsConfig,
88}
89
90#[derive(Debug, Clone, PartialEq)]
98pub struct CorsConfig {
99 pub allowed_origins: Vec<String>,
101 pub allowed_methods: Vec<String>,
103 pub allowed_headers: Vec<String>,
106 pub allow_credentials: bool,
108 pub max_age_secs: u64,
110}
111
112const DEFAULT_RATE_LIMIT: u32 = 100;
114
115const DEFAULT_CORS_MAX_AGE_SECS: u64 = 3600;
117
118impl Default for CorsConfig {
119 fn default() -> Self {
120 Self {
121 allowed_origins: vec!["*".to_string()],
122 allowed_methods: vec![
123 "GET".to_string(),
124 "POST".to_string(),
125 "PUT".to_string(),
126 "DELETE".to_string(),
127 "PATCH".to_string(),
128 "OPTIONS".to_string(),
129 ],
130 allowed_headers: vec!["*".to_string()],
131 allow_credentials: false,
132 max_age_secs: DEFAULT_CORS_MAX_AGE_SECS,
133 }
134 }
135}
136
137impl CorsConfig {
138 pub fn is_permissive(&self) -> bool {
140 self.allowed_origins.iter().any(|o| o == "*")
141 }
142}
143
144impl Default for ServerConfig {
145 fn default() -> Self {
146 Self {
147 host: "127.0.0.1".to_string(),
148 port: 8080,
149 data_dir: "./velesdb_data".to_string(),
150 api_keys: Vec::new(),
151 tls: TlsConfig::default(),
152 shutdown_timeout_secs: 30,
153 rate_limit: DEFAULT_RATE_LIMIT,
154 cors: CorsConfig::default(),
155 }
156 }
157}
158
159impl ServerConfig {
164 pub fn load(cli: CliOverrides) -> anyhow::Result<Self> {
170 let defaults = Self::default();
171 let file_cfg = load_toml_file(&cli.config_path)?;
172 Ok(Self::merge(defaults, file_cfg, cli))
173 }
174
175 fn merge(defaults: Self, file: FileConfig, cli: CliOverrides) -> Self {
176 let server = file.server.unwrap_or_default();
177 let auth = file.auth.unwrap_or_default();
178 let tls = file.tls.unwrap_or_default();
179 let cors_section = file.cors.unwrap_or_default();
180
181 let host = server.host.unwrap_or(defaults.host);
183 let port = server.port.unwrap_or(defaults.port);
184 let data_dir = server.data_dir.unwrap_or(defaults.data_dir);
185 let shutdown_timeout_secs = server
186 .shutdown_timeout_secs
187 .unwrap_or(defaults.shutdown_timeout_secs);
188 let rate_limit = server.rate_limit.unwrap_or(defaults.rate_limit);
189 let api_keys = auth.api_keys.unwrap_or(defaults.api_keys);
190 let tls = TlsConfig {
191 cert: tls.cert.or(defaults.tls.cert),
192 key: tls.key.or(defaults.tls.key),
193 };
194 let cors = resolve_cors(defaults.cors, cors_section);
195
196 let host = cli.host.unwrap_or(host);
198 let port = cli.port.unwrap_or(port);
199 let data_dir = cli.data_dir.unwrap_or(data_dir);
200 let api_keys = cli.api_keys.unwrap_or(api_keys);
201 let tls = TlsConfig {
202 cert: cli.tls_cert.or(tls.cert),
203 key: cli.tls_key.or(tls.key),
204 };
205 let rate_limit = cli.rate_limit.unwrap_or(rate_limit);
206
207 Self {
208 host,
209 port,
210 data_dir,
211 api_keys,
212 tls,
213 shutdown_timeout_secs,
214 rate_limit,
215 cors,
216 }
217 }
218
219 pub fn validate(&self) -> anyhow::Result<()> {
221 if self.port == 0 {
222 anyhow::bail!("invalid port: 0 is not allowed");
223 }
224 if self.data_dir.is_empty() {
225 anyhow::bail!("data_dir must not be empty");
226 }
227
228 match (&self.tls.cert, &self.tls.key) {
230 (Some(_), None) => {
231 anyhow::bail!("tls_cert is set but tls_key is missing");
232 }
233 (None, Some(_)) => {
234 anyhow::bail!("tls_key is set but tls_cert is missing");
235 }
236 (Some(cert), Some(key)) => {
237 if !Path::new(cert).exists() {
238 anyhow::bail!("TLS cert file not found: {cert}");
239 }
240 if !Path::new(key).exists() {
241 anyhow::bail!("TLS key file not found: {key}");
242 }
243 }
244 (None, None) => {}
245 }
246
247 Ok(())
248 }
249
250 pub fn auth_enabled(&self) -> bool {
252 !self.api_keys.is_empty()
253 }
254
255 pub fn tls_enabled(&self) -> bool {
257 self.tls.is_enabled()
258 }
259
260 pub fn rate_limit_enabled(&self) -> bool {
262 self.rate_limit > 0
263 }
264}
265
266#[derive(Debug, Default)]
273pub struct CliOverrides {
274 pub config_path: Option<PathBuf>,
275 pub host: Option<String>,
276 pub port: Option<u16>,
277 pub data_dir: Option<String>,
278 pub api_keys: Option<Vec<String>>,
279 pub tls_cert: Option<String>,
280 pub tls_key: Option<String>,
281 pub rate_limit: Option<u32>,
282}
283
284fn load_toml_file(path: &Option<PathBuf>) -> anyhow::Result<FileConfig> {
289 let candidate = match path {
290 Some(p) => {
291 if !p.exists() {
292 anyhow::bail!("config file not found: {}", p.display());
293 }
294 p.clone()
295 }
296 None => {
297 let default_path = PathBuf::from("velesdb.toml");
298 if !default_path.exists() {
299 return Ok(FileConfig::default());
300 }
301 default_path
302 }
303 };
304
305 let contents = std::fs::read_to_string(&candidate)
306 .map_err(|e| anyhow::anyhow!("failed to read config file {}: {e}", candidate.display()))?;
307
308 let cfg: FileConfig = toml::from_str(&contents)
309 .map_err(|e| anyhow::anyhow!("failed to parse config file {}: {e}", candidate.display()))?;
310
311 Ok(cfg)
312}
313
314fn resolve_cors(defaults: CorsConfig, section: CorsSection) -> CorsConfig {
320 CorsConfig {
321 allowed_origins: section.allowed_origins.unwrap_or(defaults.allowed_origins),
322 allowed_methods: section.allowed_methods.unwrap_or(defaults.allowed_methods),
323 allowed_headers: section.allowed_headers.unwrap_or(defaults.allowed_headers),
324 allow_credentials: section
325 .allow_credentials
326 .unwrap_or(defaults.allow_credentials),
327 max_age_secs: section.max_age_secs.unwrap_or(defaults.max_age_secs),
328 }
329}
330
331pub fn build_cors_layer(cors: &CorsConfig) -> tower_http::cors::CorsLayer {
337 use tower_http::cors::{AllowOrigin, CorsLayer};
338
339 if cors.is_permissive() {
340 return CorsLayer::permissive();
341 }
342
343 let origins: Vec<axum::http::HeaderValue> = cors
344 .allowed_origins
345 .iter()
346 .filter_map(|o| o.parse().ok())
347 .collect();
348 let methods: Vec<axum::http::Method> = cors
349 .allowed_methods
350 .iter()
351 .filter_map(|m| m.parse().ok())
352 .collect();
353
354 let layer = CorsLayer::new()
355 .allow_origin(AllowOrigin::list(origins))
356 .allow_methods(methods)
357 .max_age(std::time::Duration::from_secs(cors.max_age_secs));
358
359 let layer = apply_cors_headers_policy(layer, cors);
360
361 if cors.allow_credentials {
362 layer.allow_credentials(true)
363 } else {
364 layer
365 }
366}
367
368fn apply_cors_headers_policy(
372 layer: tower_http::cors::CorsLayer,
373 cors: &CorsConfig,
374) -> tower_http::cors::CorsLayer {
375 use tower_http::cors::Any;
376
377 let has_wildcard = cors.allowed_headers.iter().any(|h| h == "*");
378 if has_wildcard && !cors.allow_credentials {
379 return layer.allow_headers(Any);
380 }
381 if has_wildcard && cors.allow_credentials {
382 tracing::warn!(
383 "CORS: allow_credentials=true is incompatible with wildcard \
384 headers per CORS spec. Falling back to default headers \
385 (Content-Type, Authorization)."
386 );
387 }
388 let headers: Vec<axum::http::HeaderName> = cors
389 .allowed_headers
390 .iter()
391 .filter(|h| h.as_str() != "*")
392 .filter_map(|h| h.parse().ok())
393 .collect();
394 if headers.is_empty() && cors.allow_credentials {
395 layer.allow_headers([
396 axum::http::header::CONTENT_TYPE,
397 axum::http::header::AUTHORIZATION,
398 ])
399 } else {
400 layer.allow_headers(headers)
401 }
402}
403
404pub fn parse_api_keys_env() -> Option<Vec<String>> {
410 let val = std::env::var("VELESDB_API_KEYS").ok()?;
411 let keys: Vec<String> = val
412 .split(',')
413 .map(|s| s.trim().to_string())
414 .filter(|s| !s.is_empty())
415 .collect();
416 if keys.is_empty() {
417 None
418 } else {
419 Some(keys)
420 }
421}
422
423#[cfg(test)]
428mod tests {
429 use super::*;
430 use std::io::Write;
431
432 #[test]
433 fn test_defaults() {
434 let cfg = ServerConfig::default();
435 assert_eq!(cfg.host, "127.0.0.1");
436 assert_eq!(cfg.port, 8080);
437 assert_eq!(cfg.data_dir, "./velesdb_data");
438 assert!(cfg.api_keys.is_empty());
439 assert!(cfg.tls.cert.is_none());
440 assert!(cfg.tls.key.is_none());
441 assert_eq!(cfg.shutdown_timeout_secs, 30);
442 assert_eq!(cfg.rate_limit, 100);
443 assert!(!cfg.auth_enabled());
444 assert!(!cfg.tls_enabled());
445 assert!(cfg.rate_limit_enabled());
446 assert!(cfg.cors.is_permissive());
447 }
448
449 #[test]
450 fn test_toml_overrides_defaults() {
451 let toml_content = r#"
452[server]
453host = "0.0.0.0"
454port = 9090
455data_dir = "/var/velesdb"
456shutdown_timeout_secs = 60
457
458[auth]
459api_keys = ["key-alpha", "key-beta"]
460
461[tls]
462cert = "/etc/ssl/cert.pem"
463key = "/etc/ssl/key.pem"
464"#;
465 let file_cfg: FileConfig =
466 toml::from_str(toml_content).expect("test: valid FileConfig TOML");
467 let cli = CliOverrides::default();
468 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
469
470 assert_eq!(cfg.host, "0.0.0.0");
471 assert_eq!(cfg.port, 9090);
472 assert_eq!(cfg.data_dir, "/var/velesdb");
473 assert_eq!(cfg.shutdown_timeout_secs, 60);
474 assert_eq!(cfg.api_keys, vec!["key-alpha", "key-beta"]);
475 assert_eq!(cfg.tls.cert.as_deref(), Some("/etc/ssl/cert.pem"));
476 assert_eq!(cfg.tls.key.as_deref(), Some("/etc/ssl/key.pem"));
477 assert!(cfg.auth_enabled());
478 assert!(cfg.tls_enabled());
479 }
480
481 #[test]
482 fn test_cli_overrides_toml() {
483 let toml_content = r#"
484[server]
485host = "0.0.0.0"
486port = 9090
487"#;
488 let file_cfg: FileConfig =
489 toml::from_str(toml_content).expect("test: valid FileConfig TOML");
490 let cli = CliOverrides {
491 port: Some(3000),
492 host: Some("10.0.0.1".to_string()),
493 ..Default::default()
494 };
495 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
496
497 assert_eq!(cfg.host, "10.0.0.1");
499 assert_eq!(cfg.port, 3000);
500 assert_eq!(cfg.data_dir, "./velesdb_data");
502 }
503
504 #[test]
505 fn test_partial_toml_uses_defaults_for_missing() {
506 let toml_content = r#"
507[server]
508port = 4000
509"#;
510 let file_cfg: FileConfig =
511 toml::from_str(toml_content).expect("test: valid FileConfig TOML");
512 let cli = CliOverrides::default();
513 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
514
515 assert_eq!(cfg.port, 4000);
516 assert_eq!(cfg.host, "127.0.0.1"); assert_eq!(cfg.data_dir, "./velesdb_data"); }
519
520 #[test]
521 fn test_empty_toml_uses_all_defaults() {
522 let file_cfg: FileConfig = toml::from_str("").expect("test: empty TOML parses to default");
523 let cli = CliOverrides::default();
524 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
525
526 assert_eq!(cfg, ServerConfig::default());
527 }
528
529 #[test]
530 fn test_validate_port_zero_rejected() {
531 let cfg = ServerConfig {
532 port: 0,
533 ..ServerConfig::default()
534 };
535 let err = cfg.validate().unwrap_err();
536 assert!(err.to_string().contains("port"));
537 }
538
539 #[test]
540 fn test_validate_empty_data_dir_rejected() {
541 let cfg = ServerConfig {
542 data_dir: String::new(),
543 ..ServerConfig::default()
544 };
545 let err = cfg.validate().unwrap_err();
546 assert!(err.to_string().contains("data_dir"));
547 }
548
549 #[test]
550 fn test_validate_tls_cert_without_key() {
551 let cfg = ServerConfig {
552 tls: TlsConfig {
553 cert: Some("/tmp/cert.pem".to_string()),
554 key: None,
555 },
556 ..ServerConfig::default()
557 };
558 let err = cfg.validate().unwrap_err();
559 assert!(err.to_string().contains("tls_key is missing"));
560 }
561
562 #[test]
563 fn test_validate_tls_key_without_cert() {
564 let cfg = ServerConfig {
565 tls: TlsConfig {
566 cert: None,
567 key: Some("/tmp/key.pem".to_string()),
568 },
569 ..ServerConfig::default()
570 };
571 let err = cfg.validate().unwrap_err();
572 assert!(err.to_string().contains("tls_cert is missing"));
573 }
574
575 #[test]
576 fn test_validate_tls_missing_cert_file() {
577 let cfg = ServerConfig {
578 tls: TlsConfig {
579 cert: Some("/nonexistent/cert.pem".to_string()),
580 key: Some("/nonexistent/key.pem".to_string()),
581 },
582 ..ServerConfig::default()
583 };
584 let err = cfg.validate().unwrap_err();
585 assert!(err.to_string().contains("cert file not found"));
586 }
587
588 #[test]
589 fn test_validate_tls_valid_files() {
590 let dir = tempfile::tempdir().expect("test: create temp dir");
591 let cert_path = dir.path().join("cert.pem");
592 let key_path = dir.path().join("key.pem");
593 std::fs::File::create(&cert_path)
594 .expect("test: create cert file")
595 .write_all(b"cert")
596 .expect("test: write cert content");
597 std::fs::File::create(&key_path)
598 .expect("test: create key file")
599 .write_all(b"key")
600 .expect("test: write key content");
601
602 let cfg = ServerConfig {
603 tls: TlsConfig {
604 cert: Some(cert_path.to_string_lossy().to_string()),
605 key: Some(key_path.to_string_lossy().to_string()),
606 },
607 ..ServerConfig::default()
608 };
609 cfg.validate().expect("valid TLS config should pass");
610 }
611
612 #[test]
613 fn test_parse_api_keys_env() {
614 let input = "key1, key2 , key3";
616 let keys: Vec<String> = input
617 .split(',')
618 .map(|s| s.trim().to_string())
619 .filter(|s| !s.is_empty())
620 .collect();
621 assert_eq!(keys, vec!["key1", "key2", "key3"]);
622 }
623
624 #[test]
625 fn test_load_toml_file_not_found_explicit_path() {
626 let result = load_toml_file(&Some(PathBuf::from("/nonexistent/velesdb.toml")));
627 assert!(result.is_err());
628 assert!(result
629 .unwrap_err()
630 .to_string()
631 .contains("config file not found"));
632 }
633
634 #[test]
635 fn test_load_toml_file_no_default_returns_empty() {
636 let result = load_toml_file(&None);
638 assert!(result.is_ok());
639 }
640
641 #[test]
642 fn test_full_priority_chain() {
643 let toml_content = r#"
645[server]
646port = 9090
647host = "0.0.0.0"
648data_dir = "/toml/data"
649"#;
650 let file_cfg: FileConfig =
651 toml::from_str(toml_content).expect("test: valid FileConfig TOML");
652 let cli = CliOverrides {
653 port: Some(3000),
654 ..Default::default()
656 };
657 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
658
659 assert_eq!(cfg.port, 3000); assert_eq!(cfg.host, "0.0.0.0"); assert_eq!(cfg.data_dir, "/toml/data"); }
663
664 #[test]
665 fn test_rate_limit_from_toml() {
666 let toml_content = r#"
667[server]
668rate_limit = 50
669"#;
670 let file_cfg: FileConfig =
671 toml::from_str(toml_content).expect("test: valid FileConfig TOML");
672 let cli = CliOverrides::default();
673 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
674
675 assert_eq!(cfg.rate_limit, 50);
676 assert!(cfg.rate_limit_enabled());
677 }
678
679 #[test]
680 fn test_rate_limit_disabled_via_toml() {
681 let toml_content = r#"
682[server]
683rate_limit = 0
684"#;
685 let file_cfg: FileConfig =
686 toml::from_str(toml_content).expect("test: valid FileConfig TOML");
687 let cli = CliOverrides::default();
688 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
689
690 assert_eq!(cfg.rate_limit, 0);
691 assert!(!cfg.rate_limit_enabled());
692 }
693
694 #[test]
695 fn test_rate_limit_cli_overrides_toml() {
696 let toml_content = r#"
697[server]
698rate_limit = 50
699"#;
700 let file_cfg: FileConfig =
701 toml::from_str(toml_content).expect("test: valid FileConfig TOML");
702 let cli = CliOverrides {
703 rate_limit: Some(200),
704 ..Default::default()
705 };
706 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
707
708 assert_eq!(cfg.rate_limit, 200);
709 }
710
711 #[test]
712 fn test_rate_limit_cli_disables() {
713 let file_cfg = FileConfig::default();
714 let cli = CliOverrides {
715 rate_limit: Some(0),
716 ..Default::default()
717 };
718 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
719
720 assert_eq!(cfg.rate_limit, 0);
721 assert!(!cfg.rate_limit_enabled());
722 }
723
724 #[test]
729 fn test_cors_default_is_permissive() {
730 let cors = CorsConfig::default();
731 assert!(cors.is_permissive());
732 assert_eq!(cors.allowed_origins, vec!["*"]);
733 assert_eq!(cors.allowed_headers, vec!["*"]);
734 assert!(!cors.allow_credentials);
735 assert_eq!(cors.max_age_secs, 3600);
736 }
737
738 #[test]
739 fn test_cors_specific_origins_not_permissive() {
740 let cors = CorsConfig {
741 allowed_origins: vec![
742 "https://app.example.com".to_string(),
743 "https://admin.example.com".to_string(),
744 ],
745 ..CorsConfig::default()
746 };
747 assert!(!cors.is_permissive());
748 assert_eq!(cors.allowed_origins.len(), 2);
749 }
750
751 #[test]
752 fn test_cors_from_toml_specific_origins() {
753 let toml_content = r#"
754[cors]
755allowed_origins = ["https://app.example.com", "https://admin.example.com"]
756allowed_methods = ["GET", "POST"]
757allowed_headers = ["Content-Type", "Authorization"]
758allow_credentials = true
759max_age_secs = 7200
760"#;
761 let file_cfg: FileConfig =
762 toml::from_str(toml_content).expect("test: valid FileConfig TOML");
763 let cli = CliOverrides::default();
764 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
765
766 assert!(!cfg.cors.is_permissive());
767 assert_eq!(
768 cfg.cors.allowed_origins,
769 vec!["https://app.example.com", "https://admin.example.com"]
770 );
771 assert_eq!(cfg.cors.allowed_methods, vec!["GET", "POST"]);
772 assert_eq!(
773 cfg.cors.allowed_headers,
774 vec!["Content-Type", "Authorization"]
775 );
776 assert!(cfg.cors.allow_credentials);
777 assert_eq!(cfg.cors.max_age_secs, 7200);
778 }
779
780 #[test]
781 fn test_cors_from_toml_partial_uses_defaults() {
782 let toml_content = r#"
783[cors]
784allowed_origins = ["https://myapp.com"]
785"#;
786 let file_cfg: FileConfig =
787 toml::from_str(toml_content).expect("test: valid FileConfig TOML");
788 let cli = CliOverrides::default();
789 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
790
791 assert!(!cfg.cors.is_permissive());
792 assert_eq!(cfg.cors.allowed_origins, vec!["https://myapp.com"]);
793 assert_eq!(cfg.cors.allowed_headers, vec!["*"]);
795 assert!(!cfg.cors.allow_credentials);
796 assert_eq!(cfg.cors.max_age_secs, 3600);
797 assert_eq!(cfg.cors.allowed_methods.len(), 6); }
799
800 #[test]
801 fn test_cors_absent_from_toml_uses_permissive_default() {
802 let toml_content = r#"
803[server]
804port = 9090
805"#;
806 let file_cfg: FileConfig =
807 toml::from_str(toml_content).expect("test: valid FileConfig TOML");
808 let cli = CliOverrides::default();
809 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
810
811 assert!(cfg.cors.is_permissive());
812 assert_eq!(cfg.cors, CorsConfig::default());
813 }
814
815 #[test]
816 fn test_cors_empty_section_uses_defaults() {
817 let toml_content = r#"
818[cors]
819"#;
820 let file_cfg: FileConfig =
821 toml::from_str(toml_content).expect("test: valid FileConfig TOML");
822 let cli = CliOverrides::default();
823 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
824
825 assert!(cfg.cors.is_permissive());
826 }
827
828 #[test]
829 fn test_build_cors_layer_permissive() {
830 let cors = CorsConfig::default();
831 let _layer = build_cors_layer(&cors);
833 }
834
835 #[test]
836 fn test_build_cors_layer_specific_origins() {
837 let cors = CorsConfig {
838 allowed_origins: vec![
839 "https://app.example.com".to_string(),
840 "http://localhost:3000".to_string(),
841 ],
842 allowed_methods: vec!["GET".to_string(), "POST".to_string()],
843 allowed_headers: vec!["Content-Type".to_string(), "Authorization".to_string()],
844 allow_credentials: true,
845 max_age_secs: 600,
846 };
847 let _layer = build_cors_layer(&cors);
849 }
850
851 #[test]
852 fn test_build_cors_layer_wildcard_headers() {
853 let cors = CorsConfig {
854 allowed_origins: vec!["https://myapp.com".to_string()],
855 allowed_headers: vec!["*".to_string()],
856 ..CorsConfig::default()
857 };
858 let _layer = build_cors_layer(&cors);
859 }
860
861 #[test]
862 fn test_build_cors_layer_invalid_origin_skipped() {
863 let cors = CorsConfig {
864 allowed_origins: vec![
865 "https://valid.com".to_string(),
866 "not a valid \x00 origin".to_string(),
867 ],
868 ..CorsConfig::default()
869 };
870 let _layer = build_cors_layer(&cors);
872 }
873
874 #[test]
875 fn test_server_config_default_includes_cors() {
876 let cfg = ServerConfig::default();
877 assert!(cfg.cors.is_permissive());
878 }
879}