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)]
57pub struct ServerConfig {
58 pub host: String,
59 pub port: u16,
60 pub data_dir: String,
61 pub api_keys: Vec<String>,
62 pub tls_cert: Option<String>,
63 pub tls_key: Option<String>,
64 pub shutdown_timeout_secs: u64,
65 pub rate_limit: u32,
67 pub cors: CorsConfig,
69}
70
71#[derive(Debug, Clone, PartialEq)]
79pub struct CorsConfig {
80 pub allowed_origins: Vec<String>,
82 pub allowed_methods: Vec<String>,
84 pub allowed_headers: Vec<String>,
87 pub allow_credentials: bool,
89 pub max_age_secs: u64,
91}
92
93const DEFAULT_RATE_LIMIT: u32 = 100;
95
96const DEFAULT_CORS_MAX_AGE_SECS: u64 = 3600;
98
99impl Default for CorsConfig {
100 fn default() -> Self {
101 Self {
102 allowed_origins: vec!["*".to_string()],
103 allowed_methods: vec![
104 "GET".to_string(),
105 "POST".to_string(),
106 "PUT".to_string(),
107 "DELETE".to_string(),
108 "PATCH".to_string(),
109 "OPTIONS".to_string(),
110 ],
111 allowed_headers: vec!["*".to_string()],
112 allow_credentials: false,
113 max_age_secs: DEFAULT_CORS_MAX_AGE_SECS,
114 }
115 }
116}
117
118impl CorsConfig {
119 pub fn is_permissive(&self) -> bool {
121 self.allowed_origins.iter().any(|o| o == "*")
122 }
123}
124
125impl Default for ServerConfig {
126 fn default() -> Self {
127 Self {
128 host: "127.0.0.1".to_string(),
129 port: 8080,
130 data_dir: "./velesdb_data".to_string(),
131 api_keys: Vec::new(),
132 tls_cert: None,
133 tls_key: None,
134 shutdown_timeout_secs: 30,
135 rate_limit: DEFAULT_RATE_LIMIT,
136 cors: CorsConfig::default(),
137 }
138 }
139}
140
141impl ServerConfig {
146 pub fn load(cli: CliOverrides) -> anyhow::Result<Self> {
152 let defaults = Self::default();
153 let file_cfg = load_toml_file(&cli.config_path)?;
154 Ok(Self::merge(defaults, file_cfg, cli))
155 }
156
157 fn merge(defaults: Self, file: FileConfig, cli: CliOverrides) -> Self {
158 let server = file.server.unwrap_or_default();
159 let auth = file.auth.unwrap_or_default();
160 let tls = file.tls.unwrap_or_default();
161 let cors_section = file.cors.unwrap_or_default();
162
163 let host = server.host.unwrap_or(defaults.host);
165 let port = server.port.unwrap_or(defaults.port);
166 let data_dir = server.data_dir.unwrap_or(defaults.data_dir);
167 let shutdown_timeout_secs = server
168 .shutdown_timeout_secs
169 .unwrap_or(defaults.shutdown_timeout_secs);
170 let rate_limit = server.rate_limit.unwrap_or(defaults.rate_limit);
171 let api_keys = auth.api_keys.unwrap_or(defaults.api_keys);
172 let tls_cert = tls.cert.or(defaults.tls_cert);
173 let tls_key = tls.key.or(defaults.tls_key);
174 let cors = resolve_cors(defaults.cors, cors_section);
175
176 let host = cli.host.unwrap_or(host);
178 let port = cli.port.unwrap_or(port);
179 let data_dir = cli.data_dir.unwrap_or(data_dir);
180 let api_keys = cli.api_keys.unwrap_or(api_keys);
181 let tls_cert = cli.tls_cert.or(tls_cert);
182 let tls_key = cli.tls_key.or(tls_key);
183 let rate_limit = cli.rate_limit.unwrap_or(rate_limit);
184
185 Self {
186 host,
187 port,
188 data_dir,
189 api_keys,
190 tls_cert,
191 tls_key,
192 shutdown_timeout_secs,
193 rate_limit,
194 cors,
195 }
196 }
197
198 pub fn validate(&self) -> anyhow::Result<()> {
200 if self.port == 0 {
201 anyhow::bail!("invalid port: 0 is not allowed");
202 }
203 if self.data_dir.is_empty() {
204 anyhow::bail!("data_dir must not be empty");
205 }
206
207 match (&self.tls_cert, &self.tls_key) {
209 (Some(_), None) => {
210 anyhow::bail!("tls_cert is set but tls_key is missing");
211 }
212 (None, Some(_)) => {
213 anyhow::bail!("tls_key is set but tls_cert is missing");
214 }
215 (Some(cert), Some(key)) => {
216 if !Path::new(cert).exists() {
217 anyhow::bail!("TLS cert file not found: {cert}");
218 }
219 if !Path::new(key).exists() {
220 anyhow::bail!("TLS key file not found: {key}");
221 }
222 }
223 (None, None) => {}
224 }
225
226 Ok(())
227 }
228
229 pub fn auth_enabled(&self) -> bool {
231 !self.api_keys.is_empty()
232 }
233
234 pub fn tls_enabled(&self) -> bool {
236 self.tls_cert.is_some() && self.tls_key.is_some()
237 }
238
239 pub fn rate_limit_enabled(&self) -> bool {
241 self.rate_limit > 0
242 }
243}
244
245#[derive(Debug, Default)]
252pub struct CliOverrides {
253 pub config_path: Option<PathBuf>,
254 pub host: Option<String>,
255 pub port: Option<u16>,
256 pub data_dir: Option<String>,
257 pub api_keys: Option<Vec<String>>,
258 pub tls_cert: Option<String>,
259 pub tls_key: Option<String>,
260 pub rate_limit: Option<u32>,
261}
262
263fn load_toml_file(path: &Option<PathBuf>) -> anyhow::Result<FileConfig> {
268 let candidate = match path {
269 Some(p) => {
270 if !p.exists() {
271 anyhow::bail!("config file not found: {}", p.display());
272 }
273 p.clone()
274 }
275 None => {
276 let default_path = PathBuf::from("velesdb.toml");
277 if !default_path.exists() {
278 return Ok(FileConfig::default());
279 }
280 default_path
281 }
282 };
283
284 let contents = std::fs::read_to_string(&candidate)
285 .map_err(|e| anyhow::anyhow!("failed to read config file {}: {e}", candidate.display()))?;
286
287 let cfg: FileConfig = toml::from_str(&contents)
288 .map_err(|e| anyhow::anyhow!("failed to parse config file {}: {e}", candidate.display()))?;
289
290 Ok(cfg)
291}
292
293fn resolve_cors(defaults: CorsConfig, section: CorsSection) -> CorsConfig {
299 CorsConfig {
300 allowed_origins: section.allowed_origins.unwrap_or(defaults.allowed_origins),
301 allowed_methods: section.allowed_methods.unwrap_or(defaults.allowed_methods),
302 allowed_headers: section.allowed_headers.unwrap_or(defaults.allowed_headers),
303 allow_credentials: section
304 .allow_credentials
305 .unwrap_or(defaults.allow_credentials),
306 max_age_secs: section.max_age_secs.unwrap_or(defaults.max_age_secs),
307 }
308}
309
310pub fn build_cors_layer(cors: &CorsConfig) -> tower_http::cors::CorsLayer {
316 use tower_http::cors::{AllowOrigin, CorsLayer};
317
318 if cors.is_permissive() {
319 return CorsLayer::permissive();
320 }
321
322 let origins: Vec<axum::http::HeaderValue> = cors
323 .allowed_origins
324 .iter()
325 .filter_map(|o| o.parse().ok())
326 .collect();
327 let methods: Vec<axum::http::Method> = cors
328 .allowed_methods
329 .iter()
330 .filter_map(|m| m.parse().ok())
331 .collect();
332
333 let layer = CorsLayer::new()
334 .allow_origin(AllowOrigin::list(origins))
335 .allow_methods(methods)
336 .max_age(std::time::Duration::from_secs(cors.max_age_secs));
337
338 let layer = apply_cors_headers_policy(layer, cors);
339
340 if cors.allow_credentials {
341 layer.allow_credentials(true)
342 } else {
343 layer
344 }
345}
346
347fn apply_cors_headers_policy(
351 layer: tower_http::cors::CorsLayer,
352 cors: &CorsConfig,
353) -> tower_http::cors::CorsLayer {
354 use tower_http::cors::Any;
355
356 let has_wildcard = cors.allowed_headers.iter().any(|h| h == "*");
357 if has_wildcard && !cors.allow_credentials {
358 return layer.allow_headers(Any);
359 }
360 if has_wildcard && cors.allow_credentials {
361 tracing::warn!(
362 "CORS: allow_credentials=true is incompatible with wildcard \
363 headers per CORS spec. Falling back to default headers \
364 (Content-Type, Authorization)."
365 );
366 }
367 let headers: Vec<axum::http::HeaderName> = cors
368 .allowed_headers
369 .iter()
370 .filter(|h| h.as_str() != "*")
371 .filter_map(|h| h.parse().ok())
372 .collect();
373 if headers.is_empty() && cors.allow_credentials {
374 layer.allow_headers([
375 axum::http::header::CONTENT_TYPE,
376 axum::http::header::AUTHORIZATION,
377 ])
378 } else {
379 layer.allow_headers(headers)
380 }
381}
382
383pub fn parse_api_keys_env() -> Option<Vec<String>> {
389 let val = std::env::var("VELESDB_API_KEYS").ok()?;
390 let keys: Vec<String> = val
391 .split(',')
392 .map(|s| s.trim().to_string())
393 .filter(|s| !s.is_empty())
394 .collect();
395 if keys.is_empty() {
396 None
397 } else {
398 Some(keys)
399 }
400}
401
402#[cfg(test)]
407mod tests {
408 use super::*;
409 use std::io::Write;
410
411 #[test]
412 fn test_defaults() {
413 let cfg = ServerConfig::default();
414 assert_eq!(cfg.host, "127.0.0.1");
415 assert_eq!(cfg.port, 8080);
416 assert_eq!(cfg.data_dir, "./velesdb_data");
417 assert!(cfg.api_keys.is_empty());
418 assert!(cfg.tls_cert.is_none());
419 assert!(cfg.tls_key.is_none());
420 assert_eq!(cfg.shutdown_timeout_secs, 30);
421 assert_eq!(cfg.rate_limit, 100);
422 assert!(!cfg.auth_enabled());
423 assert!(!cfg.tls_enabled());
424 assert!(cfg.rate_limit_enabled());
425 assert!(cfg.cors.is_permissive());
426 }
427
428 #[test]
429 fn test_toml_overrides_defaults() {
430 let toml_content = r#"
431[server]
432host = "0.0.0.0"
433port = 9090
434data_dir = "/var/velesdb"
435shutdown_timeout_secs = 60
436
437[auth]
438api_keys = ["key-alpha", "key-beta"]
439
440[tls]
441cert = "/etc/ssl/cert.pem"
442key = "/etc/ssl/key.pem"
443"#;
444 let file_cfg: FileConfig = toml::from_str(toml_content).unwrap();
445 let cli = CliOverrides::default();
446 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
447
448 assert_eq!(cfg.host, "0.0.0.0");
449 assert_eq!(cfg.port, 9090);
450 assert_eq!(cfg.data_dir, "/var/velesdb");
451 assert_eq!(cfg.shutdown_timeout_secs, 60);
452 assert_eq!(cfg.api_keys, vec!["key-alpha", "key-beta"]);
453 assert_eq!(cfg.tls_cert.as_deref(), Some("/etc/ssl/cert.pem"));
454 assert_eq!(cfg.tls_key.as_deref(), Some("/etc/ssl/key.pem"));
455 assert!(cfg.auth_enabled());
456 assert!(cfg.tls_enabled());
457 }
458
459 #[test]
460 fn test_cli_overrides_toml() {
461 let toml_content = r#"
462[server]
463host = "0.0.0.0"
464port = 9090
465"#;
466 let file_cfg: FileConfig = toml::from_str(toml_content).unwrap();
467 let cli = CliOverrides {
468 port: Some(3000),
469 host: Some("10.0.0.1".to_string()),
470 ..Default::default()
471 };
472 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
473
474 assert_eq!(cfg.host, "10.0.0.1");
476 assert_eq!(cfg.port, 3000);
477 assert_eq!(cfg.data_dir, "./velesdb_data");
479 }
480
481 #[test]
482 fn test_partial_toml_uses_defaults_for_missing() {
483 let toml_content = r#"
484[server]
485port = 4000
486"#;
487 let file_cfg: FileConfig = toml::from_str(toml_content).unwrap();
488 let cli = CliOverrides::default();
489 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
490
491 assert_eq!(cfg.port, 4000);
492 assert_eq!(cfg.host, "127.0.0.1"); assert_eq!(cfg.data_dir, "./velesdb_data"); }
495
496 #[test]
497 fn test_empty_toml_uses_all_defaults() {
498 let file_cfg: FileConfig = toml::from_str("").unwrap();
499 let cli = CliOverrides::default();
500 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
501
502 assert_eq!(cfg, ServerConfig::default());
503 }
504
505 #[test]
506 fn test_validate_port_zero_rejected() {
507 let cfg = ServerConfig {
508 port: 0,
509 ..ServerConfig::default()
510 };
511 let err = cfg.validate().unwrap_err();
512 assert!(err.to_string().contains("port"));
513 }
514
515 #[test]
516 fn test_validate_empty_data_dir_rejected() {
517 let cfg = ServerConfig {
518 data_dir: String::new(),
519 ..ServerConfig::default()
520 };
521 let err = cfg.validate().unwrap_err();
522 assert!(err.to_string().contains("data_dir"));
523 }
524
525 #[test]
526 fn test_validate_tls_cert_without_key() {
527 let cfg = ServerConfig {
528 tls_cert: Some("/tmp/cert.pem".to_string()),
529 ..ServerConfig::default()
530 };
531 let err = cfg.validate().unwrap_err();
532 assert!(err.to_string().contains("tls_key is missing"));
533 }
534
535 #[test]
536 fn test_validate_tls_key_without_cert() {
537 let cfg = ServerConfig {
538 tls_key: Some("/tmp/key.pem".to_string()),
539 ..ServerConfig::default()
540 };
541 let err = cfg.validate().unwrap_err();
542 assert!(err.to_string().contains("tls_cert is missing"));
543 }
544
545 #[test]
546 fn test_validate_tls_missing_cert_file() {
547 let cfg = ServerConfig {
548 tls_cert: Some("/nonexistent/cert.pem".to_string()),
549 tls_key: Some("/nonexistent/key.pem".to_string()),
550 ..ServerConfig::default()
551 };
552 let err = cfg.validate().unwrap_err();
553 assert!(err.to_string().contains("cert file not found"));
554 }
555
556 #[test]
557 fn test_validate_tls_valid_files() {
558 let dir = tempfile::tempdir().unwrap();
559 let cert_path = dir.path().join("cert.pem");
560 let key_path = dir.path().join("key.pem");
561 std::fs::File::create(&cert_path)
562 .unwrap()
563 .write_all(b"cert")
564 .unwrap();
565 std::fs::File::create(&key_path)
566 .unwrap()
567 .write_all(b"key")
568 .unwrap();
569
570 let cfg = ServerConfig {
571 tls_cert: Some(cert_path.to_string_lossy().to_string()),
572 tls_key: Some(key_path.to_string_lossy().to_string()),
573 ..ServerConfig::default()
574 };
575 cfg.validate().expect("valid TLS config should pass");
576 }
577
578 #[test]
579 fn test_parse_api_keys_env() {
580 let input = "key1, key2 , key3";
582 let keys: Vec<String> = input
583 .split(',')
584 .map(|s| s.trim().to_string())
585 .filter(|s| !s.is_empty())
586 .collect();
587 assert_eq!(keys, vec!["key1", "key2", "key3"]);
588 }
589
590 #[test]
591 fn test_load_toml_file_not_found_explicit_path() {
592 let result = load_toml_file(&Some(PathBuf::from("/nonexistent/velesdb.toml")));
593 assert!(result.is_err());
594 assert!(result
595 .unwrap_err()
596 .to_string()
597 .contains("config file not found"));
598 }
599
600 #[test]
601 fn test_load_toml_file_no_default_returns_empty() {
602 let result = load_toml_file(&None);
604 assert!(result.is_ok());
605 }
606
607 #[test]
608 fn test_full_priority_chain() {
609 let toml_content = r#"
611[server]
612port = 9090
613host = "0.0.0.0"
614data_dir = "/toml/data"
615"#;
616 let file_cfg: FileConfig = toml::from_str(toml_content).unwrap();
617 let cli = CliOverrides {
618 port: Some(3000),
619 ..Default::default()
621 };
622 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
623
624 assert_eq!(cfg.port, 3000); assert_eq!(cfg.host, "0.0.0.0"); assert_eq!(cfg.data_dir, "/toml/data"); }
628
629 #[test]
630 fn test_rate_limit_from_toml() {
631 let toml_content = r#"
632[server]
633rate_limit = 50
634"#;
635 let file_cfg: FileConfig = toml::from_str(toml_content).unwrap();
636 let cli = CliOverrides::default();
637 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
638
639 assert_eq!(cfg.rate_limit, 50);
640 assert!(cfg.rate_limit_enabled());
641 }
642
643 #[test]
644 fn test_rate_limit_disabled_via_toml() {
645 let toml_content = r#"
646[server]
647rate_limit = 0
648"#;
649 let file_cfg: FileConfig = toml::from_str(toml_content).unwrap();
650 let cli = CliOverrides::default();
651 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
652
653 assert_eq!(cfg.rate_limit, 0);
654 assert!(!cfg.rate_limit_enabled());
655 }
656
657 #[test]
658 fn test_rate_limit_cli_overrides_toml() {
659 let toml_content = r#"
660[server]
661rate_limit = 50
662"#;
663 let file_cfg: FileConfig = toml::from_str(toml_content).unwrap();
664 let cli = CliOverrides {
665 rate_limit: Some(200),
666 ..Default::default()
667 };
668 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
669
670 assert_eq!(cfg.rate_limit, 200);
671 }
672
673 #[test]
674 fn test_rate_limit_cli_disables() {
675 let file_cfg = FileConfig::default();
676 let cli = CliOverrides {
677 rate_limit: Some(0),
678 ..Default::default()
679 };
680 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
681
682 assert_eq!(cfg.rate_limit, 0);
683 assert!(!cfg.rate_limit_enabled());
684 }
685
686 #[test]
691 fn test_cors_default_is_permissive() {
692 let cors = CorsConfig::default();
693 assert!(cors.is_permissive());
694 assert_eq!(cors.allowed_origins, vec!["*"]);
695 assert_eq!(cors.allowed_headers, vec!["*"]);
696 assert!(!cors.allow_credentials);
697 assert_eq!(cors.max_age_secs, 3600);
698 }
699
700 #[test]
701 fn test_cors_specific_origins_not_permissive() {
702 let cors = CorsConfig {
703 allowed_origins: vec![
704 "https://app.example.com".to_string(),
705 "https://admin.example.com".to_string(),
706 ],
707 ..CorsConfig::default()
708 };
709 assert!(!cors.is_permissive());
710 assert_eq!(cors.allowed_origins.len(), 2);
711 }
712
713 #[test]
714 fn test_cors_from_toml_specific_origins() {
715 let toml_content = r#"
716[cors]
717allowed_origins = ["https://app.example.com", "https://admin.example.com"]
718allowed_methods = ["GET", "POST"]
719allowed_headers = ["Content-Type", "Authorization"]
720allow_credentials = true
721max_age_secs = 7200
722"#;
723 let file_cfg: FileConfig = toml::from_str(toml_content).unwrap();
724 let cli = CliOverrides::default();
725 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
726
727 assert!(!cfg.cors.is_permissive());
728 assert_eq!(
729 cfg.cors.allowed_origins,
730 vec!["https://app.example.com", "https://admin.example.com"]
731 );
732 assert_eq!(cfg.cors.allowed_methods, vec!["GET", "POST"]);
733 assert_eq!(
734 cfg.cors.allowed_headers,
735 vec!["Content-Type", "Authorization"]
736 );
737 assert!(cfg.cors.allow_credentials);
738 assert_eq!(cfg.cors.max_age_secs, 7200);
739 }
740
741 #[test]
742 fn test_cors_from_toml_partial_uses_defaults() {
743 let toml_content = r#"
744[cors]
745allowed_origins = ["https://myapp.com"]
746"#;
747 let file_cfg: FileConfig = toml::from_str(toml_content).unwrap();
748 let cli = CliOverrides::default();
749 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
750
751 assert!(!cfg.cors.is_permissive());
752 assert_eq!(cfg.cors.allowed_origins, vec!["https://myapp.com"]);
753 assert_eq!(cfg.cors.allowed_headers, vec!["*"]);
755 assert!(!cfg.cors.allow_credentials);
756 assert_eq!(cfg.cors.max_age_secs, 3600);
757 assert_eq!(cfg.cors.allowed_methods.len(), 6); }
759
760 #[test]
761 fn test_cors_absent_from_toml_uses_permissive_default() {
762 let toml_content = r#"
763[server]
764port = 9090
765"#;
766 let file_cfg: FileConfig = toml::from_str(toml_content).unwrap();
767 let cli = CliOverrides::default();
768 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
769
770 assert!(cfg.cors.is_permissive());
771 assert_eq!(cfg.cors, CorsConfig::default());
772 }
773
774 #[test]
775 fn test_cors_empty_section_uses_defaults() {
776 let toml_content = r#"
777[cors]
778"#;
779 let file_cfg: FileConfig = toml::from_str(toml_content).unwrap();
780 let cli = CliOverrides::default();
781 let cfg = ServerConfig::merge(ServerConfig::default(), file_cfg, cli);
782
783 assert!(cfg.cors.is_permissive());
784 }
785
786 #[test]
787 fn test_build_cors_layer_permissive() {
788 let cors = CorsConfig::default();
789 let _layer = build_cors_layer(&cors);
791 }
792
793 #[test]
794 fn test_build_cors_layer_specific_origins() {
795 let cors = CorsConfig {
796 allowed_origins: vec![
797 "https://app.example.com".to_string(),
798 "http://localhost:3000".to_string(),
799 ],
800 allowed_methods: vec!["GET".to_string(), "POST".to_string()],
801 allowed_headers: vec!["Content-Type".to_string(), "Authorization".to_string()],
802 allow_credentials: true,
803 max_age_secs: 600,
804 };
805 let _layer = build_cors_layer(&cors);
807 }
808
809 #[test]
810 fn test_build_cors_layer_wildcard_headers() {
811 let cors = CorsConfig {
812 allowed_origins: vec!["https://myapp.com".to_string()],
813 allowed_headers: vec!["*".to_string()],
814 ..CorsConfig::default()
815 };
816 let _layer = build_cors_layer(&cors);
817 }
818
819 #[test]
820 fn test_build_cors_layer_invalid_origin_skipped() {
821 let cors = CorsConfig {
822 allowed_origins: vec![
823 "https://valid.com".to_string(),
824 "not a valid \x00 origin".to_string(),
825 ],
826 ..CorsConfig::default()
827 };
828 let _layer = build_cors_layer(&cors);
830 }
831
832 #[test]
833 fn test_server_config_default_includes_cors() {
834 let cfg = ServerConfig::default();
835 assert!(cfg.cors.is_permissive());
836 }
837}