1use std::net::SocketAddr;
7use std::path::PathBuf;
8use std::sync::Arc;
9use std::time::Duration;
10
11use axum::{
12 Router, middleware,
13 routing::{delete, get, post},
14};
15use tower::ServiceBuilder;
16use tower_http::{
17 cors::{Any, CorsLayer},
18 request_id::MakeRequestUuid,
19 trace::TraceLayer,
20};
21use tracing::info;
22
23use crate::auth::{
24 ApiKey, ApiKeyStore, AuthState, JwtConfig, Role, auth_middleware, local_mode_auth_middleware,
25};
26use crate::cleanup::spawn_cleanup_task;
27use crate::error::ConfigError;
28use crate::handlers::{
29 DefaultRetentionDays, admin_cleanup, create_key, dashboard_index, delete_baseline,
30 dependency_impact, get_baseline, get_latest_baseline, get_trend, health_check,
31 list_audit_events, list_baselines, list_fleet_alerts, list_keys, list_verdicts,
32 promote_baseline, record_dependency_event, revoke_key, static_asset, submit_verdict,
33 upload_baseline,
34};
35use crate::metrics::{metrics_handler, metrics_middleware, setup_metrics_recorder};
36use crate::oidc::{OidcConfig, OidcProvider, OidcRegistry};
37use crate::storage::fleet::{FleetStore, InMemoryFleetStore};
38use crate::storage::{
39 ArtifactStore, AuditStore, BaselineStore, InMemoryKeyStore, InMemoryStore, KeyStore,
40 ObjectArtifactStore, PostgresStore, SqliteKeyStore, SqliteStore,
41};
42use metrics_exporter_prometheus::PrometheusHandle;
43
44#[derive(Clone)]
46pub struct AppState {
47 pub store: Arc<dyn BaselineStore>,
49 pub audit: Arc<dyn AuditStore>,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
55pub enum StorageBackend {
56 #[default]
58 Memory,
59 Sqlite,
61 Postgres,
63}
64
65impl std::str::FromStr for StorageBackend {
66 type Err = String;
67
68 fn from_str(s: &str) -> Result<Self, Self::Err> {
69 match s.to_lowercase().as_str() {
70 "memory" => Ok(Self::Memory),
71 "sqlite" => Ok(Self::Sqlite),
72 "postgres" | "postgresql" => Ok(Self::Postgres),
73 _ => Err(format!("Unknown storage backend: {}", s)),
74 }
75 }
76}
77
78#[derive(Debug, Clone)]
99pub struct PostgresPoolConfig {
100 pub max_connections: u32,
102 pub min_connections: u32,
104 pub idle_timeout: Duration,
106 pub max_lifetime: Duration,
108 pub acquire_timeout: Duration,
110 pub statement_timeout: Duration,
113}
114
115impl Default for PostgresPoolConfig {
116 fn default() -> Self {
117 Self {
118 max_connections: 10,
119 min_connections: 2,
120 idle_timeout: Duration::from_secs(300),
121 max_lifetime: Duration::from_secs(1800),
122 acquire_timeout: Duration::from_secs(5),
123 statement_timeout: Duration::from_secs(30),
124 }
125 }
126}
127
128#[derive(Debug, Clone)]
130pub struct ApiKeyConfig {
131 pub key: String,
133 pub role: Role,
135 pub project: String,
137 pub benchmark_regex: Option<String>,
139}
140
141#[derive(Debug, Clone)]
143pub struct ServerConfig {
144 pub bind: SocketAddr,
146
147 pub storage_backend: StorageBackend,
149
150 pub sqlite_path: Option<PathBuf>,
152
153 pub postgres_url: Option<String>,
155
156 pub postgres_pool: PostgresPoolConfig,
158
159 pub artifacts_url: Option<String>,
161
162 pub api_keys: Vec<ApiKeyConfig>,
164
165 pub jwt: Option<JwtConfig>,
167
168 pub oidc_configs: Vec<OidcConfig>,
170
171 pub cors: bool,
173
174 pub timeout_seconds: u64,
176
177 pub local_mode: bool,
179
180 pub retention_days: u64,
182
183 pub cleanup_interval_hours: u64,
185}
186
187impl Default for ServerConfig {
188 fn default() -> Self {
189 Self {
190 bind: "0.0.0.0:8080".parse().unwrap(),
191 storage_backend: StorageBackend::Memory,
192 sqlite_path: None,
193 postgres_url: None,
194 postgres_pool: PostgresPoolConfig::default(),
195 artifacts_url: None,
196 api_keys: vec![],
197 jwt: None,
198 oidc_configs: vec![],
199 cors: true,
200 timeout_seconds: 30,
201 local_mode: false,
202 retention_days: 0,
203 cleanup_interval_hours: 1,
204 }
205 }
206}
207
208impl ServerConfig {
209 pub fn new() -> Self {
211 Self::default()
212 }
213
214 pub fn bind(mut self, addr: impl Into<String>) -> Result<Self, ConfigError> {
216 self.bind = addr
217 .into()
218 .parse()
219 .map_err(|e| ConfigError::InvalidValue(format!("Invalid bind address: {}", e)))?;
220 Ok(self)
221 }
222
223 pub fn storage_backend(mut self, backend: StorageBackend) -> Self {
225 self.storage_backend = backend;
226 self
227 }
228
229 pub fn sqlite_path(mut self, path: impl Into<PathBuf>) -> Self {
231 self.sqlite_path = Some(path.into());
232 self
233 }
234
235 pub fn postgres_url(mut self, url: impl Into<String>) -> Self {
237 self.postgres_url = Some(url.into());
238 self
239 }
240
241 pub fn postgres_pool(mut self, pool_config: PostgresPoolConfig) -> Self {
243 self.postgres_pool = pool_config;
244 self
245 }
246
247 pub fn artifacts_url(mut self, url: impl Into<String>) -> Self {
249 self.artifacts_url = Some(url.into());
250 self
251 }
252
253 pub fn api_key(self, key: impl Into<String>, role: Role) -> Self {
255 self.scoped_api_key(key, role, "default", None)
256 }
257
258 pub fn scoped_api_key(
260 mut self,
261 key: impl Into<String>,
262 role: Role,
263 project: impl Into<String>,
264 benchmark_regex: Option<String>,
265 ) -> Self {
266 self.api_keys.push(ApiKeyConfig {
267 key: key.into(),
268 role,
269 project: project.into(),
270 benchmark_regex,
271 });
272 self
273 }
274
275 pub fn jwt(mut self, jwt: JwtConfig) -> Self {
277 self.jwt = Some(jwt);
278 self
279 }
280
281 pub fn oidc(mut self, config: OidcConfig) -> Self {
285 self.oidc_configs.push(config);
286 self
287 }
288
289 pub fn cors(mut self, enabled: bool) -> Self {
291 self.cors = enabled;
292 self
293 }
294
295 pub fn local_mode(mut self, enabled: bool) -> Self {
297 self.local_mode = enabled;
298 self
299 }
300
301 pub fn retention_days(mut self, days: u64) -> Self {
303 self.retention_days = days;
304 self
305 }
306
307 pub fn cleanup_interval_hours(mut self, hours: u64) -> Self {
309 self.cleanup_interval_hours = hours;
310 self
311 }
312}
313
314pub(crate) async fn create_artifacts(
316 config: &ServerConfig,
317) -> Result<Option<Arc<dyn ArtifactStore>>, ConfigError> {
318 if let Some(url) = &config.artifacts_url {
319 info!(url = %url, "Using object storage for artifacts");
320 let (store, _path) = object_store::parse_url(
321 &url.parse()
322 .map_err(|e| ConfigError::InvalidValue(format!("Invalid artifacts URL: {}", e)))?,
323 )
324 .map_err(|e| ConfigError::InvalidValue(format!("Failed to parse artifacts URL: {}", e)))?;
325
326 Ok(Some(Arc::new(ObjectArtifactStore::new(Arc::from(store)))))
327 } else {
328 Ok(None)
329 }
330}
331
332#[allow(dead_code)]
336pub(crate) async fn create_storage(
337 config: &ServerConfig,
338) -> Result<(Arc<dyn BaselineStore>, Arc<dyn AuditStore>), ConfigError> {
339 let artifacts = create_artifacts(config).await?;
340 create_storage_with_artifacts(config, artifacts).await
341}
342
343pub(crate) async fn create_storage_with_artifacts(
345 config: &ServerConfig,
346 artifacts: Option<Arc<dyn ArtifactStore>>,
347) -> Result<(Arc<dyn BaselineStore>, Arc<dyn AuditStore>), ConfigError> {
348 match config.storage_backend {
349 StorageBackend::Memory => {
350 info!("Using in-memory storage");
351 let store = Arc::new(InMemoryStore::new());
352 Ok((store.clone(), store))
353 }
354 StorageBackend::Sqlite => {
355 let path = config
356 .sqlite_path
357 .clone()
358 .unwrap_or_else(|| PathBuf::from("perfgate.db"));
359 info!(path = %path.display(), "Using SQLite storage");
360 let store = SqliteStore::new(&path, artifacts)
361 .map_err(|e| ConfigError::InvalidValue(format!("Failed to open SQLite: {}", e)))?;
362 let store = Arc::new(store);
363 Ok((store.clone(), store))
364 }
365 StorageBackend::Postgres => {
366 let url = config
367 .postgres_url
368 .clone()
369 .unwrap_or_else(|| "postgres://localhost:5432/perfgate".to_string());
370 info!(url = %url, "Using PostgreSQL storage");
371 let store = PostgresStore::new(&url, artifacts, &config.postgres_pool)
372 .await
373 .map_err(|e| {
374 ConfigError::InvalidValue(format!("Failed to connect to Postgres: {}", e))
375 })?;
376 let store = Arc::new(store);
377 Ok((store.clone(), store))
378 }
379 }
380}
381
382pub(crate) async fn create_key_store(
384 config: &ServerConfig,
385) -> Result<Arc<ApiKeyStore>, ConfigError> {
386 let store = ApiKeyStore::new();
387
388 for cfg in &config.api_keys {
390 let mut api_key = ApiKey::new(
391 uuid::Uuid::new_v4().to_string(),
392 format!("{:?} key for {}", cfg.role, cfg.project),
393 cfg.project.clone(),
394 cfg.role,
395 );
396 api_key.benchmark_regex = cfg.benchmark_regex.clone();
397
398 store.add_key(api_key, &cfg.key).await;
399 info!(role = ?cfg.role, project = %cfg.project, "Added API key");
400 }
401
402 Ok(Arc::new(store))
403}
404
405pub(crate) fn create_persistent_key_store(
407 config: &ServerConfig,
408 sqlite_conn: Option<Arc<std::sync::Mutex<rusqlite::Connection>>>,
409) -> Result<Arc<dyn KeyStore>, ConfigError> {
410 match config.storage_backend {
411 StorageBackend::Sqlite => {
412 if let Some(conn) = sqlite_conn {
413 let store = SqliteKeyStore::new(conn).map_err(|e| {
414 ConfigError::InvalidValue(format!("Failed to create SQLite key store: {}", e))
415 })?;
416 info!("Using SQLite persistent key store");
417 Ok(Arc::new(store))
418 } else {
419 info!("Using in-memory key store (no SQLite connection available)");
420 Ok(Arc::new(InMemoryKeyStore::new()))
421 }
422 }
423 _ => {
424 info!("Using in-memory key store");
425 Ok(Arc::new(InMemoryKeyStore::new()))
426 }
427 }
428}
429
430pub(crate) fn create_fleet_store() -> Arc<dyn FleetStore> {
432 Arc::new(InMemoryFleetStore::new())
433}
434
435pub(crate) fn create_router(
437 state: AppState,
438 persistent_key_store: Arc<dyn KeyStore>,
439 fleet_store: Arc<dyn FleetStore>,
440 artifact_store: Option<Arc<dyn ArtifactStore>>,
441 auth_state: AuthState,
442 config: &ServerConfig,
443 prometheus_handle: Option<PrometheusHandle>,
444) -> Router {
445 let local_mode = config.local_mode;
446
447 let health_routes = Router::new().route("/health", get(health_check));
449
450 let info_routes = Router::new().route(
452 "/info",
453 get(move || async move { axum::Json(serde_json::json!({ "local_mode": local_mode })) }),
454 );
455
456 let dashboard_routes = Router::new()
458 .route("/", get(dashboard_index))
459 .route("/index.html", get(dashboard_index))
460 .route("/assets/{*path}", get(static_asset));
461
462 let key_routes = Router::new()
464 .route("/keys", post(create_key))
465 .route("/keys", get(list_keys))
466 .route("/keys/{id}", delete(revoke_key))
467 .with_state(persistent_key_store)
468 .layer(middleware::from_fn_with_state(
469 auth_state.clone(),
470 auth_middleware,
471 ));
472
473 let admin_routes = Router::new()
475 .route("/admin/cleanup", delete(admin_cleanup))
476 .layer(axum::Extension(artifact_store.clone()))
477 .layer(axum::Extension(DefaultRetentionDays(config.retention_days)))
478 .layer(middleware::from_fn_with_state(
479 auth_state.clone(),
480 auth_middleware,
481 ));
482
483 let fleet_routes = Router::new()
485 .route("/fleet/dependency-event", post(record_dependency_event))
486 .route("/fleet/alerts", get(list_fleet_alerts))
487 .route(
488 "/fleet/dependency/{dep_name}/impact",
489 get(dependency_impact),
490 )
491 .with_state(fleet_store);
492
493 let api_routes_inner = Router::new()
495 .route("/projects/{project}/baselines", post(upload_baseline))
497 .route(
498 "/projects/{project}/baselines/{benchmark}/latest",
499 get(get_latest_baseline),
500 )
501 .route(
502 "/projects/{project}/baselines/{benchmark}/versions/{version}",
503 get(get_baseline),
504 )
505 .route(
506 "/projects/{project}/baselines/{benchmark}/versions/{version}",
507 delete(delete_baseline),
508 )
509 .route("/projects/{project}/baselines", get(list_baselines))
510 .route("/projects/{project}/verdicts", post(submit_verdict))
511 .route("/projects/{project}/verdicts", get(list_verdicts))
512 .route(
513 "/projects/{project}/baselines/{benchmark}/promote",
514 post(promote_baseline),
515 )
516 .route(
517 "/projects/{project}/baselines/{benchmark}/trend",
518 get(get_trend),
519 )
520 .route("/audit", get(list_audit_events));
522
523 let api_routes = if config.local_mode {
524 api_routes_inner.layer(middleware::from_fn(local_mode_auth_middleware))
525 } else {
526 api_routes_inner.layer(middleware::from_fn_with_state(auth_state, auth_middleware))
527 };
528
529 let mut app = Router::new()
531 .merge(dashboard_routes)
532 .merge(health_routes.clone())
533 .nest(
534 "/api/v1",
535 health_routes
536 .merge(info_routes)
537 .merge(api_routes)
538 .merge(key_routes)
539 .merge(admin_routes)
540 .merge(fleet_routes),
541 );
542
543 if let Some(handle) = prometheus_handle {
545 let metrics_routes = Router::new()
546 .route("/metrics", get(metrics_handler))
547 .with_state(handle);
548 app = app.merge(metrics_routes);
549 app = app.layer(middleware::from_fn(metrics_middleware));
551 }
552
553 if config.cors {
555 app = app.layer(
556 CorsLayer::new()
557 .allow_origin(Any)
558 .allow_methods(Any)
559 .allow_headers(Any),
560 );
561 }
562
563 app.with_state(state)
564}
565
566pub async fn run_server(config: ServerConfig) -> Result<(), Box<dyn std::error::Error>> {
570 info!(
571 bind = %config.bind,
572 backend = ?config.storage_backend,
573 retention_days = config.retention_days,
574 "Starting perfgate server"
575 );
576
577 let artifact_store = create_artifacts(&config).await?;
579
580 let (store, audit) = create_storage_with_artifacts(&config, artifact_store.clone()).await?;
582
583 let sqlite_conn = if config.storage_backend == StorageBackend::Sqlite {
585 let path = config
586 .sqlite_path
587 .clone()
588 .unwrap_or_else(|| PathBuf::from("perfgate.db"));
589 let conn = rusqlite::Connection::open(&path).map_err(|e| {
590 ConfigError::InvalidValue(format!("Failed to open SQLite for key store: {}", e))
591 })?;
592 Some(Arc::new(std::sync::Mutex::new(conn)))
593 } else {
594 None
595 };
596 let persistent_key_store = create_persistent_key_store(&config, sqlite_conn)?;
597
598 let key_store = create_key_store(&config).await?;
600
601 let mut oidc_registry = OidcRegistry::new();
602 for oidc_cfg in &config.oidc_configs {
603 let provider = OidcProvider::new(oidc_cfg.clone())
604 .await
605 .map_err(|e| e.to_string())?;
606 oidc_registry.add(provider);
607 }
608
609 let auth_state = AuthState::new(key_store, config.jwt.clone(), oidc_registry)
610 .with_persistent_key_store(persistent_key_store.clone());
611
612 let cleanup_handle = if config.retention_days > 0 {
614 if let Some(ref art_store) = artifact_store {
615 info!(
616 retention_days = config.retention_days,
617 interval_hours = config.cleanup_interval_hours,
618 "Spawning background artifact cleanup task"
619 );
620 Some(spawn_cleanup_task(
621 art_store.clone(),
622 config.retention_days,
623 config.cleanup_interval_hours,
624 ))
625 } else {
626 info!(
627 "Retention policy configured but no artifact store available; skipping background cleanup"
628 );
629 None
630 }
631 } else {
632 None
633 };
634
635 let prometheus_handle = setup_metrics_recorder();
637 info!("Prometheus metrics enabled at /metrics");
638
639 let app_state = AppState { store, audit };
640
641 let fleet_store = create_fleet_store();
643
644 let app = create_router(
646 app_state,
647 persistent_key_store.clone(),
648 fleet_store,
649 artifact_store,
650 auth_state,
651 &config,
652 Some(prometheus_handle),
653 );
654
655 let app = app.layer(
657 ServiceBuilder::new()
658 .layer(TraceLayer::new_for_http())
659 .layer(tower_http::request_id::SetRequestIdLayer::x_request_id(
660 MakeRequestUuid,
661 )),
662 );
663
664 let listener = tokio::net::TcpListener::bind(config.bind).await?;
666 info!(addr = %config.bind, "Server listening");
667
668 axum::serve(listener, app)
670 .with_graceful_shutdown(shutdown_signal())
671 .await?;
672
673 if let Some(handle) = cleanup_handle {
675 handle.abort();
676 }
677
678 info!("Server shutdown complete");
679 Ok(())
680}
681
682async fn shutdown_signal() {
684 use tokio::signal;
685
686 let ctrl_c = async {
687 signal::ctrl_c()
688 .await
689 .expect("Failed to install Ctrl+C handler");
690 };
691
692 #[cfg(unix)]
693 let terminate = async {
694 signal::unix::signal(signal::unix::SignalKind::terminate())
695 .expect("Failed to install signal handler")
696 .recv()
697 .await;
698 };
699
700 #[cfg(not(unix))]
701 let terminate = std::future::pending::<()>();
702
703 tokio::select! {
704 _ = ctrl_c => {},
705 _ = terminate => {},
706 }
707
708 info!("Shutdown signal received");
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714
715 #[test]
716 fn test_server_config_default() {
717 let config = ServerConfig::new();
718 assert_eq!(config.bind.to_string(), "0.0.0.0:8080");
719 assert_eq!(config.storage_backend, StorageBackend::Memory);
720 }
721
722 #[test]
723 fn test_server_config_builder() {
724 let config = ServerConfig::new()
725 .bind("127.0.0.1:3000")
726 .unwrap()
727 .storage_backend(StorageBackend::Sqlite)
728 .sqlite_path("/tmp/test.db")
729 .api_key("test-key", Role::Admin)
730 .scoped_api_key(
731 "scoped-key",
732 Role::Contributor,
733 "my-proj",
734 Some("^bench-.*$".to_string()),
735 )
736 .jwt(JwtConfig::hs256(b"test-secret".to_vec()).issuer("perfgate"))
737 .cors(false);
738
739 assert_eq!(config.bind.to_string(), "127.0.0.1:3000");
740 assert_eq!(config.storage_backend, StorageBackend::Sqlite);
741 assert_eq!(config.sqlite_path, Some(PathBuf::from("/tmp/test.db")));
742 assert_eq!(config.api_keys.len(), 2);
743 assert_eq!(config.api_keys[1].project, "my-proj");
744 assert_eq!(
745 config.api_keys[1].benchmark_regex,
746 Some("^bench-.*$".to_string())
747 );
748 assert!(config.jwt.is_some());
749 assert!(!config.cors);
750 }
751
752 #[test]
753 fn test_storage_backend_from_str() {
754 assert_eq!(
755 "memory".parse::<StorageBackend>().unwrap(),
756 StorageBackend::Memory
757 );
758 assert_eq!(
759 "sqlite".parse::<StorageBackend>().unwrap(),
760 StorageBackend::Sqlite
761 );
762 assert_eq!(
763 "postgres".parse::<StorageBackend>().unwrap(),
764 StorageBackend::Postgres
765 );
766 assert!("invalid".parse::<StorageBackend>().is_err());
767 }
768
769 #[tokio::test]
770 async fn test_create_storage_memory() {
771 let config = ServerConfig::new().storage_backend(StorageBackend::Memory);
772 let (storage, _audit) = create_storage(&config).await.unwrap();
773 assert_eq!(storage.backend_type(), "memory");
774 }
775
776 #[tokio::test(flavor = "multi_thread")]
777 async fn test_create_storage_sqlite() {
778 let config = ServerConfig::new()
779 .storage_backend(StorageBackend::Sqlite)
780 .sqlite_path(":memory:");
781 let (storage, _audit) = create_storage(&config).await.unwrap();
782 assert_eq!(storage.backend_type(), "sqlite");
783 }
784
785 #[tokio::test]
786 async fn test_create_storage_postgres() {
787 let config = ServerConfig::new()
788 .storage_backend(StorageBackend::Postgres)
789 .postgres_url("postgresql://localhost/test");
790 let result = create_storage(&config).await;
791 assert!(result.is_err());
793 }
794
795 #[tokio::test]
796 async fn test_create_key_store() {
797 let config = ServerConfig::new()
798 .api_key("pg_live_test123456789012345678901234567890", Role::Admin)
799 .scoped_api_key(
800 "pg_live_viewer123456789012345678901234567",
801 Role::Viewer,
802 "project-1",
803 None,
804 );
805
806 let key_store = create_key_store(&config).await.unwrap();
807 let keys = key_store.list_keys().await;
808
809 assert_eq!(keys.len(), 2);
810 let viewer_key = keys.iter().find(|k| k.role == Role::Viewer).unwrap();
811 assert_eq!(viewer_key.project_id, "project-1");
812 }
813
814 #[tokio::test]
815 async fn test_router_creation() {
816 let store = Arc::new(InMemoryStore::new());
817 let persistent_key_store: Arc<dyn KeyStore> = Arc::new(InMemoryKeyStore::new());
818 let fleet_store = create_fleet_store();
819 let auth_state = AuthState::new(Arc::new(ApiKeyStore::new()), None, Default::default());
820 let config = ServerConfig::new();
821 let app_state = AppState {
822 store: store.clone(),
823 audit: store,
824 };
825
826 let _router = create_router(
827 app_state,
828 persistent_key_store,
829 fleet_store,
830 None,
831 auth_state,
832 &config,
833 None,
834 );
835 }
837
838 #[tokio::test]
839 async fn test_router_local_mode_injects_auth_context_for_api_routes() {
840 let store = Arc::new(InMemoryStore::new());
841 let persistent_key_store: Arc<dyn KeyStore> = Arc::new(InMemoryKeyStore::new());
842 let fleet_store = create_fleet_store();
843 let auth_state = AuthState::new(Arc::new(ApiKeyStore::new()), None, Default::default());
844 let config = ServerConfig::new().local_mode(true);
845 let app_state = AppState {
846 store: store.clone(),
847 audit: store,
848 };
849
850 let router = create_router(
851 app_state,
852 persistent_key_store,
853 fleet_store,
854 None,
855 auth_state,
856 &config,
857 None,
858 );
859
860 let response = tower::ServiceExt::oneshot(
861 router,
862 axum::http::Request::builder()
863 .uri("/api/v1/projects/test/baselines")
864 .body(axum::body::Body::empty())
865 .unwrap(),
866 )
867 .await
868 .unwrap();
869
870 assert_eq!(response.status(), axum::http::StatusCode::OK);
871 }
872
873 #[test]
874 fn test_postgres_pool_config_defaults() {
875 let cfg = PostgresPoolConfig::default();
876 assert_eq!(cfg.max_connections, 10);
877 assert_eq!(cfg.min_connections, 2);
878 assert_eq!(cfg.idle_timeout, Duration::from_secs(300));
879 assert_eq!(cfg.max_lifetime, Duration::from_secs(1800));
880 assert_eq!(cfg.acquire_timeout, Duration::from_secs(5));
881 assert_eq!(cfg.statement_timeout, Duration::from_secs(30));
882 }
883
884 #[test]
885 fn test_server_config_with_postgres_pool() {
886 let pool_config = PostgresPoolConfig {
887 max_connections: 20,
888 min_connections: 5,
889 idle_timeout: Duration::from_secs(120),
890 max_lifetime: Duration::from_secs(3600),
891 acquire_timeout: Duration::from_secs(10),
892 statement_timeout: Duration::from_secs(60),
893 };
894
895 let config = ServerConfig::new()
896 .storage_backend(StorageBackend::Postgres)
897 .postgres_url("postgres://localhost:5432/perfgate")
898 .postgres_pool(pool_config);
899
900 assert_eq!(config.postgres_pool.max_connections, 20);
901 assert_eq!(config.postgres_pool.min_connections, 5);
902 assert_eq!(config.postgres_pool.idle_timeout, Duration::from_secs(120));
903 assert_eq!(config.postgres_pool.max_lifetime, Duration::from_secs(3600));
904 assert_eq!(
905 config.postgres_pool.acquire_timeout,
906 Duration::from_secs(10)
907 );
908 assert_eq!(
909 config.postgres_pool.statement_timeout,
910 Duration::from_secs(60)
911 );
912 }
913
914 #[tokio::test]
915 async fn test_health_endpoint_no_pool_for_memory() {
916 let store: Arc<dyn crate::storage::BaselineStore> = Arc::new(InMemoryStore::new());
917 assert!(store.pool_metrics().is_none());
918 }
919}