1use std::{net::SocketAddr, path::PathBuf};
4
5use fraiseql_core::security::OidcConfig;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
10#[serde(rename_all = "kebab-case")]
11pub enum PlaygroundTool {
12 GraphiQL,
14 #[default]
22 ApolloSandbox,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct TlsServerConfig {
28 pub enabled: bool,
30
31 pub cert_path: PathBuf,
33
34 pub key_path: PathBuf,
36
37 #[serde(default)]
39 pub require_client_cert: bool,
40
41 #[serde(default)]
43 pub client_ca_path: Option<PathBuf>,
44
45 #[serde(default = "default_tls_min_version")]
47 pub min_version: String,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct DatabaseTlsConfig {
53 #[serde(default = "default_postgres_ssl_mode")]
55 pub postgres_ssl_mode: String,
56
57 #[serde(default = "default_redis_ssl")]
59 pub redis_ssl: bool,
60
61 #[serde(default = "default_clickhouse_https")]
63 pub clickhouse_https: bool,
64
65 #[serde(default = "default_elasticsearch_https")]
67 pub elasticsearch_https: bool,
68
69 #[serde(default = "default_verify_certs")]
71 pub verify_certificates: bool,
72
73 #[serde(default)]
75 pub ca_bundle_path: Option<PathBuf>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct RateLimitingConfig {
81 #[serde(default = "default_rate_limiting_enabled")]
83 pub enabled: bool,
84
85 #[serde(default = "default_rate_limit_rps_per_ip")]
87 pub rps_per_ip: u32,
88
89 #[serde(default = "default_rate_limit_rps_per_user")]
91 pub rps_per_user: u32,
92
93 #[serde(default = "default_rate_limit_burst_size")]
95 pub burst_size: u32,
96
97 #[serde(default = "default_rate_limit_cleanup_interval")]
99 pub cleanup_interval_secs: u64,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct ServerConfig {
105 #[serde(default = "default_schema_path")]
107 pub schema_path: PathBuf,
108
109 #[serde(default = "default_database_url")]
111 pub database_url: String,
112
113 #[serde(default = "default_bind_addr")]
115 pub bind_addr: SocketAddr,
116
117 #[serde(default = "default_true")]
119 pub cors_enabled: bool,
120
121 #[serde(default)]
123 pub cors_origins: Vec<String>,
124
125 #[serde(default = "default_true")]
127 pub compression_enabled: bool,
128
129 #[serde(default = "default_true")]
131 pub tracing_enabled: bool,
132
133 #[serde(default = "default_true")]
135 pub apq_enabled: bool,
136
137 #[serde(default = "default_true")]
139 pub cache_enabled: bool,
140
141 #[serde(default = "default_graphql_path")]
143 pub graphql_path: String,
144
145 #[serde(default = "default_health_path")]
147 pub health_path: String,
148
149 #[serde(default = "default_introspection_path")]
151 pub introspection_path: String,
152
153 #[serde(default = "default_metrics_path")]
155 pub metrics_path: String,
156
157 #[serde(default = "default_metrics_json_path")]
159 pub metrics_json_path: String,
160
161 #[serde(default = "default_playground_path")]
163 pub playground_path: String,
164
165 #[serde(default)]
174 pub playground_enabled: bool,
175
176 #[serde(default)]
181 pub playground_tool: PlaygroundTool,
182
183 #[serde(default = "default_subscription_path")]
185 pub subscription_path: String,
186
187 #[serde(default = "default_true")]
192 pub subscriptions_enabled: bool,
193
194 #[serde(default)]
199 pub metrics_enabled: bool,
200
201 #[serde(default)]
208 pub metrics_token: Option<String>,
209
210 #[serde(default)]
215 pub admin_api_enabled: bool,
216
217 #[serde(default)]
225 pub admin_token: Option<String>,
226
227 #[serde(default)]
233 pub introspection_enabled: bool,
234
235 #[serde(default = "default_true")]
240 pub introspection_require_auth: bool,
241
242 #[serde(default = "default_true")]
248 pub design_api_require_auth: bool,
249
250 #[serde(default = "default_pool_min_size")]
252 pub pool_min_size: usize,
253
254 #[serde(default = "default_pool_max_size")]
256 pub pool_max_size: usize,
257
258 #[serde(default = "default_pool_timeout")]
260 pub pool_timeout_secs: u64,
261
262 #[serde(default)]
276 pub auth: Option<OidcConfig>,
277
278 #[serde(default)]
294 pub tls: Option<TlsServerConfig>,
295
296 #[serde(default)]
312 pub database_tls: Option<DatabaseTlsConfig>,
313
314 #[serde(default)]
329 pub rate_limiting: Option<RateLimitingConfig>,
330
331 #[cfg(feature = "observers")]
333 #[serde(default)]
334 pub observers: Option<ObserverConfig>,
335}
336
337#[cfg(feature = "observers")]
338fn default_observers_enabled() -> bool {
339 true
340}
341
342#[cfg(feature = "observers")]
343fn default_poll_interval_ms() -> u64 {
344 100
345}
346
347#[cfg(feature = "observers")]
348fn default_batch_size() -> usize {
349 100
350}
351
352#[cfg(feature = "observers")]
353fn default_channel_capacity() -> usize {
354 1000
355}
356
357#[cfg(feature = "observers")]
358fn default_auto_reload() -> bool {
359 true
360}
361
362#[cfg(feature = "observers")]
363fn default_reload_interval_secs() -> u64 {
364 60
365}
366
367#[cfg(feature = "observers")]
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct ObserverConfig {
371 #[serde(default = "default_observers_enabled")]
373 pub enabled: bool,
374
375 #[serde(default = "default_poll_interval_ms")]
377 pub poll_interval_ms: u64,
378
379 #[serde(default = "default_batch_size")]
381 pub batch_size: usize,
382
383 #[serde(default = "default_channel_capacity")]
385 pub channel_capacity: usize,
386
387 #[serde(default = "default_auto_reload")]
389 pub auto_reload: bool,
390
391 #[serde(default = "default_reload_interval_secs")]
393 pub reload_interval_secs: u64,
394}
395
396impl Default for ServerConfig {
397 fn default() -> Self {
398 Self {
399 schema_path: default_schema_path(),
400 database_url: default_database_url(),
401 bind_addr: default_bind_addr(),
402 cors_enabled: true,
403 cors_origins: Vec::new(),
404 compression_enabled: true,
405 tracing_enabled: true,
406 apq_enabled: true,
407 cache_enabled: true,
408 graphql_path: default_graphql_path(),
409 health_path: default_health_path(),
410 introspection_path: default_introspection_path(),
411 metrics_path: default_metrics_path(),
412 metrics_json_path: default_metrics_json_path(),
413 playground_path: default_playground_path(),
414 playground_enabled: false, playground_tool: PlaygroundTool::default(),
416 subscription_path: default_subscription_path(),
417 subscriptions_enabled: true,
418 metrics_enabled: false, metrics_token: None,
420 admin_api_enabled: false, admin_token: None,
422 introspection_enabled: false, introspection_require_auth: true, design_api_require_auth: true, pool_min_size: default_pool_min_size(),
426 pool_max_size: default_pool_max_size(),
427 pool_timeout_secs: default_pool_timeout(),
428 auth: None, tls: None, database_tls: None, rate_limiting: None, #[cfg(feature = "observers")]
433 observers: None, }
435 }
436}
437
438impl ServerConfig {
439 #[must_use]
445 pub fn is_production_mode() -> bool {
446 let env = std::env::var("FRAISEQL_ENV")
447 .unwrap_or_else(|_| "production".to_string())
448 .to_lowercase();
449 env != "development" && env != "dev"
450 }
451
452 pub fn validate(&self) -> Result<(), String> {
465 if self.metrics_enabled {
466 match &self.metrics_token {
467 None => {
468 return Err("metrics_enabled is true but metrics_token is not set. \
469 Set FRAISEQL_METRICS_TOKEN or metrics_token in config."
470 .to_string());
471 },
472 Some(token) if token.len() < 16 => {
473 return Err(
474 "metrics_token must be at least 16 characters for security.".to_string()
475 );
476 },
477 Some(_) => {},
478 }
479 }
480
481 if self.admin_api_enabled {
483 match &self.admin_token {
484 None => {
485 return Err("admin_api_enabled is true but admin_token is not set. \
486 Set FRAISEQL_ADMIN_TOKEN or admin_token in config."
487 .to_string());
488 },
489 Some(token) if token.len() < 32 => {
490 return Err(
491 "admin_token must be at least 32 characters for security.".to_string()
492 );
493 },
494 Some(_) => {},
495 }
496 }
497
498 if let Some(ref auth) = self.auth {
500 auth.validate().map_err(|e| e.to_string())?;
501 }
502
503 if let Some(ref tls) = self.tls {
505 if tls.enabled {
506 if !tls.cert_path.exists() {
507 return Err(format!(
508 "TLS enabled but certificate file not found: {}",
509 tls.cert_path.display()
510 ));
511 }
512 if !tls.key_path.exists() {
513 return Err(format!(
514 "TLS enabled but key file not found: {}",
515 tls.key_path.display()
516 ));
517 }
518
519 if !["1.2", "1.3"].contains(&tls.min_version.as_str()) {
521 return Err("TLS min_version must be '1.2' or '1.3'".to_string());
522 }
523
524 if tls.require_client_cert {
526 if let Some(ref ca_path) = tls.client_ca_path {
527 if !ca_path.exists() {
528 return Err(format!("Client CA file not found: {}", ca_path.display()));
529 }
530 } else {
531 return Err(
532 "require_client_cert is true but client_ca_path is not set".to_string()
533 );
534 }
535 }
536 }
537 }
538
539 if let Some(ref db_tls) = self.database_tls {
541 if ![
543 "disable",
544 "allow",
545 "prefer",
546 "require",
547 "verify-ca",
548 "verify-full",
549 ]
550 .contains(&db_tls.postgres_ssl_mode.as_str())
551 {
552 return Err("Invalid postgres_ssl_mode. Must be one of: \
553 disable, allow, prefer, require, verify-ca, verify-full"
554 .to_string());
555 }
556
557 if let Some(ref ca_path) = db_tls.ca_bundle_path {
559 if !ca_path.exists() {
560 return Err(format!("CA bundle file not found: {}", ca_path.display()));
561 }
562 }
563 }
564
565 if Self::is_production_mode() {
567 if self.playground_enabled {
569 return Err("playground_enabled is true in production mode. \
570 Disable the playground or set FRAISEQL_ENV=development. \
571 The playground exposes sensitive schema information."
572 .to_string());
573 }
574
575 if self.cors_enabled && self.cors_origins.is_empty() {
577 return Err("cors_enabled is true but cors_origins is empty in production mode. \
578 This allows requests from ANY origin, which is a security risk. \
579 Explicitly configure cors_origins with your allowed domains, \
580 or disable CORS and set FRAISEQL_ENV=development to bypass this check."
581 .to_string());
582 }
583 }
584
585 Ok(())
586 }
587
588 #[must_use]
590 pub fn auth_enabled(&self) -> bool {
591 self.auth.is_some()
592 }
593}
594
595fn default_schema_path() -> PathBuf {
596 PathBuf::from("schema.compiled.json")
597}
598
599fn default_database_url() -> String {
600 "postgresql://localhost/fraiseql".to_string()
601}
602
603fn default_bind_addr() -> SocketAddr {
604 "127.0.0.1:8000".parse().unwrap()
605}
606
607fn default_true() -> bool {
608 true
609}
610
611fn default_graphql_path() -> String {
612 "/graphql".to_string()
613}
614
615fn default_health_path() -> String {
616 "/health".to_string()
617}
618
619fn default_introspection_path() -> String {
620 "/introspection".to_string()
621}
622
623fn default_metrics_path() -> String {
624 "/metrics".to_string()
625}
626
627fn default_metrics_json_path() -> String {
628 "/metrics/json".to_string()
629}
630
631fn default_playground_path() -> String {
632 "/playground".to_string()
633}
634
635fn default_subscription_path() -> String {
636 "/ws".to_string()
637}
638
639fn default_pool_min_size() -> usize {
640 5
641}
642
643fn default_pool_max_size() -> usize {
644 20
645}
646
647fn default_pool_timeout() -> u64 {
648 30
649}
650
651fn default_tls_min_version() -> String {
652 "1.2".to_string()
653}
654
655fn default_postgres_ssl_mode() -> String {
656 "prefer".to_string()
657}
658
659fn default_redis_ssl() -> bool {
660 false
661}
662
663fn default_clickhouse_https() -> bool {
664 false
665}
666
667fn default_elasticsearch_https() -> bool {
668 false
669}
670
671fn default_verify_certs() -> bool {
672 true
673}
674
675fn default_rate_limiting_enabled() -> bool {
676 true
677}
678
679fn default_rate_limit_rps_per_ip() -> u32 {
680 100
681}
682
683fn default_rate_limit_rps_per_user() -> u32 {
684 1000
685}
686
687fn default_rate_limit_burst_size() -> u32 {
688 500
689}
690
691fn default_rate_limit_cleanup_interval() -> u64 {
692 300
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698
699 #[test]
700 fn test_default_config() {
701 let config = ServerConfig::default();
702 assert_eq!(config.schema_path, PathBuf::from("schema.compiled.json"));
703 assert_eq!(config.database_url, "postgresql://localhost/fraiseql");
704 assert_eq!(config.graphql_path, "/graphql");
705 assert_eq!(config.health_path, "/health");
706 assert_eq!(config.metrics_path, "/metrics");
707 assert_eq!(config.metrics_json_path, "/metrics/json");
708 assert!(config.cors_enabled);
709 assert!(config.compression_enabled);
710 }
711
712 #[test]
713 fn test_default_config_metrics_disabled() {
714 let config = ServerConfig::default();
715 assert!(!config.metrics_enabled, "Metrics should be disabled by default for security");
716 assert!(config.metrics_token.is_none());
717 }
718
719 #[test]
720 fn test_config_with_custom_database_url() {
721 let config = ServerConfig {
722 database_url: "postgresql://user:pass@db.example.com/mydb".to_string(),
723 ..ServerConfig::default()
724 };
725 assert_eq!(config.database_url, "postgresql://user:pass@db.example.com/mydb");
726 }
727
728 #[test]
729 fn test_default_pool_config() {
730 let config = ServerConfig::default();
731 assert_eq!(config.pool_min_size, 5);
732 assert_eq!(config.pool_max_size, 20);
733 assert_eq!(config.pool_timeout_secs, 30);
734 }
735
736 #[test]
737 fn test_config_with_custom_pool_size() {
738 let config = ServerConfig {
739 pool_min_size: 2,
740 pool_max_size: 50,
741 pool_timeout_secs: 60,
742 ..ServerConfig::default()
743 };
744 assert_eq!(config.pool_min_size, 2);
745 assert_eq!(config.pool_max_size, 50);
746 assert_eq!(config.pool_timeout_secs, 60);
747 }
748
749 #[test]
750 fn test_validate_metrics_disabled_ok() {
751 let config = ServerConfig {
752 cors_enabled: false,
753 ..ServerConfig::default()
754 };
755 assert!(config.validate().is_ok());
756 }
757
758 #[test]
759 fn test_validate_metrics_enabled_without_token_fails() {
760 let config = ServerConfig {
761 metrics_enabled: true,
762 metrics_token: None,
763 ..ServerConfig::default()
764 };
765 let result = config.validate();
766 assert!(result.is_err());
767 assert!(result.unwrap_err().contains("metrics_token is not set"));
768 }
769
770 #[test]
771 fn test_validate_metrics_enabled_with_short_token_fails() {
772 let config = ServerConfig {
773 metrics_enabled: true,
774 metrics_token: Some("short".to_string()), ..ServerConfig::default()
776 };
777 let result = config.validate();
778 assert!(result.is_err());
779 assert!(result.unwrap_err().contains("at least 16 characters"));
780 }
781
782 #[test]
783 fn test_validate_metrics_enabled_with_valid_token_ok() {
784 let config = ServerConfig {
785 metrics_enabled: true,
786 metrics_token: Some("a-secure-token-that-is-long-enough".to_string()),
787 cors_enabled: false,
788 ..ServerConfig::default()
789 };
790 assert!(config.validate().is_ok());
791 }
792
793 #[test]
794 fn test_default_subscription_config() {
795 let config = ServerConfig::default();
796 assert_eq!(config.subscription_path, "/ws");
797 assert!(config.subscriptions_enabled);
798 }
799
800 #[test]
801 fn test_subscription_config_with_custom_path() {
802 let config = ServerConfig {
803 subscription_path: "/subscriptions".to_string(),
804 ..ServerConfig::default()
805 };
806 assert_eq!(config.subscription_path, "/subscriptions");
807 assert!(config.subscriptions_enabled);
808 }
809
810 #[test]
811 fn test_subscriptions_can_be_disabled() {
812 let config = ServerConfig {
813 subscriptions_enabled: false,
814 ..ServerConfig::default()
815 };
816 assert!(!config.subscriptions_enabled);
817 assert_eq!(config.subscription_path, "/ws");
818 }
819
820 #[test]
821 fn test_subscription_path_serialization() {
822 let config = ServerConfig::default();
823 let json = serde_json::to_string(&config).expect("serialize should work");
824 let restored: ServerConfig = serde_json::from_str(&json).expect("deserialize should work");
825
826 assert_eq!(restored.subscription_path, config.subscription_path);
827 assert_eq!(restored.subscriptions_enabled, config.subscriptions_enabled);
828 }
829
830 #[test]
831 fn test_subscription_config_with_partial_toml() {
832 let toml_str = r#"
833 subscription_path = "/graphql-ws"
834 subscriptions_enabled = false
835 "#;
836
837 let decoded: ServerConfig = toml::from_str(toml_str).expect("decode should work");
838 assert_eq!(decoded.subscription_path, "/graphql-ws");
839 assert!(!decoded.subscriptions_enabled);
840 }
841
842 #[test]
843 fn test_tls_config_defaults() {
844 let config = ServerConfig::default();
845 assert!(config.tls.is_none());
846 assert!(config.database_tls.is_none());
847 }
848
849 #[test]
850 fn test_database_tls_config_defaults() {
851 let db_tls = DatabaseTlsConfig {
852 postgres_ssl_mode: "prefer".to_string(),
853 redis_ssl: false,
854 clickhouse_https: false,
855 elasticsearch_https: false,
856 verify_certificates: true,
857 ca_bundle_path: None,
858 };
859
860 assert_eq!(db_tls.postgres_ssl_mode, "prefer");
861 assert!(!db_tls.redis_ssl);
862 assert!(!db_tls.clickhouse_https);
863 assert!(!db_tls.elasticsearch_https);
864 assert!(db_tls.verify_certificates);
865 }
866
867 #[test]
868 fn test_tls_server_config_fields() {
869 let tls = TlsServerConfig {
870 enabled: true,
871 cert_path: PathBuf::from("/etc/fraiseql/cert.pem"),
872 key_path: PathBuf::from("/etc/fraiseql/key.pem"),
873 require_client_cert: false,
874 client_ca_path: None,
875 min_version: "1.3".to_string(),
876 };
877
878 assert!(tls.enabled);
879 assert_eq!(tls.cert_path, PathBuf::from("/etc/fraiseql/cert.pem"));
880 assert_eq!(tls.key_path, PathBuf::from("/etc/fraiseql/key.pem"));
881 assert!(!tls.require_client_cert);
882 assert_eq!(tls.min_version, "1.3");
883 }
884
885 #[test]
886 fn test_validate_tls_enabled_without_cert() {
887 let config = ServerConfig {
888 tls: Some(TlsServerConfig {
889 enabled: true,
890 cert_path: PathBuf::from("/nonexistent/cert.pem"),
891 key_path: PathBuf::from("/etc/fraiseql/key.pem"),
892 require_client_cert: false,
893 client_ca_path: None,
894 min_version: "1.2".to_string(),
895 }),
896 ..ServerConfig::default()
897 };
898
899 let result = config.validate();
900 assert!(result.is_err());
901 assert!(result.unwrap_err().contains("certificate file not found"));
902 }
903
904 #[test]
905 fn test_validate_tls_invalid_min_version() {
906 let cert_path = PathBuf::from("/tmp/test_cert.pem");
908 let key_path = PathBuf::from("/tmp/test_key.pem");
909 std::fs::write(&cert_path, "test").ok();
910 std::fs::write(&key_path, "test").ok();
911
912 let config = ServerConfig {
913 tls: Some(TlsServerConfig {
914 enabled: true,
915 cert_path,
916 key_path,
917 require_client_cert: false,
918 client_ca_path: None,
919 min_version: "1.1".to_string(),
920 }),
921 ..ServerConfig::default()
922 };
923
924 let result = config.validate();
925 assert!(result.is_err());
926 assert!(result.unwrap_err().contains("min_version must be"));
927 }
928
929 #[test]
930 fn test_validate_database_tls_invalid_postgres_ssl_mode() {
931 let config = ServerConfig {
932 database_tls: Some(DatabaseTlsConfig {
933 postgres_ssl_mode: "invalid_mode".to_string(),
934 redis_ssl: false,
935 clickhouse_https: false,
936 elasticsearch_https: false,
937 verify_certificates: true,
938 ca_bundle_path: None,
939 }),
940 ..ServerConfig::default()
941 };
942
943 let result = config.validate();
944 assert!(result.is_err());
945 assert!(result.unwrap_err().contains("Invalid postgres_ssl_mode"));
946 }
947
948 #[test]
949 fn test_validate_tls_requires_client_ca() {
950 let cert_path = PathBuf::from("/tmp/test_cert2.pem");
952 let key_path = PathBuf::from("/tmp/test_key2.pem");
953 std::fs::write(&cert_path, "test").ok();
954 std::fs::write(&key_path, "test").ok();
955
956 let config = ServerConfig {
957 tls: Some(TlsServerConfig {
958 enabled: true,
959 cert_path,
960 key_path,
961 require_client_cert: true,
962 client_ca_path: None,
963 min_version: "1.3".to_string(),
964 }),
965 ..ServerConfig::default()
966 };
967
968 let result = config.validate();
969 assert!(result.is_err());
970 assert!(result.unwrap_err().contains("client_ca_path is not set"));
971 }
972
973 #[test]
974 fn test_database_tls_serialization() {
975 let db_tls = DatabaseTlsConfig {
976 postgres_ssl_mode: "require".to_string(),
977 redis_ssl: true,
978 clickhouse_https: true,
979 elasticsearch_https: true,
980 verify_certificates: true,
981 ca_bundle_path: Some(PathBuf::from("/etc/ssl/certs/ca-bundle.crt")),
982 };
983
984 let json = serde_json::to_string(&db_tls).expect("serialize should work");
985 let restored: DatabaseTlsConfig =
986 serde_json::from_str(&json).expect("deserialize should work");
987
988 assert_eq!(restored.postgres_ssl_mode, db_tls.postgres_ssl_mode);
989 assert_eq!(restored.redis_ssl, db_tls.redis_ssl);
990 assert_eq!(restored.clickhouse_https, db_tls.clickhouse_https);
991 assert_eq!(restored.elasticsearch_https, db_tls.elasticsearch_https);
992 assert_eq!(restored.ca_bundle_path, db_tls.ca_bundle_path);
993 }
994
995 #[test]
996 fn test_admin_api_disabled_by_default() {
997 let config = ServerConfig::default();
998 assert!(
999 !config.admin_api_enabled,
1000 "Admin API should be disabled by default for security"
1001 );
1002 assert!(config.admin_token.is_none());
1003 }
1004
1005 #[test]
1006 fn test_validate_admin_api_enabled_without_token_fails() {
1007 let config = ServerConfig {
1008 admin_api_enabled: true,
1009 admin_token: None,
1010 ..ServerConfig::default()
1011 };
1012 let result = config.validate();
1013 assert!(result.is_err());
1014 assert!(result.unwrap_err().contains("admin_token is not set"));
1015 }
1016
1017 #[test]
1018 fn test_validate_admin_api_enabled_with_short_token_fails() {
1019 let config = ServerConfig {
1020 admin_api_enabled: true,
1021 admin_token: Some("short".to_string()), ..ServerConfig::default()
1023 };
1024 let result = config.validate();
1025 assert!(result.is_err());
1026 assert!(result.unwrap_err().contains("at least 32 characters"));
1027 }
1028
1029 #[test]
1030 fn test_validate_admin_api_enabled_with_valid_token_ok() {
1031 let config = ServerConfig {
1032 admin_api_enabled: true,
1033 admin_token: Some("a-very-secure-admin-token-that-is-long-enough".to_string()),
1034 cors_enabled: false,
1035 ..ServerConfig::default()
1036 };
1037 assert!(config.validate().is_ok());
1038 }
1039}