Skip to main content

fraiseql_server/
server_config.rs

1//! Server configuration.
2
3use std::{net::SocketAddr, path::PathBuf};
4
5use fraiseql_core::security::OidcConfig;
6use serde::{Deserialize, Serialize};
7
8/// GraphQL IDE/playground tool to use.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
10#[serde(rename_all = "kebab-case")]
11pub enum PlaygroundTool {
12    /// GraphiQL - the classic GraphQL IDE.
13    GraphiQL,
14    /// Apollo Sandbox - Apollo's embeddable GraphQL IDE (default).
15    ///
16    /// Apollo Sandbox offers a better UX with features like:
17    /// - Query collections and history
18    /// - Schema documentation explorer
19    /// - Variables and headers panels
20    /// - Operation tracing
21    #[default]
22    ApolloSandbox,
23}
24
25/// TLS server configuration for HTTPS and secure connections.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct TlsServerConfig {
28    /// Enable TLS for HTTP/gRPC endpoints.
29    pub enabled: bool,
30
31    /// Path to TLS certificate file (PEM format).
32    pub cert_path: PathBuf,
33
34    /// Path to TLS private key file (PEM format).
35    pub key_path: PathBuf,
36
37    /// Require client certificate (mTLS) for all connections.
38    #[serde(default)]
39    pub require_client_cert: bool,
40
41    /// Path to CA certificate for validating client certificates (for mTLS).
42    #[serde(default)]
43    pub client_ca_path: Option<PathBuf>,
44
45    /// Minimum TLS version ("1.2" or "1.3", default: "1.2").
46    #[serde(default = "default_tls_min_version")]
47    pub min_version: String,
48}
49
50/// Database TLS configuration for encrypted database connections.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct DatabaseTlsConfig {
53    /// PostgreSQL SSL mode: disable, allow, prefer, require, verify-ca, verify-full.
54    #[serde(default = "default_postgres_ssl_mode")]
55    pub postgres_ssl_mode: String,
56
57    /// Enable TLS for Redis connections (use rediss:// protocol).
58    #[serde(default = "default_redis_ssl")]
59    pub redis_ssl: bool,
60
61    /// Enable HTTPS for ClickHouse connections.
62    #[serde(default = "default_clickhouse_https")]
63    pub clickhouse_https: bool,
64
65    /// Enable HTTPS for Elasticsearch connections.
66    #[serde(default = "default_elasticsearch_https")]
67    pub elasticsearch_https: bool,
68
69    /// Verify server certificates for HTTPS connections.
70    #[serde(default = "default_verify_certs")]
71    pub verify_certificates: bool,
72
73    /// Path to CA certificate bundle for verifying server certificates.
74    #[serde(default)]
75    pub ca_bundle_path: Option<PathBuf>,
76}
77
78/// Rate limiting configuration for GraphQL requests.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct RateLimitingConfig {
81    /// Enable rate limiting (default: true for security).
82    #[serde(default = "default_rate_limiting_enabled")]
83    pub enabled: bool,
84
85    /// Requests per second per IP address.
86    #[serde(default = "default_rate_limit_rps_per_ip")]
87    pub rps_per_ip: u32,
88
89    /// Requests per second per authenticated user.
90    #[serde(default = "default_rate_limit_rps_per_user")]
91    pub rps_per_user: u32,
92
93    /// Burst capacity (maximum accumulated tokens).
94    #[serde(default = "default_rate_limit_burst_size")]
95    pub burst_size: u32,
96
97    /// Cleanup interval for stale entries (seconds).
98    #[serde(default = "default_rate_limit_cleanup_interval")]
99    pub cleanup_interval_secs: u64,
100}
101
102/// Server configuration.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct ServerConfig {
105    /// Path to compiled schema JSON file.
106    #[serde(default = "default_schema_path")]
107    pub schema_path: PathBuf,
108
109    /// Database connection URL (PostgreSQL, MySQL, SQLite, SQL Server).
110    #[serde(default = "default_database_url")]
111    pub database_url: String,
112
113    /// Server bind address.
114    #[serde(default = "default_bind_addr")]
115    pub bind_addr: SocketAddr,
116
117    /// Enable CORS.
118    #[serde(default = "default_true")]
119    pub cors_enabled: bool,
120
121    /// CORS allowed origins (if empty, allows all).
122    #[serde(default)]
123    pub cors_origins: Vec<String>,
124
125    /// Enable compression.
126    #[serde(default = "default_true")]
127    pub compression_enabled: bool,
128
129    /// Enable request tracing.
130    #[serde(default = "default_true")]
131    pub tracing_enabled: bool,
132
133    /// Enable APQ (Automatic Persisted Queries).
134    #[serde(default = "default_true")]
135    pub apq_enabled: bool,
136
137    /// Enable query caching.
138    #[serde(default = "default_true")]
139    pub cache_enabled: bool,
140
141    /// GraphQL endpoint path.
142    #[serde(default = "default_graphql_path")]
143    pub graphql_path: String,
144
145    /// Health check endpoint path.
146    #[serde(default = "default_health_path")]
147    pub health_path: String,
148
149    /// Introspection endpoint path.
150    #[serde(default = "default_introspection_path")]
151    pub introspection_path: String,
152
153    /// Metrics endpoint path (Prometheus format).
154    #[serde(default = "default_metrics_path")]
155    pub metrics_path: String,
156
157    /// Metrics JSON endpoint path.
158    #[serde(default = "default_metrics_json_path")]
159    pub metrics_json_path: String,
160
161    /// Playground (GraphQL IDE) endpoint path.
162    #[serde(default = "default_playground_path")]
163    pub playground_path: String,
164
165    /// Enable GraphQL playground/IDE (default: false for production safety).
166    ///
167    /// When enabled, serves a GraphQL IDE (GraphiQL or Apollo Sandbox)
168    /// at the configured `playground_path`.
169    ///
170    /// **Security**: Disabled by default for production safety. Set to true for development
171    /// environments only. The playground exposes schema information and can be a
172    /// reconnaissance vector for attackers.
173    #[serde(default)]
174    pub playground_enabled: bool,
175
176    /// Which GraphQL IDE to use.
177    ///
178    /// - `graphiql`: The classic GraphQL IDE (default)
179    /// - `apollo-sandbox`: Apollo's embeddable sandbox
180    #[serde(default)]
181    pub playground_tool: PlaygroundTool,
182
183    /// WebSocket endpoint path for GraphQL subscriptions.
184    #[serde(default = "default_subscription_path")]
185    pub subscription_path: String,
186
187    /// Enable GraphQL subscriptions over WebSocket.
188    ///
189    /// When enabled, provides graphql-ws (graphql-transport-ws) protocol
190    /// support for real-time subscription events.
191    #[serde(default = "default_true")]
192    pub subscriptions_enabled: bool,
193
194    /// Enable metrics endpoints.
195    ///
196    /// **Security**: Disabled by default for production safety.
197    /// When enabled, requires `metrics_token` to be set for authentication.
198    #[serde(default)]
199    pub metrics_enabled: bool,
200
201    /// Bearer token for metrics endpoint authentication.
202    ///
203    /// Required when `metrics_enabled` is true. Requests must include:
204    /// `Authorization: Bearer <token>`
205    ///
206    /// **Security**: Use a strong, random token (e.g., 32+ characters).
207    #[serde(default)]
208    pub metrics_token: Option<String>,
209
210    /// Enable admin API endpoints (default: false for production safety).
211    ///
212    /// **Security**: Disabled by default. When enabled, requires `admin_token` to be set.
213    /// Admin endpoints allow schema reloading, cache management, and config inspection.
214    #[serde(default)]
215    pub admin_api_enabled: bool,
216
217    /// Bearer token for admin API authentication.
218    ///
219    /// Required when `admin_api_enabled` is true. Requests must include:
220    /// `Authorization: Bearer <token>`
221    ///
222    /// **Security**: Use a strong, random token (minimum 32 characters).
223    /// This token grants access to sensitive operations like schema reloading.
224    #[serde(default)]
225    pub admin_token: Option<String>,
226
227    /// Enable introspection endpoint (default: false for production safety).
228    ///
229    /// **Security**: Disabled by default. When enabled, the introspection endpoint
230    /// exposes the complete GraphQL schema structure. Combined with `introspection_require_auth`,
231    /// you can optionally protect it with OIDC authentication.
232    #[serde(default)]
233    pub introspection_enabled: bool,
234
235    /// Require authentication for introspection endpoint (default: true).
236    ///
237    /// When true and OIDC is configured, introspection requires same auth as GraphQL endpoint.
238    /// When false, introspection is publicly accessible (use only in development).
239    #[serde(default = "default_true")]
240    pub introspection_require_auth: bool,
241
242    /// Require authentication for design audit API endpoints (default: true).
243    ///
244    /// Design audit endpoints expose system architecture and optimization opportunities.
245    /// When true and OIDC is configured, design endpoints require same auth as GraphQL endpoint.
246    /// When false, design endpoints are publicly accessible (use only in development).
247    #[serde(default = "default_true")]
248    pub design_api_require_auth: bool,
249
250    /// Database connection pool minimum size.
251    #[serde(default = "default_pool_min_size")]
252    pub pool_min_size: usize,
253
254    /// Database connection pool maximum size.
255    #[serde(default = "default_pool_max_size")]
256    pub pool_max_size: usize,
257
258    /// Database connection pool timeout in seconds.
259    #[serde(default = "default_pool_timeout")]
260    pub pool_timeout_secs: u64,
261
262    /// OIDC authentication configuration (optional).
263    ///
264    /// When set, enables JWT authentication using OIDC discovery.
265    /// Supports Auth0, Keycloak, Okta, Cognito, Azure AD, and any
266    /// OIDC-compliant provider.
267    ///
268    /// # Example (TOML)
269    ///
270    /// ```toml
271    /// [auth]
272    /// issuer = "https://your-tenant.auth0.com/"
273    /// audience = "your-api-identifier"
274    /// ```
275    #[serde(default)]
276    pub auth: Option<OidcConfig>,
277
278    /// TLS/SSL configuration for HTTPS and encrypted connections.
279    ///
280    /// When set, enables TLS enforcement for HTTP/gRPC endpoints and
281    /// optionally requires mutual TLS (mTLS) for client certificates.
282    ///
283    /// # Example (TOML)
284    ///
285    /// ```toml
286    /// [tls]
287    /// enabled = true
288    /// cert_path = "/etc/fraiseql/cert.pem"
289    /// key_path = "/etc/fraiseql/key.pem"
290    /// require_client_cert = false
291    /// min_version = "1.2"  # "1.2" or "1.3"
292    /// ```
293    #[serde(default)]
294    pub tls: Option<TlsServerConfig>,
295
296    /// Database TLS configuration.
297    ///
298    /// Enables TLS for database connections and configures
299    /// per-database TLS settings (PostgreSQL, Redis, ClickHouse, etc.).
300    ///
301    /// # Example (TOML)
302    ///
303    /// ```toml
304    /// [database_tls]
305    /// postgres_ssl_mode = "require"  # disable, allow, prefer, require, verify-ca, verify-full
306    /// redis_ssl = true               # Use rediss:// protocol
307    /// clickhouse_https = true         # Use HTTPS
308    /// elasticsearch_https = true      # Use HTTPS
309    /// verify_certificates = true      # Verify server certificates
310    /// ```
311    #[serde(default)]
312    pub database_tls: Option<DatabaseTlsConfig>,
313
314    /// Rate limiting configuration for GraphQL requests.
315    ///
316    /// When configured, enables per-IP and per-user rate limiting with token bucket algorithm.
317    /// Defaults to enabled with sensible per-IP limits for security-by-default.
318    ///
319    /// # Example (TOML)
320    ///
321    /// ```toml
322    /// [rate_limiting]
323    /// enabled = true
324    /// rps_per_ip = 100      # 100 requests/second per IP
325    /// rps_per_user = 1000   # 1000 requests/second per authenticated user
326    /// burst_size = 500      # Allow bursts up to 500 requests
327    /// ```
328    #[serde(default)]
329    pub rate_limiting: Option<RateLimitingConfig>,
330
331    /// Observer runtime configuration (optional, requires `observers` feature).
332    #[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/// Observer runtime configuration.
368#[cfg(feature = "observers")]
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct ObserverConfig {
371    /// Enable observer runtime (default: true).
372    #[serde(default = "default_observers_enabled")]
373    pub enabled: bool,
374
375    /// Poll interval for change log in milliseconds (default: 100).
376    #[serde(default = "default_poll_interval_ms")]
377    pub poll_interval_ms: u64,
378
379    /// Batch size for fetching change log entries (default: 100).
380    #[serde(default = "default_batch_size")]
381    pub batch_size: usize,
382
383    /// Channel capacity for event buffering (default: 1000).
384    #[serde(default = "default_channel_capacity")]
385    pub channel_capacity: usize,
386
387    /// Auto-reload observers on changes (default: true).
388    #[serde(default = "default_auto_reload")]
389    pub auto_reload: bool,
390
391    /// Reload interval in seconds (default: 60).
392    #[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, // Disabled by default for security
415            playground_tool: PlaygroundTool::default(),
416            subscription_path: default_subscription_path(),
417            subscriptions_enabled: true,
418            metrics_enabled: false, // Disabled by default for security
419            metrics_token: None,
420            admin_api_enabled: false, // Disabled by default for security
421            admin_token: None,
422            introspection_enabled: false, // Disabled by default for security
423            introspection_require_auth: true, // Require auth when enabled
424            design_api_require_auth: true, // Require auth for design endpoints
425            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,          // No auth by default
429            tls: None,           // TLS disabled by default
430            database_tls: None,  // Database TLS disabled by default
431            rate_limiting: None, // Rate limiting uses defaults
432            #[cfg(feature = "observers")]
433            observers: None, // Observers disabled by default
434        }
435    }
436}
437
438impl ServerConfig {
439    /// Check if running in production mode.
440    ///
441    /// Production mode is detected via `FRAISEQL_ENV` environment variable.
442    /// - `production` or `prod` (or any value other than `development`/`dev`) → production mode
443    /// - `development` or `dev` → development mode
444    #[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    /// Validate configuration.
453    ///
454    /// # Errors
455    ///
456    /// Returns error if:
457    /// - `metrics_enabled` is true but `metrics_token` is not set
458    /// - `metrics_token` is set but too short (< 16 characters)
459    /// - `auth` config is set but invalid (e.g., empty issuer)
460    /// - `tls` is enabled but cert or key path is missing
461    /// - TLS minimum version is invalid
462    /// - In production mode: `playground_enabled` is true
463    /// - In production mode: `cors_enabled` is true but `cors_origins` is empty
464    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        // Admin API validation
482        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        // Validate OIDC config if present
499        if let Some(ref auth) = self.auth {
500            auth.validate().map_err(|e| e.to_string())?;
501        }
502
503        // Validate TLS config if present and enabled
504        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                // Validate TLS version
520                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                // Validate mTLS config if required
525                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        // Validate database TLS config if present
540        if let Some(ref db_tls) = self.database_tls {
541            // Validate PostgreSQL SSL mode
542            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            // Validate CA bundle path if provided
558            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        // Production safety validation
566        if Self::is_production_mode() {
567            // Playground should be disabled in production
568            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            // CORS origins must be explicitly configured in production
576            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    /// Check if authentication is enabled.
589    #[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()), // < 16 chars
775            ..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        // Create temp cert and key files that exist
907        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        // Create temp cert and key files that exist
951        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()), // < 32 chars
1022            ..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}