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 = "default_true")]
319 pub require_json_content_type: bool,
320
321 #[serde(default = "default_max_request_body_bytes")]
326 pub max_request_body_bytes: usize,
327
328 #[serde(default)]
343 pub rate_limiting: Option<RateLimitingConfig>,
344
345 #[cfg(feature = "observers")]
347 #[serde(default)]
348 pub observers: Option<ObserverConfig>,
349}
350
351#[cfg(feature = "observers")]
352fn default_observers_enabled() -> bool {
353 true
354}
355
356#[cfg(feature = "observers")]
357fn default_poll_interval_ms() -> u64 {
358 100
359}
360
361#[cfg(feature = "observers")]
362fn default_batch_size() -> usize {
363 100
364}
365
366#[cfg(feature = "observers")]
367fn default_channel_capacity() -> usize {
368 1000
369}
370
371#[cfg(feature = "observers")]
372fn default_auto_reload() -> bool {
373 true
374}
375
376#[cfg(feature = "observers")]
377fn default_reload_interval_secs() -> u64 {
378 60
379}
380
381#[cfg(feature = "observers")]
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct ObserverConfig {
385 #[serde(default = "default_observers_enabled")]
387 pub enabled: bool,
388
389 #[serde(default = "default_poll_interval_ms")]
391 pub poll_interval_ms: u64,
392
393 #[serde(default = "default_batch_size")]
395 pub batch_size: usize,
396
397 #[serde(default = "default_channel_capacity")]
399 pub channel_capacity: usize,
400
401 #[serde(default = "default_auto_reload")]
403 pub auto_reload: bool,
404
405 #[serde(default = "default_reload_interval_secs")]
407 pub reload_interval_secs: u64,
408}
409
410impl Default for ServerConfig {
411 fn default() -> Self {
412 Self {
413 schema_path: default_schema_path(),
414 database_url: default_database_url(),
415 bind_addr: default_bind_addr(),
416 cors_enabled: true,
417 cors_origins: Vec::new(),
418 compression_enabled: true,
419 tracing_enabled: true,
420 apq_enabled: true,
421 cache_enabled: true,
422 graphql_path: default_graphql_path(),
423 health_path: default_health_path(),
424 introspection_path: default_introspection_path(),
425 metrics_path: default_metrics_path(),
426 metrics_json_path: default_metrics_json_path(),
427 playground_path: default_playground_path(),
428 playground_enabled: false, playground_tool: PlaygroundTool::default(),
430 subscription_path: default_subscription_path(),
431 subscriptions_enabled: true,
432 metrics_enabled: false, metrics_token: None,
434 admin_api_enabled: false, admin_token: None,
436 introspection_enabled: false, introspection_require_auth: true, design_api_require_auth: true, pool_min_size: default_pool_min_size(),
440 pool_max_size: default_pool_max_size(),
441 pool_timeout_secs: default_pool_timeout(),
442 auth: None, tls: None, database_tls: None, require_json_content_type: true, max_request_body_bytes: default_max_request_body_bytes(), rate_limiting: None, #[cfg(feature = "observers")]
449 observers: None, }
451 }
452}
453
454impl ServerConfig {
455 #[must_use]
461 pub fn is_production_mode() -> bool {
462 let env = std::env::var("FRAISEQL_ENV")
463 .unwrap_or_else(|_| "production".to_string())
464 .to_lowercase();
465 env != "development" && env != "dev"
466 }
467
468 pub fn validate(&self) -> Result<(), String> {
481 if self.metrics_enabled {
482 match &self.metrics_token {
483 None => {
484 return Err("metrics_enabled is true but metrics_token is not set. \
485 Set FRAISEQL_METRICS_TOKEN or metrics_token in config."
486 .to_string());
487 },
488 Some(token) if token.len() < 16 => {
489 return Err(
490 "metrics_token must be at least 16 characters for security.".to_string()
491 );
492 },
493 Some(_) => {},
494 }
495 }
496
497 if self.admin_api_enabled {
499 match &self.admin_token {
500 None => {
501 return Err("admin_api_enabled is true but admin_token is not set. \
502 Set FRAISEQL_ADMIN_TOKEN or admin_token in config."
503 .to_string());
504 },
505 Some(token) if token.len() < 32 => {
506 return Err(
507 "admin_token must be at least 32 characters for security.".to_string()
508 );
509 },
510 Some(_) => {},
511 }
512 }
513
514 if let Some(ref auth) = self.auth {
516 auth.validate().map_err(|e| e.to_string())?;
517 }
518
519 if let Some(ref tls) = self.tls {
521 if tls.enabled {
522 if !tls.cert_path.exists() {
523 return Err(format!(
524 "TLS enabled but certificate file not found: {}",
525 tls.cert_path.display()
526 ));
527 }
528 if !tls.key_path.exists() {
529 return Err(format!(
530 "TLS enabled but key file not found: {}",
531 tls.key_path.display()
532 ));
533 }
534
535 if !["1.2", "1.3"].contains(&tls.min_version.as_str()) {
537 return Err("TLS min_version must be '1.2' or '1.3'".to_string());
538 }
539
540 if tls.require_client_cert {
542 if let Some(ref ca_path) = tls.client_ca_path {
543 if !ca_path.exists() {
544 return Err(format!("Client CA file not found: {}", ca_path.display()));
545 }
546 } else {
547 return Err(
548 "require_client_cert is true but client_ca_path is not set".to_string()
549 );
550 }
551 }
552 }
553 }
554
555 if let Some(ref db_tls) = self.database_tls {
557 if ![
559 "disable",
560 "allow",
561 "prefer",
562 "require",
563 "verify-ca",
564 "verify-full",
565 ]
566 .contains(&db_tls.postgres_ssl_mode.as_str())
567 {
568 return Err("Invalid postgres_ssl_mode. Must be one of: \
569 disable, allow, prefer, require, verify-ca, verify-full"
570 .to_string());
571 }
572
573 if let Some(ref ca_path) = db_tls.ca_bundle_path {
575 if !ca_path.exists() {
576 return Err(format!("CA bundle file not found: {}", ca_path.display()));
577 }
578 }
579 }
580
581 if Self::is_production_mode() {
583 if self.playground_enabled {
585 return Err("playground_enabled is true in production mode. \
586 Disable the playground or set FRAISEQL_ENV=development. \
587 The playground exposes sensitive schema information."
588 .to_string());
589 }
590
591 if self.cors_enabled && self.cors_origins.is_empty() {
593 return Err("cors_enabled is true but cors_origins is empty in production mode. \
594 This allows requests from ANY origin, which is a security risk. \
595 Explicitly configure cors_origins with your allowed domains, \
596 or disable CORS and set FRAISEQL_ENV=development to bypass this check."
597 .to_string());
598 }
599 }
600
601 Ok(())
602 }
603
604 #[must_use]
606 pub fn auth_enabled(&self) -> bool {
607 self.auth.is_some()
608 }
609}
610
611fn default_schema_path() -> PathBuf {
612 PathBuf::from("schema.compiled.json")
613}
614
615fn default_database_url() -> String {
616 "postgresql://localhost/fraiseql".to_string()
617}
618
619fn default_bind_addr() -> SocketAddr {
620 "127.0.0.1:8000".parse().unwrap()
621}
622
623fn default_true() -> bool {
624 true
625}
626
627fn default_max_request_body_bytes() -> usize {
629 1_048_576
630}
631
632fn default_graphql_path() -> String {
633 "/graphql".to_string()
634}
635
636fn default_health_path() -> String {
637 "/health".to_string()
638}
639
640fn default_introspection_path() -> String {
641 "/introspection".to_string()
642}
643
644fn default_metrics_path() -> String {
645 "/metrics".to_string()
646}
647
648fn default_metrics_json_path() -> String {
649 "/metrics/json".to_string()
650}
651
652fn default_playground_path() -> String {
653 "/playground".to_string()
654}
655
656fn default_subscription_path() -> String {
657 "/ws".to_string()
658}
659
660fn default_pool_min_size() -> usize {
661 5
662}
663
664fn default_pool_max_size() -> usize {
665 20
666}
667
668fn default_pool_timeout() -> u64 {
669 30
670}
671
672fn default_tls_min_version() -> String {
673 "1.2".to_string()
674}
675
676fn default_postgres_ssl_mode() -> String {
677 "prefer".to_string()
678}
679
680fn default_redis_ssl() -> bool {
681 false
682}
683
684fn default_clickhouse_https() -> bool {
685 false
686}
687
688fn default_elasticsearch_https() -> bool {
689 false
690}
691
692fn default_verify_certs() -> bool {
693 true
694}
695
696fn default_rate_limiting_enabled() -> bool {
697 true
698}
699
700fn default_rate_limit_rps_per_ip() -> u32 {
701 100
702}
703
704fn default_rate_limit_rps_per_user() -> u32 {
705 1000
706}
707
708fn default_rate_limit_burst_size() -> u32 {
709 500
710}
711
712fn default_rate_limit_cleanup_interval() -> u64 {
713 300
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719
720 #[test]
721 fn test_default_config() {
722 let config = ServerConfig::default();
723 assert_eq!(config.schema_path, PathBuf::from("schema.compiled.json"));
724 assert_eq!(config.database_url, "postgresql://localhost/fraiseql");
725 assert_eq!(config.graphql_path, "/graphql");
726 assert_eq!(config.health_path, "/health");
727 assert_eq!(config.metrics_path, "/metrics");
728 assert_eq!(config.metrics_json_path, "/metrics/json");
729 assert!(config.cors_enabled);
730 assert!(config.compression_enabled);
731 }
732
733 #[test]
734 fn test_default_config_metrics_disabled() {
735 let config = ServerConfig::default();
736 assert!(!config.metrics_enabled, "Metrics should be disabled by default for security");
737 assert!(config.metrics_token.is_none());
738 }
739
740 #[test]
741 fn test_config_with_custom_database_url() {
742 let config = ServerConfig {
743 database_url: "postgresql://user:pass@db.example.com/mydb".to_string(),
744 ..ServerConfig::default()
745 };
746 assert_eq!(config.database_url, "postgresql://user:pass@db.example.com/mydb");
747 }
748
749 #[test]
750 fn test_default_pool_config() {
751 let config = ServerConfig::default();
752 assert_eq!(config.pool_min_size, 5);
753 assert_eq!(config.pool_max_size, 20);
754 assert_eq!(config.pool_timeout_secs, 30);
755 }
756
757 #[test]
758 fn test_config_with_custom_pool_size() {
759 let config = ServerConfig {
760 pool_min_size: 2,
761 pool_max_size: 50,
762 pool_timeout_secs: 60,
763 ..ServerConfig::default()
764 };
765 assert_eq!(config.pool_min_size, 2);
766 assert_eq!(config.pool_max_size, 50);
767 assert_eq!(config.pool_timeout_secs, 60);
768 }
769
770 #[test]
771 fn test_validate_metrics_disabled_ok() {
772 let config = ServerConfig {
773 cors_enabled: false,
774 ..ServerConfig::default()
775 };
776 assert!(config.validate().is_ok());
777 }
778
779 #[test]
780 fn test_validate_metrics_enabled_without_token_fails() {
781 let config = ServerConfig {
782 metrics_enabled: true,
783 metrics_token: None,
784 ..ServerConfig::default()
785 };
786 let result = config.validate();
787 assert!(result.is_err());
788 assert!(result.unwrap_err().contains("metrics_token is not set"));
789 }
790
791 #[test]
792 fn test_validate_metrics_enabled_with_short_token_fails() {
793 let config = ServerConfig {
794 metrics_enabled: true,
795 metrics_token: Some("short".to_string()), ..ServerConfig::default()
797 };
798 let result = config.validate();
799 assert!(result.is_err());
800 assert!(result.unwrap_err().contains("at least 16 characters"));
801 }
802
803 #[test]
804 fn test_validate_metrics_enabled_with_valid_token_ok() {
805 let config = ServerConfig {
806 metrics_enabled: true,
807 metrics_token: Some("a-secure-token-that-is-long-enough".to_string()),
808 cors_enabled: false,
809 ..ServerConfig::default()
810 };
811 assert!(config.validate().is_ok());
812 }
813
814 #[test]
815 fn test_default_subscription_config() {
816 let config = ServerConfig::default();
817 assert_eq!(config.subscription_path, "/ws");
818 assert!(config.subscriptions_enabled);
819 }
820
821 #[test]
822 fn test_subscription_config_with_custom_path() {
823 let config = ServerConfig {
824 subscription_path: "/subscriptions".to_string(),
825 ..ServerConfig::default()
826 };
827 assert_eq!(config.subscription_path, "/subscriptions");
828 assert!(config.subscriptions_enabled);
829 }
830
831 #[test]
832 fn test_subscriptions_can_be_disabled() {
833 let config = ServerConfig {
834 subscriptions_enabled: false,
835 ..ServerConfig::default()
836 };
837 assert!(!config.subscriptions_enabled);
838 assert_eq!(config.subscription_path, "/ws");
839 }
840
841 #[test]
842 fn test_subscription_path_serialization() {
843 let config = ServerConfig::default();
844 let json = serde_json::to_string(&config).expect(
845 "ServerConfig derives Serialize with serializable fields; serialization is infallible",
846 );
847 let restored: ServerConfig = serde_json::from_str(&json).expect(
848 "ServerConfig roundtrip: deserialization of just-serialized data is infallible",
849 );
850
851 assert_eq!(restored.subscription_path, config.subscription_path);
852 assert_eq!(restored.subscriptions_enabled, config.subscriptions_enabled);
853 }
854
855 #[test]
856 fn test_subscription_config_with_partial_toml() {
857 let toml_str = r#"
858 subscription_path = "/graphql-ws"
859 subscriptions_enabled = false
860 "#;
861
862 let decoded: ServerConfig = toml::from_str(toml_str).expect(
863 "TOML config parsing: valid TOML syntax with expected fields deserializes correctly",
864 );
865 assert_eq!(decoded.subscription_path, "/graphql-ws");
866 assert!(!decoded.subscriptions_enabled);
867 }
868
869 #[test]
870 fn test_tls_config_defaults() {
871 let config = ServerConfig::default();
872 assert!(config.tls.is_none());
873 assert!(config.database_tls.is_none());
874 }
875
876 #[test]
877 fn test_database_tls_config_defaults() {
878 let db_tls = DatabaseTlsConfig {
879 postgres_ssl_mode: "prefer".to_string(),
880 redis_ssl: false,
881 clickhouse_https: false,
882 elasticsearch_https: false,
883 verify_certificates: true,
884 ca_bundle_path: None,
885 };
886
887 assert_eq!(db_tls.postgres_ssl_mode, "prefer");
888 assert!(!db_tls.redis_ssl);
889 assert!(!db_tls.clickhouse_https);
890 assert!(!db_tls.elasticsearch_https);
891 assert!(db_tls.verify_certificates);
892 }
893
894 #[test]
895 fn test_tls_server_config_fields() {
896 let tls = TlsServerConfig {
897 enabled: true,
898 cert_path: PathBuf::from("/etc/fraiseql/cert.pem"),
899 key_path: PathBuf::from("/etc/fraiseql/key.pem"),
900 require_client_cert: false,
901 client_ca_path: None,
902 min_version: "1.3".to_string(),
903 };
904
905 assert!(tls.enabled);
906 assert_eq!(tls.cert_path, PathBuf::from("/etc/fraiseql/cert.pem"));
907 assert_eq!(tls.key_path, PathBuf::from("/etc/fraiseql/key.pem"));
908 assert!(!tls.require_client_cert);
909 assert_eq!(tls.min_version, "1.3");
910 }
911
912 #[test]
913 fn test_validate_tls_enabled_without_cert() {
914 let config = ServerConfig {
915 tls: Some(TlsServerConfig {
916 enabled: true,
917 cert_path: PathBuf::from("/nonexistent/cert.pem"),
918 key_path: PathBuf::from("/etc/fraiseql/key.pem"),
919 require_client_cert: false,
920 client_ca_path: None,
921 min_version: "1.2".to_string(),
922 }),
923 ..ServerConfig::default()
924 };
925
926 let result = config.validate();
927 assert!(result.is_err());
928 assert!(result.unwrap_err().contains("certificate file not found"));
929 }
930
931 #[test]
932 fn test_validate_tls_invalid_min_version() {
933 let cert_path = PathBuf::from("/tmp/test_cert.pem");
935 let key_path = PathBuf::from("/tmp/test_key.pem");
936 std::fs::write(&cert_path, "test").ok();
937 std::fs::write(&key_path, "test").ok();
938
939 let config = ServerConfig {
940 tls: Some(TlsServerConfig {
941 enabled: true,
942 cert_path,
943 key_path,
944 require_client_cert: false,
945 client_ca_path: None,
946 min_version: "1.1".to_string(),
947 }),
948 ..ServerConfig::default()
949 };
950
951 let result = config.validate();
952 assert!(result.is_err());
953 assert!(result.unwrap_err().contains("min_version must be"));
954 }
955
956 #[test]
957 fn test_validate_database_tls_invalid_postgres_ssl_mode() {
958 let config = ServerConfig {
959 database_tls: Some(DatabaseTlsConfig {
960 postgres_ssl_mode: "invalid_mode".to_string(),
961 redis_ssl: false,
962 clickhouse_https: false,
963 elasticsearch_https: false,
964 verify_certificates: true,
965 ca_bundle_path: None,
966 }),
967 ..ServerConfig::default()
968 };
969
970 let result = config.validate();
971 assert!(result.is_err());
972 assert!(result.unwrap_err().contains("Invalid postgres_ssl_mode"));
973 }
974
975 #[test]
976 fn test_validate_tls_requires_client_ca() {
977 let cert_path = PathBuf::from("/tmp/test_cert2.pem");
979 let key_path = PathBuf::from("/tmp/test_key2.pem");
980 std::fs::write(&cert_path, "test").ok();
981 std::fs::write(&key_path, "test").ok();
982
983 let config = ServerConfig {
984 tls: Some(TlsServerConfig {
985 enabled: true,
986 cert_path,
987 key_path,
988 require_client_cert: true,
989 client_ca_path: None,
990 min_version: "1.3".to_string(),
991 }),
992 ..ServerConfig::default()
993 };
994
995 let result = config.validate();
996 assert!(result.is_err());
997 assert!(result.unwrap_err().contains("client_ca_path is not set"));
998 }
999
1000 #[test]
1001 fn test_database_tls_serialization() {
1002 let db_tls = DatabaseTlsConfig {
1003 postgres_ssl_mode: "require".to_string(),
1004 redis_ssl: true,
1005 clickhouse_https: true,
1006 elasticsearch_https: true,
1007 verify_certificates: true,
1008 ca_bundle_path: Some(PathBuf::from("/etc/ssl/certs/ca-bundle.crt")),
1009 };
1010
1011 let json = serde_json::to_string(&db_tls)
1012 .expect("DatabaseTlsConfig derives Serialize with serializable fields; serialization is infallible");
1013 let restored: DatabaseTlsConfig = serde_json::from_str(&json).expect(
1014 "DatabaseTlsConfig roundtrip: deserialization of just-serialized data is infallible",
1015 );
1016
1017 assert_eq!(restored.postgres_ssl_mode, db_tls.postgres_ssl_mode);
1018 assert_eq!(restored.redis_ssl, db_tls.redis_ssl);
1019 assert_eq!(restored.clickhouse_https, db_tls.clickhouse_https);
1020 assert_eq!(restored.elasticsearch_https, db_tls.elasticsearch_https);
1021 assert_eq!(restored.ca_bundle_path, db_tls.ca_bundle_path);
1022 }
1023
1024 #[test]
1025 fn test_admin_api_disabled_by_default() {
1026 let config = ServerConfig::default();
1027 assert!(
1028 !config.admin_api_enabled,
1029 "Admin API should be disabled by default for security"
1030 );
1031 assert!(config.admin_token.is_none());
1032 }
1033
1034 #[test]
1035 fn test_validate_admin_api_enabled_without_token_fails() {
1036 let config = ServerConfig {
1037 admin_api_enabled: true,
1038 admin_token: None,
1039 ..ServerConfig::default()
1040 };
1041 let result = config.validate();
1042 assert!(result.is_err());
1043 assert!(result.unwrap_err().contains("admin_token is not set"));
1044 }
1045
1046 #[test]
1047 fn test_validate_admin_api_enabled_with_short_token_fails() {
1048 let config = ServerConfig {
1049 admin_api_enabled: true,
1050 admin_token: Some("short".to_string()), ..ServerConfig::default()
1052 };
1053 let result = config.validate();
1054 assert!(result.is_err());
1055 assert!(result.unwrap_err().contains("at least 32 characters"));
1056 }
1057
1058 #[test]
1059 fn test_validate_admin_api_enabled_with_valid_token_ok() {
1060 let config = ServerConfig {
1061 admin_api_enabled: true,
1062 admin_token: Some("a-very-secure-admin-token-that-is-long-enough".to_string()),
1063 cors_enabled: false,
1064 ..ServerConfig::default()
1065 };
1066 assert!(config.validate().is_ok());
1067 }
1068}