Skip to main content

mz_environmentd/
test_util.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::collections::BTreeMap;
11use std::error::Error;
12use std::future::IntoFuture;
13use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream};
14use std::path::{Path, PathBuf};
15use std::pin::Pin;
16use std::str::FromStr;
17use std::sync::Arc;
18use std::sync::LazyLock;
19use std::time::Duration;
20use std::{env, fs, iter};
21
22use anyhow::anyhow;
23use futures::Future;
24use futures::future::{BoxFuture, LocalBoxFuture};
25use headers::{Header, HeaderMapExt};
26use http::{HeaderValue, Uri};
27use hyper::http::header::HeaderMap;
28use maplit::btreemap;
29use mz_adapter::TimestampExplanation;
30use mz_adapter_types::bootstrap_builtin_cluster_config::{
31    ANALYTICS_CLUSTER_DEFAULT_REPLICATION_FACTOR, BootstrapBuiltinClusterConfig,
32    CATALOG_SERVER_CLUSTER_DEFAULT_REPLICATION_FACTOR, PROBE_CLUSTER_DEFAULT_REPLICATION_FACTOR,
33    SUPPORT_CLUSTER_DEFAULT_REPLICATION_FACTOR, SYSTEM_CLUSTER_DEFAULT_REPLICATION_FACTOR,
34};
35use mz_adapter_types::dyncfgs::ENABLE_CLUSTER_RECONFIGURATION_LAG_GATE;
36
37use mz_auth::password::Password;
38use mz_catalog::config::ClusterReplicaSizeMap;
39use mz_controller::ControllerConfig;
40use mz_dyncfg::ConfigUpdates;
41use mz_license_keys::ValidatedLicenseKey;
42use mz_orchestrator_process::{ProcessOrchestrator, ProcessOrchestratorConfig};
43use mz_orchestrator_tracing::{TracingCliArgs, TracingOrchestrator};
44use mz_ore::cast::CastLossy;
45use mz_ore::metrics::MetricsRegistry;
46use mz_ore::now::{EpochMillis, NowFn, SYSTEM_TIME};
47use mz_ore::retry::Retry;
48use mz_ore::task;
49use mz_ore::tracing::{
50    OpenTelemetryConfig, StderrLogConfig, StderrLogFormat, TracingConfig, TracingHandle,
51};
52use mz_persist_client::PersistLocation;
53use mz_persist_client::cache::PersistClientCache;
54use mz_persist_client::cfg::{
55    CONSENSUS_CONNECTION_POOL_MAX_SIZE, PersistConfig, SHARD_METRICS, ShardMetricsExport,
56};
57use mz_persist_client::rpc::PersistGrpcPubSubServer;
58use mz_postgres_util::{
59    Sql, batch_execute as pg_batch_execute, execute as pg_execute, query_one as pg_query_one, sql,
60};
61use mz_secrets::SecretsController;
62use mz_server_core::listeners::v26_32_0::ListenersConfig;
63use mz_server_core::listeners::{
64    AllowedRoles, AuthenticatorKind, HttpListenerConfig, HttpRoutesEnabled, RouteGroup,
65};
66use mz_server_core::{ReloadTrigger, TlsCertConfig};
67use mz_sql::catalog::EnvironmentId;
68use mz_storage_types::connections::ConnectionContext;
69use mz_tracing::CloneableEnvFilter;
70use openssl::asn1::Asn1Time;
71use openssl::error::ErrorStack;
72use openssl::hash::MessageDigest;
73use openssl::nid::Nid;
74use openssl::pkey::{PKey, Private};
75use openssl::rsa::Rsa;
76use openssl::ssl::{SslConnector, SslConnectorBuilder, SslMethod, SslOptions};
77use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName};
78use openssl::x509::{X509, X509Name, X509NameBuilder};
79use postgres::error::DbError;
80use postgres::tls::{MakeTlsConnect, TlsConnect};
81use postgres::types::{FromSql, Type};
82use postgres::{NoTls, Socket};
83use postgres_openssl::MakeTlsConnector;
84use tempfile::TempDir;
85use tokio::net::TcpListener;
86use tokio::runtime::Runtime;
87use tokio_postgres::config::{Host, SslMode};
88use tokio_postgres::{AsyncMessage, Client};
89use tokio_stream::wrappers::TcpListenerStream;
90use tower_http::cors::AllowOrigin;
91use tracing::Level;
92use tracing_capture::SharedStorage;
93use tracing_subscriber::EnvFilter;
94use tungstenite::stream::MaybeTlsStream;
95use tungstenite::{Message, WebSocket};
96
97use crate::{
98    CatalogConfig, FronteggAuthenticator, SqlListenerConfig, WebSocketAuth, WebSocketResponse,
99};
100
101pub static KAFKA_ADDRS: LazyLock<String> =
102    LazyLock::new(|| env::var("KAFKA_ADDRS").unwrap_or_else(|_| "localhost:9092".into()));
103
104/// Entry point for creating and configuring an `environmentd` test harness.
105#[derive(Clone)]
106pub struct TestHarness {
107    data_directory: Option<PathBuf>,
108    tls: Option<TlsCertConfig>,
109    frontegg: Option<FronteggAuthenticator>,
110    external_login_password_mz_system: Option<Password>,
111    listeners_config: ListenersConfig,
112    unsafe_mode: bool,
113    /// Whether the connection context carries the AWS external ID prefix and
114    /// connection role ARN. Default true, matching a cloud deployment. Set false
115    /// via [`TestHarness::without_aws_connection_context`] to model a deployment
116    /// that never configured the AWS context, where the context functions fold
117    /// to NULL.
118    aws_connection_context: bool,
119    workers: usize,
120    now: NowFn,
121    seed: u32,
122    storage_usage_collection_interval: Duration,
123    storage_usage_retention_period: Option<Duration>,
124    default_cluster_replica_size: String,
125    default_cluster_replication_factor: u32,
126    builtin_system_cluster_config: BootstrapBuiltinClusterConfig,
127    builtin_catalog_server_cluster_config: BootstrapBuiltinClusterConfig,
128    builtin_probe_cluster_config: BootstrapBuiltinClusterConfig,
129    builtin_support_cluster_config: BootstrapBuiltinClusterConfig,
130    builtin_analytics_cluster_config: BootstrapBuiltinClusterConfig,
131
132    propagate_crashes: bool,
133    enable_tracing: bool,
134    // This is currently unrelated to enable_tracing, and is used only to disable orchestrator
135    // tracing.
136    orchestrator_tracing_cli_args: TracingCliArgs,
137    bootstrap_role: Option<String>,
138    deploy_generation: u64,
139    system_parameter_defaults: BTreeMap<String, String>,
140    internal_console_redirect_url: Option<String>,
141    cors_allowed_origins: Vec<HeaderValue>,
142    metrics_registry: Option<MetricsRegistry>,
143    code_version: semver::Version,
144    force_builtin_schema_migration: Option<String>,
145    capture: Option<SharedStorage>,
146    pub environment_id: EnvironmentId,
147}
148
149impl Default for TestHarness {
150    fn default() -> TestHarness {
151        TestHarness {
152            data_directory: None,
153            tls: None,
154            frontegg: None,
155            external_login_password_mz_system: None,
156            listeners_config: ListenersConfig {
157                sql: btreemap![
158                    "external".to_owned() => SqlListenerConfig {
159                        addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
160                        authenticator_kind: AuthenticatorKind::None,
161                        allowed_roles: AllowedRoles::Normal,
162                        enable_tls: false,
163                    },
164                    "internal".to_owned() => SqlListenerConfig {
165                        addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
166                        authenticator_kind: AuthenticatorKind::None,
167                        allowed_roles: AllowedRoles::NormalAndInternal,
168                        enable_tls: false,
169                    },
170                ],
171                http: btreemap![
172                    "external".to_owned() => HttpListenerConfig {
173                        addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
174                        authenticator_kind: AuthenticatorKind::None,
175                        enable_tls: false,
176                        routes: HttpRoutesEnabled {
177                            base: RouteGroup::Enabled(AllowedRoles::Normal),
178                            webhook: RouteGroup::Enabled(AllowedRoles::Normal),
179                            internal: RouteGroup::Disabled,
180                            metrics: RouteGroup::Disabled,
181                            profiling: RouteGroup::Disabled,
182                            mcp_agent: RouteGroup::Disabled,
183                            mcp_developer: RouteGroup::Disabled,
184                            console_config: RouteGroup::Enabled(AllowedRoles::Normal),
185                        },
186                    },
187                    "internal".to_owned() => HttpListenerConfig {
188                        addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
189                        authenticator_kind: AuthenticatorKind::None,
190                        enable_tls: false,
191                        routes: HttpRoutesEnabled {
192                            base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
193                            webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
194                            internal: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
195                            metrics: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
196                            profiling: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
197                            mcp_agent: RouteGroup::Disabled,
198                            mcp_developer: RouteGroup::Disabled,
199                            console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
200                        },
201                    },
202                ],
203            },
204            unsafe_mode: false,
205            aws_connection_context: true,
206            workers: 1,
207            now: SYSTEM_TIME.clone(),
208            seed: rand::random(),
209            storage_usage_collection_interval: Duration::from_secs(3600),
210            storage_usage_retention_period: None,
211            default_cluster_replica_size: "scale=1,workers=1".to_string(),
212            default_cluster_replication_factor: 1,
213            builtin_system_cluster_config: BootstrapBuiltinClusterConfig {
214                size: "scale=1,workers=1".to_string(),
215                replication_factor: SYSTEM_CLUSTER_DEFAULT_REPLICATION_FACTOR,
216            },
217            builtin_catalog_server_cluster_config: BootstrapBuiltinClusterConfig {
218                size: "scale=1,workers=1".to_string(),
219                replication_factor: CATALOG_SERVER_CLUSTER_DEFAULT_REPLICATION_FACTOR,
220            },
221            builtin_probe_cluster_config: BootstrapBuiltinClusterConfig {
222                size: "scale=1,workers=1".to_string(),
223                replication_factor: PROBE_CLUSTER_DEFAULT_REPLICATION_FACTOR,
224            },
225            builtin_support_cluster_config: BootstrapBuiltinClusterConfig {
226                size: "scale=1,workers=1".to_string(),
227                replication_factor: SUPPORT_CLUSTER_DEFAULT_REPLICATION_FACTOR,
228            },
229            builtin_analytics_cluster_config: BootstrapBuiltinClusterConfig {
230                size: "scale=1,workers=1".to_string(),
231                replication_factor: ANALYTICS_CLUSTER_DEFAULT_REPLICATION_FACTOR,
232            },
233            propagate_crashes: false,
234            enable_tracing: false,
235            bootstrap_role: Some("materialize".into()),
236            deploy_generation: 0,
237            // This and startup_log_filter below are both (?) needed to suppress clusterd messages.
238            // If we need those in the future, we might need to change both.
239            system_parameter_defaults: BTreeMap::from([
240                ("log_filter".to_string(), "error".to_string()),
241                (
242                    ENABLE_CLUSTER_RECONFIGURATION_LAG_GATE.name().to_string(),
243                    "true".to_string(),
244                ),
245                (
246                    SHARD_METRICS.name().to_string(),
247                    ShardMetricsExport::Summary.as_str().to_string(),
248                ),
249            ]),
250            internal_console_redirect_url: None,
251            cors_allowed_origins: Vec::new(),
252            metrics_registry: None,
253            orchestrator_tracing_cli_args: TracingCliArgs {
254                startup_log_filter: CloneableEnvFilter::from_str("error").expect("must parse"),
255                ..Default::default()
256            },
257            code_version: crate::BUILD_INFO.semver_version(),
258            force_builtin_schema_migration: None,
259            environment_id: EnvironmentId::for_tests(),
260            capture: None,
261        }
262    }
263}
264
265impl TestHarness {
266    /// Starts a test [`TestServer`], panicking if the server could not be started.
267    ///
268    /// For cases when startup might fail, see [`TestHarness::try_start`].
269    pub async fn start(self) -> TestServer {
270        self.try_start().await.expect("Failed to start test Server")
271    }
272
273    /// Like [`TestHarness::start`] but can specify a cert reload trigger.
274    pub async fn start_with_trigger(self, tls_reload_certs: ReloadTrigger) -> TestServer {
275        self.try_start_with_trigger(tls_reload_certs)
276            .await
277            .expect("Failed to start test Server")
278    }
279
280    /// Starts a test [`TestServer`], returning an error if the server could not be started.
281    pub async fn try_start(self) -> Result<TestServer, anyhow::Error> {
282        self.try_start_with_trigger(mz_server_core::cert_reload_never_reload())
283            .await
284    }
285
286    /// Like [`TestHarness::try_start`] but can specify a cert reload trigger.
287    pub async fn try_start_with_trigger(
288        self,
289        tls_reload_certs: ReloadTrigger,
290    ) -> Result<TestServer, anyhow::Error> {
291        let listeners = Listeners::new(&self).await?;
292        listeners.serve_with_trigger(self, tls_reload_certs).await
293    }
294
295    /// Starts a runtime and returns a [`TestServerWithRuntime`].
296    pub fn start_blocking(self) -> TestServerWithRuntime {
297        let runtime = tokio::runtime::Builder::new_multi_thread()
298            .enable_all()
299            .thread_stack_size(mz_ore::stack::STACK_SIZE)
300            .build()
301            .expect("failed to spawn runtime for test");
302        let runtime = Arc::new(runtime);
303        let server = runtime.block_on(self.start());
304        TestServerWithRuntime { runtime, server }
305    }
306
307    pub fn data_directory(mut self, data_directory: impl Into<PathBuf>) -> Self {
308        self.data_directory = Some(data_directory.into());
309        self
310    }
311
312    pub fn with_tls(mut self, cert_path: impl Into<PathBuf>, key_path: impl Into<PathBuf>) -> Self {
313        self.tls = Some(TlsCertConfig {
314            cert: cert_path.into(),
315            key: key_path.into(),
316        });
317        for (_, listener) in &mut self.listeners_config.sql {
318            listener.enable_tls = true;
319        }
320        for (_, listener) in &mut self.listeners_config.http {
321            listener.enable_tls = true;
322        }
323        self
324    }
325
326    pub fn unsafe_mode(mut self) -> Self {
327        self.unsafe_mode = true;
328        self
329    }
330
331    /// Models a deployment that never configured the AWS context, so the AWS
332    /// external ID prefix and connection role ARN are absent and the plan-time
333    /// AWS context functions fold to NULL.
334    pub fn without_aws_connection_context(mut self) -> Self {
335        self.aws_connection_context = false;
336        self
337    }
338
339    pub fn workers(mut self, workers: usize) -> Self {
340        self.workers = workers;
341        self
342    }
343
344    pub fn with_frontegg_auth(mut self, frontegg: &FronteggAuthenticator) -> Self {
345        self.frontegg = Some(frontegg.clone());
346        let enable_tls = self.tls.is_some();
347        self.listeners_config = ListenersConfig {
348            sql: btreemap! {
349                "external".to_owned() => SqlListenerConfig {
350                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
351                    authenticator_kind: AuthenticatorKind::Frontegg,
352                    allowed_roles: AllowedRoles::Normal,
353                    enable_tls,
354                },
355                "internal".to_owned() => SqlListenerConfig {
356                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
357                    authenticator_kind: AuthenticatorKind::None,
358                    allowed_roles: AllowedRoles::NormalAndInternal,
359                    enable_tls: false,
360                },
361            },
362            http: btreemap! {
363                "external".to_owned() => HttpListenerConfig {
364                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
365                    authenticator_kind: AuthenticatorKind::Frontegg,
366                    enable_tls,
367                    routes: HttpRoutesEnabled {
368                        base: RouteGroup::Enabled(AllowedRoles::Normal),
369                        webhook: RouteGroup::Enabled(AllowedRoles::Normal),
370                        internal: RouteGroup::Disabled,
371                        metrics: RouteGroup::Disabled,
372                        profiling: RouteGroup::Disabled,
373                        mcp_agent: RouteGroup::Disabled,
374                        mcp_developer: RouteGroup::Disabled,
375                        console_config: RouteGroup::Enabled(AllowedRoles::Normal),
376                    },
377                },
378                "internal".to_owned() => HttpListenerConfig {
379                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
380                    authenticator_kind: AuthenticatorKind::None,
381                    enable_tls: false,
382                    routes: HttpRoutesEnabled {
383                        base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
384                        webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
385                        internal: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
386                        metrics: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
387                        profiling: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
388                        mcp_agent: RouteGroup::Disabled,
389                        mcp_developer: RouteGroup::Disabled,
390                        console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
391                    },
392                },
393            },
394        };
395        self
396    }
397
398    pub fn with_oidc_auth(
399        mut self,
400        issuer: Option<String>,
401        authentication_claim: Option<String>,
402        expected_audiences: Option<Vec<String>>,
403        external_login_password_mz_system: Option<Password>,
404    ) -> Self {
405        let enable_tls = self.tls.is_some();
406        self.listeners_config = ListenersConfig {
407            sql: btreemap! {
408                "external".to_owned() => SqlListenerConfig {
409                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
410                    authenticator_kind: AuthenticatorKind::Oidc,
411                    allowed_roles: AllowedRoles::NormalAndInternal,
412                    enable_tls,
413                },
414                "internal".to_owned() => SqlListenerConfig {
415                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
416                    authenticator_kind: AuthenticatorKind::None,
417                    allowed_roles: AllowedRoles::NormalAndInternal,
418                    enable_tls: false,
419                },
420            },
421            http: btreemap! {
422                "external".to_owned() => HttpListenerConfig {
423                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
424                    authenticator_kind: AuthenticatorKind::Oidc,
425                    enable_tls,
426                    routes: HttpRoutesEnabled {
427                        base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
428                        webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
429                        internal: RouteGroup::Disabled,
430                        metrics: RouteGroup::Disabled,
431                        profiling: RouteGroup::Disabled,
432                        mcp_agent: RouteGroup::Disabled,
433                        mcp_developer: RouteGroup::Disabled,
434                        console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
435                    },
436                },
437                "internal".to_owned() => HttpListenerConfig {
438                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
439                    authenticator_kind: AuthenticatorKind::None,
440                    enable_tls: false,
441                    routes: HttpRoutesEnabled {
442                        base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
443                        webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
444                        internal: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
445                        metrics: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
446                        profiling: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
447                        mcp_agent: RouteGroup::Disabled,
448                        mcp_developer: RouteGroup::Disabled,
449                        console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
450                    },
451                },
452            },
453        };
454
455        if let Some(issuer) = issuer {
456            self.system_parameter_defaults
457                .insert("oidc_issuer".to_string(), issuer);
458        }
459
460        if let Some(authentication_claim) = authentication_claim {
461            self.system_parameter_defaults.insert(
462                "oidc_authentication_claim".to_string(),
463                authentication_claim,
464            );
465        }
466
467        if let Some(expected_audiences) = expected_audiences {
468            self.system_parameter_defaults.insert(
469                "oidc_audience".to_string(),
470                serde_json::to_string(&expected_audiences).unwrap(),
471            );
472        }
473
474        if let Some(external_login_password_mz_system) = external_login_password_mz_system {
475            self.external_login_password_mz_system = Some(external_login_password_mz_system);
476            self.system_parameter_defaults
477                .insert("enable_password_auth".to_string(), "true".to_string());
478        }
479
480        self
481    }
482
483    pub fn with_password_auth(mut self, mz_system_password: Password) -> Self {
484        self.external_login_password_mz_system = Some(mz_system_password);
485        let enable_tls = self.tls.is_some();
486        self.listeners_config = ListenersConfig {
487            sql: btreemap! {
488                "external".to_owned() => SqlListenerConfig {
489                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
490                    authenticator_kind: AuthenticatorKind::Password,
491                    allowed_roles: AllowedRoles::NormalAndInternal,
492                    enable_tls,
493                },
494            },
495            http: btreemap! {
496                "external".to_owned() => HttpListenerConfig {
497                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
498                    authenticator_kind: AuthenticatorKind::Password,
499                    enable_tls,
500                    routes: HttpRoutesEnabled {
501                        base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
502                        webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
503                        internal: RouteGroup::Enabled(AllowedRoles::Internal),
504                        metrics: RouteGroup::Disabled,
505                        profiling: RouteGroup::Enabled(AllowedRoles::Internal),
506                        mcp_agent: RouteGroup::Disabled,
507                        mcp_developer: RouteGroup::Disabled,
508                        console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
509                    },
510                },
511                "metrics".to_owned() => HttpListenerConfig {
512                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
513                    authenticator_kind: AuthenticatorKind::None,
514                    enable_tls: false,
515                    routes: HttpRoutesEnabled {
516                        base: RouteGroup::Disabled,
517                        webhook: RouteGroup::Disabled,
518                        internal: RouteGroup::Disabled,
519                        metrics: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
520                        profiling: RouteGroup::Disabled,
521                        mcp_agent: RouteGroup::Disabled,
522                        mcp_developer: RouteGroup::Disabled,
523                        console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
524                    },
525                },
526            },
527        };
528        self
529    }
530
531    pub fn with_sasl_scram_auth(mut self, mz_system_password: Password) -> Self {
532        self.external_login_password_mz_system = Some(mz_system_password);
533        let enable_tls = self.tls.is_some();
534        self.listeners_config = ListenersConfig {
535            sql: btreemap! {
536                "external".to_owned() => SqlListenerConfig {
537                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
538                    authenticator_kind: AuthenticatorKind::Sasl,
539                    allowed_roles: AllowedRoles::NormalAndInternal,
540                    enable_tls,
541                },
542            },
543            http: btreemap! {
544                "external".to_owned() => HttpListenerConfig {
545                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
546                    authenticator_kind: AuthenticatorKind::Password,
547                    enable_tls,
548                    routes: HttpRoutesEnabled {
549                        base: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
550                        webhook: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
551                        internal: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
552                        metrics: RouteGroup::Disabled,
553                        profiling: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
554                        mcp_agent: RouteGroup::Disabled,
555                        mcp_developer: RouteGroup::Disabled,
556                        console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
557                    },
558                },
559                "metrics".to_owned() => HttpListenerConfig {
560                    addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
561                    authenticator_kind: AuthenticatorKind::None,
562                    enable_tls: false,
563                    routes: HttpRoutesEnabled {
564                        base: RouteGroup::Disabled,
565                        webhook: RouteGroup::Disabled,
566                        internal: RouteGroup::Disabled,
567                        metrics: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
568                        profiling: RouteGroup::Disabled,
569                        mcp_agent: RouteGroup::Disabled,
570                        mcp_developer: RouteGroup::Disabled,
571                        console_config: RouteGroup::Enabled(AllowedRoles::NormalAndInternal),
572                    },
573                },
574            },
575        };
576        self
577    }
578
579    pub fn with_now(mut self, now: NowFn) -> Self {
580        self.now = now;
581        self
582    }
583
584    pub fn with_storage_usage_collection_interval(
585        mut self,
586        storage_usage_collection_interval: Duration,
587    ) -> Self {
588        self.storage_usage_collection_interval = storage_usage_collection_interval;
589        self
590    }
591
592    pub fn with_storage_usage_retention_period(
593        mut self,
594        storage_usage_retention_period: Duration,
595    ) -> Self {
596        self.storage_usage_retention_period = Some(storage_usage_retention_period);
597        self
598    }
599
600    pub fn with_default_cluster_replica_size(
601        mut self,
602        default_cluster_replica_size: String,
603    ) -> Self {
604        self.default_cluster_replica_size = default_cluster_replica_size;
605        self
606    }
607
608    pub fn with_builtin_system_cluster_replica_size(
609        mut self,
610        builtin_system_cluster_replica_size: String,
611    ) -> Self {
612        self.builtin_system_cluster_config.size = builtin_system_cluster_replica_size;
613        self
614    }
615
616    pub fn with_builtin_system_cluster_replication_factor(
617        mut self,
618        builtin_system_cluster_replication_factor: u32,
619    ) -> Self {
620        self.builtin_system_cluster_config.replication_factor =
621            builtin_system_cluster_replication_factor;
622        self
623    }
624
625    pub fn with_builtin_support_cluster_replication_factor(
626        mut self,
627        builtin_support_cluster_replication_factor: u32,
628    ) -> Self {
629        self.builtin_support_cluster_config.replication_factor =
630            builtin_support_cluster_replication_factor;
631        self
632    }
633
634    pub fn with_builtin_catalog_server_cluster_replica_size(
635        mut self,
636        builtin_catalog_server_cluster_replica_size: String,
637    ) -> Self {
638        self.builtin_catalog_server_cluster_config.size =
639            builtin_catalog_server_cluster_replica_size;
640        self
641    }
642
643    pub fn with_propagate_crashes(mut self, propagate_crashes: bool) -> Self {
644        self.propagate_crashes = propagate_crashes;
645        self
646    }
647
648    pub fn with_enable_tracing(mut self, enable_tracing: bool) -> Self {
649        self.enable_tracing = enable_tracing;
650        self
651    }
652
653    pub fn with_bootstrap_role(mut self, bootstrap_role: Option<String>) -> Self {
654        self.bootstrap_role = bootstrap_role;
655        self
656    }
657
658    pub fn with_deploy_generation(mut self, deploy_generation: u64) -> Self {
659        self.deploy_generation = deploy_generation;
660        self
661    }
662
663    pub fn with_cors_allowed_origin(mut self, origin: &'static str) -> Self {
664        self.cors_allowed_origins
665            .push(HeaderValue::from_static(origin));
666        self
667    }
668
669    pub fn with_system_parameter_default(mut self, param: String, value: String) -> Self {
670        self.system_parameter_defaults.insert(param, value);
671        self
672    }
673
674    pub fn with_mcp_routes(mut self, agent: bool, developer: bool) -> Self {
675        for config in self.listeners_config.http.values_mut() {
676            // Match the MCP routes to the listener's `base` policy (falling back
677            // to `NormalAndInternal` if `base` is disabled, e.g. a metrics-only
678            // listener), then enable/disable them.
679            let roles = config
680                .routes
681                .base
682                .allowed_roles()
683                .unwrap_or(AllowedRoles::NormalAndInternal);
684            let group = |enabled| {
685                if enabled {
686                    RouteGroup::Enabled(roles)
687                } else {
688                    RouteGroup::Disabled
689                }
690            };
691            config.routes.mcp_agent = group(agent);
692            config.routes.mcp_developer = group(developer);
693        }
694        self
695    }
696
697    pub fn with_internal_console_redirect_url(
698        mut self,
699        internal_console_redirect_url: Option<String>,
700    ) -> Self {
701        self.internal_console_redirect_url = internal_console_redirect_url;
702        self
703    }
704
705    pub fn with_metrics_registry(mut self, registry: MetricsRegistry) -> Self {
706        self.metrics_registry = Some(registry);
707        self
708    }
709
710    pub fn with_code_version(mut self, version: semver::Version) -> Self {
711        self.code_version = version;
712        self
713    }
714
715    /// Forces every builtin storage collection through the given migration mechanism,
716    /// `"evolution"` or `"replacement"`.
717    pub fn with_force_builtin_schema_migration(mut self, mechanism: &str) -> Self {
718        self.force_builtin_schema_migration = Some(mechanism.into());
719        self
720    }
721
722    pub fn with_capture(mut self, storage: SharedStorage) -> Self {
723        self.capture = Some(storage);
724        self
725    }
726}
727
728pub struct Listeners {
729    pub inner: crate::Listeners,
730}
731
732impl Listeners {
733    pub async fn new(config: &TestHarness) -> Result<Listeners, anyhow::Error> {
734        let inner = crate::Listeners::bind(config.listeners_config.clone()).await?;
735        Ok(Listeners { inner })
736    }
737
738    pub async fn serve(self, config: TestHarness) -> Result<TestServer, anyhow::Error> {
739        self.serve_with_trigger(config, mz_server_core::cert_reload_never_reload())
740            .await
741    }
742
743    pub async fn serve_with_trigger(
744        self,
745        config: TestHarness,
746        tls_reload_certs: ReloadTrigger,
747    ) -> Result<TestServer, anyhow::Error> {
748        let (data_directory, temp_dir) = match config.data_directory {
749            None => {
750                // If no data directory is provided, we create a temporary
751                // directory. The temporary directory is cleaned up when the
752                // `TempDir` is dropped, so we keep it alive until the `Server` is
753                // dropped.
754                let temp_dir = tempfile::tempdir()?;
755                (temp_dir.path().to_path_buf(), Some(temp_dir))
756            }
757            Some(data_directory) => (data_directory, None),
758        };
759        let scratch_dir = tempfile::tempdir()?;
760        let (consensus_uri, timestamp_oracle_url) = {
761            let seed = config.seed;
762            let cockroach_url = env::var("METADATA_BACKEND_URL")
763                .map_err(|_| anyhow!("METADATA_BACKEND_URL environment variable is not set"))?;
764            let (client, conn) = tokio_postgres::connect(&cockroach_url, NoTls).await?;
765            mz_ore::task::spawn(|| "startup-postgres-conn", async move {
766                if let Err(err) = conn.await {
767                    panic!("connection error: {}", err);
768                };
769            });
770            let consensus_schema = sql!("consensus_{}", seed);
771            let tsoracle_schema = sql!("tsoracle_{}", seed);
772            pg_batch_execute(
773                &client,
774                sql!(
775                    "CREATE SCHEMA IF NOT EXISTS {};
776                     CREATE SCHEMA IF NOT EXISTS {};",
777                    consensus_schema,
778                    tsoracle_schema,
779                ),
780            )
781            .await?;
782            (
783                format!("{cockroach_url}?options=--search_path=consensus_{seed}")
784                    .parse()
785                    .expect("invalid consensus URI"),
786                format!("{cockroach_url}?options=--search_path=tsoracle_{seed}")
787                    .parse()
788                    .expect("invalid timestamp oracle URI"),
789            )
790        };
791        let metrics_registry = config.metrics_registry.unwrap_or_else(MetricsRegistry::new);
792        let orchestrator = ProcessOrchestrator::new(ProcessOrchestratorConfig {
793            image_dir: env::current_exe()?
794                .parent()
795                .unwrap()
796                .parent()
797                .unwrap()
798                .to_path_buf(),
799            suppress_output: false,
800            environment_id: config.environment_id.to_string(),
801            secrets_dir: data_directory.join("secrets"),
802            command_wrapper: vec![],
803            propagate_crashes: config.propagate_crashes,
804            tcp_proxy: None,
805            scratch_directory: scratch_dir.path().to_path_buf(),
806        })
807        .await?;
808        let orchestrator = Arc::new(orchestrator);
809        // Messing with the clock causes persist to expire leases, causing hangs and
810        // panics. Is it possible/desirable to put this back somehow?
811        let persist_now = SYSTEM_TIME.clone();
812        let dyncfgs = mz_dyncfgs::all_dyncfgs();
813
814        let mut updates = ConfigUpdates::default();
815        // Tune down the number of connections to make this all work a little easier
816        // with local postgres.
817        updates.add(&CONSENSUS_CONNECTION_POOL_MAX_SIZE, 1);
818        updates.apply(&dyncfgs);
819
820        let mut persist_cfg = PersistConfig::new(&crate::BUILD_INFO, persist_now.clone(), dyncfgs);
821        persist_cfg.build_version = config.code_version;
822        // Stress persist more by writing rollups frequently
823        persist_cfg.set_rollup_threshold(5);
824
825        let persist_pubsub_server = PersistGrpcPubSubServer::new(&persist_cfg, &metrics_registry);
826        let persist_pubsub_client = persist_pubsub_server.new_same_process_connection();
827        let persist_pubsub_tcp_listener =
828            TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))
829                .await
830                .expect("pubsub addr binding");
831        let persist_pubsub_server_port = persist_pubsub_tcp_listener
832            .local_addr()
833            .expect("pubsub addr has local addr")
834            .port();
835
836        // Spawn the persist pub-sub server.
837        mz_ore::task::spawn(|| "persist_pubsub_server", async move {
838            persist_pubsub_server
839                .serve_with_stream(TcpListenerStream::new(persist_pubsub_tcp_listener))
840                .await
841                .expect("success")
842        });
843        let persist_clients =
844            PersistClientCache::new(persist_cfg, &metrics_registry, |_, _| persist_pubsub_client);
845        let persist_clients = Arc::new(persist_clients);
846        let system_dyncfgs = Arc::clone(&persist_clients.cfg().configs);
847
848        let secrets_controller = Arc::clone(&orchestrator);
849        let mut connection_context = ConnectionContext::for_tests(orchestrator.reader());
850        if !config.aws_connection_context {
851            connection_context.aws_external_id_prefix = None;
852            connection_context.aws_connection_role_arn = None;
853        }
854        let orchestrator = Arc::new(TracingOrchestrator::new(
855            orchestrator,
856            config.orchestrator_tracing_cli_args,
857        ));
858        let tracing_handle = if config.enable_tracing {
859            let config = TracingConfig::<fn(&tracing::Metadata) -> sentry_tracing::EventFilter> {
860                service_name: "environmentd",
861                stderr_log: StderrLogConfig {
862                    format: StderrLogFormat::Json,
863                    filter: EnvFilter::default(),
864                },
865                opentelemetry: Some(OpenTelemetryConfig {
866                    endpoint: "http://fake_address_for_testing:8080".to_string(),
867                    headers: http::HeaderMap::new(),
868                    filter: EnvFilter::default().add_directive(Level::DEBUG.into()),
869                    resource: opentelemetry_sdk::resource::Resource::builder().build(),
870                    max_batch_queue_size: 2048,
871                    max_export_batch_size: 512,
872                    max_concurrent_exports: 1,
873                    batch_scheduled_delay: Duration::from_millis(5000),
874                    max_export_timeout: Duration::from_secs(30),
875                }),
876                tokio_console: None,
877                sentry: None,
878                build_version: crate::BUILD_INFO.version,
879                build_sha: crate::BUILD_INFO.sha,
880                registry: metrics_registry.clone(),
881                capture: config.capture,
882            };
883            mz_ore::tracing::configure(config).await?
884        } else {
885            TracingHandle::disabled()
886        };
887        let host_name = format!(
888            "localhost:{}",
889            self.inner.http["external"].handle.local_addr.port()
890        );
891        let catalog_config = CatalogConfig {
892            persist_clients: Arc::clone(&persist_clients),
893            metrics: Arc::new(mz_catalog::durable::Metrics::new(&MetricsRegistry::new())),
894        };
895
896        let inner = self
897            .inner
898            .serve(crate::Config {
899                catalog_config,
900                timestamp_oracle_url: Some(timestamp_oracle_url),
901                controller: ControllerConfig {
902                    build_info: &crate::BUILD_INFO,
903                    orchestrator,
904                    clusterd_image: "clusterd".into(),
905                    init_container_image: None,
906                    deploy_generation: config.deploy_generation,
907                    persist_location: PersistLocation {
908                        blob_uri: format!("file://{}/persist/blob", data_directory.display())
909                            .parse()
910                            .expect("invalid blob URI"),
911                        consensus_uri,
912                    },
913                    persist_clients,
914                    now: config.now.clone(),
915                    metrics_registry: metrics_registry.clone(),
916                    persist_pubsub_url: format!("http://localhost:{}", persist_pubsub_server_port),
917                    secrets_args: mz_secrets_cli::SecretsReaderCliArgs {
918                        secrets_reader: mz_secrets_cli::SecretsControllerKind::LocalFile,
919                        secrets_reader_local_file_dir: Some(data_directory.join("secrets")),
920                        secrets_reader_kubernetes_context: None,
921                        secrets_reader_aws_prefix: None,
922                        secrets_reader_name_prefix: None,
923                    },
924                    connection_context,
925                    replica_http_locator: Default::default(),
926                },
927                secrets_controller,
928                cloud_resource_controller: None,
929                system_dyncfgs,
930                tls: config.tls,
931                frontegg: config.frontegg,
932                frontegg_oauth_issuer_url: None,
933                unsafe_mode: config.unsafe_mode,
934                all_features: false,
935                metrics_registry: metrics_registry.clone(),
936                now: config.now,
937                environment_id: config.environment_id,
938                cors_allowed_origin: AllowOrigin::list(config.cors_allowed_origins.clone()),
939                cors_allowed_origin_list: config.cors_allowed_origins.clone(),
940                cluster_replica_sizes: ClusterReplicaSizeMap::for_tests(),
941                bootstrap_default_cluster_replica_size: config.default_cluster_replica_size,
942                bootstrap_default_cluster_replication_factor: config
943                    .default_cluster_replication_factor,
944                bootstrap_builtin_system_cluster_config: config.builtin_system_cluster_config,
945                bootstrap_builtin_catalog_server_cluster_config: config
946                    .builtin_catalog_server_cluster_config,
947                bootstrap_builtin_probe_cluster_config: config.builtin_probe_cluster_config,
948                bootstrap_builtin_support_cluster_config: config.builtin_support_cluster_config,
949                bootstrap_builtin_analytics_cluster_config: config.builtin_analytics_cluster_config,
950                system_parameter_defaults: config.system_parameter_defaults,
951                availability_zones: Default::default(),
952                tracing_handle,
953                storage_usage_collection_interval: config.storage_usage_collection_interval,
954                storage_usage_retention_period: config.storage_usage_retention_period,
955                segment_api_key: None,
956                segment_client_side: false,
957                test_only_dummy_segment_client: false,
958                egress_addresses: vec![],
959                aws_account_id: None,
960                aws_privatelink_availability_zones: None,
961                launchdarkly_sdk_key: None,
962                launchdarkly_base_uri: None,
963                launchdarkly_key_map: Default::default(),
964                config_sync_file_path: None,
965                config_sync_timeout: Duration::from_secs(30),
966                config_sync_loop_interval: None,
967                bootstrap_role: config.bootstrap_role,
968                http_host_name: Some(host_name),
969                internal_console_redirect_url: config.internal_console_redirect_url,
970                tls_reload_certs,
971                helm_chart_version: None,
972                license_key: ValidatedLicenseKey::for_tests(),
973                external_login_password_mz_system: config.external_login_password_mz_system,
974                force_builtin_schema_migration: config.force_builtin_schema_migration,
975            })
976            .await?;
977
978        Ok(TestServer {
979            inner,
980            metrics_registry,
981            _temp_dir: temp_dir,
982            _scratch_dir: scratch_dir,
983        })
984    }
985}
986
987/// A running instance of `environmentd`.
988pub struct TestServer {
989    pub inner: crate::Server,
990    pub metrics_registry: MetricsRegistry,
991    /// The `TempDir`s are saved to prevent them from being dropped, and thus cleaned up too early.
992    _temp_dir: Option<TempDir>,
993    _scratch_dir: TempDir,
994}
995
996impl TestServer {
997    pub fn connect(&self) -> ConnectBuilder<'_, postgres::NoTls, NoHandle> {
998        ConnectBuilder::new(self).no_tls()
999    }
1000
1001    pub async fn enable_feature_flags(&self, flags: &[&'static str]) {
1002        let internal_client = self.connect().internal().await.unwrap();
1003
1004        for flag in flags {
1005            let query = sql!("ALTER SYSTEM SET {} = true;", Sql::ident(flag));
1006            pg_batch_execute(&internal_client, query).await.unwrap();
1007        }
1008    }
1009
1010    pub async fn disable_feature_flags(&self, flags: &[&'static str]) {
1011        let internal_client = self.connect().internal().await.unwrap();
1012
1013        for flag in flags {
1014            let query = sql!("ALTER SYSTEM SET {} = false;", Sql::ident(flag));
1015            pg_batch_execute(&internal_client, query).await.unwrap();
1016        }
1017    }
1018
1019    pub fn ws_addr(&self) -> Uri {
1020        format!(
1021            "ws://{}/api/experimental/sql",
1022            self.inner.http_listener_handles["external"].local_addr
1023        )
1024        .parse()
1025        .unwrap()
1026    }
1027
1028    pub fn internal_ws_addr(&self) -> Uri {
1029        format!(
1030            "ws://{}/api/experimental/sql",
1031            self.inner.http_listener_handles["internal"].local_addr
1032        )
1033        .parse()
1034        .unwrap()
1035    }
1036
1037    pub fn http_local_addr(&self) -> SocketAddr {
1038        self.inner.http_listener_handles["external"].local_addr
1039    }
1040
1041    pub fn internal_http_local_addr(&self) -> SocketAddr {
1042        self.inner.http_listener_handles["internal"].local_addr
1043    }
1044
1045    pub fn sql_local_addr(&self) -> SocketAddr {
1046        self.inner.sql_listener_handles["external"].local_addr
1047    }
1048
1049    pub fn internal_sql_local_addr(&self) -> SocketAddr {
1050        self.inner.sql_listener_handles["internal"].local_addr
1051    }
1052}
1053
1054/// A builder struct to configure a pgwire connection to a running [`TestServer`].
1055///
1056/// You can create this struct, and thus open a pgwire connection, using [`TestServer::connect`].
1057pub struct ConnectBuilder<'s, T, H> {
1058    /// A running `environmentd` test server.
1059    server: &'s TestServer,
1060
1061    /// Postgres configuration for connecting to the test server.
1062    pg_config: tokio_postgres::Config,
1063    /// Port to use when connecting to the test server.
1064    port: u16,
1065    /// Tls settings to use.
1066    tls: T,
1067
1068    /// Callback that gets invoked for every notice we receive.
1069    notice_callback: Option<Box<dyn FnMut(tokio_postgres::error::DbError) + Send + 'static>>,
1070
1071    /// Type variable for whether or not we include the handle for the spawned [`tokio::task`].
1072    _with_handle: H,
1073}
1074
1075impl<'s> ConnectBuilder<'s, (), NoHandle> {
1076    fn new(server: &'s TestServer) -> Self {
1077        let mut pg_config = tokio_postgres::Config::new();
1078        pg_config
1079            .host(&Ipv4Addr::LOCALHOST.to_string())
1080            .user("materialize")
1081            .options("--welcome_message=off")
1082            .application_name("environmentd_test_framework");
1083
1084        ConnectBuilder {
1085            server,
1086            pg_config,
1087            port: server.sql_local_addr().port(),
1088            tls: (),
1089            notice_callback: None,
1090            _with_handle: NoHandle,
1091        }
1092    }
1093}
1094
1095impl<'s, T, H> ConnectBuilder<'s, T, H> {
1096    /// Create a pgwire connection without using TLS.
1097    ///
1098    /// Note: this is the default for all connections.
1099    pub fn no_tls(self) -> ConnectBuilder<'s, postgres::NoTls, H> {
1100        ConnectBuilder {
1101            server: self.server,
1102            pg_config: self.pg_config,
1103            port: self.port,
1104            tls: postgres::NoTls,
1105            notice_callback: self.notice_callback,
1106            _with_handle: self._with_handle,
1107        }
1108    }
1109
1110    /// Create a pgwire connection with TLS.
1111    pub fn with_tls<Tls>(self, tls: Tls) -> ConnectBuilder<'s, Tls, H>
1112    where
1113        Tls: MakeTlsConnect<Socket> + Send + 'static,
1114        Tls::TlsConnect: Send,
1115        Tls::Stream: Send,
1116        <Tls::TlsConnect as TlsConnect<Socket>>::Future: Send,
1117    {
1118        ConnectBuilder {
1119            server: self.server,
1120            pg_config: self.pg_config,
1121            port: self.port,
1122            tls,
1123            notice_callback: self.notice_callback,
1124            _with_handle: self._with_handle,
1125        }
1126    }
1127
1128    /// Create a [`ConnectBuilder`] using the provided [`tokio_postgres::Config`].
1129    pub fn with_config(mut self, pg_config: tokio_postgres::Config) -> Self {
1130        self.pg_config = pg_config;
1131        self
1132    }
1133
1134    /// Set the [`SslMode`] to be used with the resulting connection.
1135    pub fn ssl_mode(mut self, mode: SslMode) -> Self {
1136        self.pg_config.ssl_mode(mode);
1137        self
1138    }
1139
1140    /// Set the user for the pgwire connection.
1141    pub fn user(mut self, user: &str) -> Self {
1142        self.pg_config.user(user);
1143        self
1144    }
1145
1146    /// Set the password for the pgwire connection.
1147    pub fn password(mut self, password: &str) -> Self {
1148        self.pg_config.password(password);
1149        self
1150    }
1151
1152    /// Set the application name for the pgwire connection.
1153    pub fn application_name(mut self, application_name: &str) -> Self {
1154        self.pg_config.application_name(application_name);
1155        self
1156    }
1157
1158    /// Set the database name for the pgwire connection.
1159    pub fn dbname(mut self, dbname: &str) -> Self {
1160        self.pg_config.dbname(dbname);
1161        self
1162    }
1163
1164    /// Set the options for the pgwire connection.
1165    pub fn options(mut self, options: &str) -> Self {
1166        self.pg_config.options(options);
1167        self
1168    }
1169
1170    /// Configures this [`ConnectBuilder`] to connect to the __internal__ SQL port of the running
1171    /// [`TestServer`].
1172    ///
1173    /// For example, this will change the port we connect to, and the user we connect as.
1174    pub fn internal(mut self) -> Self {
1175        self.port = self.server.internal_sql_local_addr().port();
1176        self.pg_config.user(mz_sql::session::user::SYSTEM_USER_NAME);
1177        self
1178    }
1179
1180    /// Sets a callback for any database notices that are received from the [`TestServer`].
1181    pub fn notice_callback(self, callback: impl FnMut(DbError) + Send + 'static) -> Self {
1182        ConnectBuilder {
1183            notice_callback: Some(Box::new(callback)),
1184            ..self
1185        }
1186    }
1187
1188    /// Configures this [`ConnectBuilder`] to return the [`mz_ore::task::JoinHandle`] that is
1189    /// polling the underlying postgres connection, associated with the returned client.
1190    pub fn with_handle(self) -> ConnectBuilder<'s, T, WithHandle> {
1191        ConnectBuilder {
1192            server: self.server,
1193            pg_config: self.pg_config,
1194            port: self.port,
1195            tls: self.tls,
1196            notice_callback: self.notice_callback,
1197            _with_handle: WithHandle,
1198        }
1199    }
1200
1201    /// Returns the [`tokio_postgres::Config`] that will be used to connect.
1202    pub fn as_pg_config(&self) -> &tokio_postgres::Config {
1203        &self.pg_config
1204    }
1205}
1206
1207/// This trait enables us to either include or omit the [`mz_ore::task::JoinHandle`] in the result
1208/// of a client connection.
1209pub trait IncludeHandle: Send {
1210    type Output;
1211    fn transform_result(
1212        client: tokio_postgres::Client,
1213        handle: mz_ore::task::JoinHandle<()>,
1214    ) -> Self::Output;
1215}
1216
1217/// Type parameter that denotes we __will not__ return the [`mz_ore::task::JoinHandle`] in the
1218/// result of a [`ConnectBuilder`].
1219pub struct NoHandle;
1220impl IncludeHandle for NoHandle {
1221    type Output = tokio_postgres::Client;
1222    fn transform_result(
1223        client: tokio_postgres::Client,
1224        _handle: mz_ore::task::JoinHandle<()>,
1225    ) -> Self::Output {
1226        client
1227    }
1228}
1229
1230/// Type parameter that denotes we __will__ return the [`mz_ore::task::JoinHandle`] in the result of
1231/// a [`ConnectBuilder`].
1232pub struct WithHandle;
1233impl IncludeHandle for WithHandle {
1234    type Output = (tokio_postgres::Client, mz_ore::task::JoinHandle<()>);
1235    fn transform_result(
1236        client: tokio_postgres::Client,
1237        handle: mz_ore::task::JoinHandle<()>,
1238    ) -> Self::Output {
1239        (client, handle)
1240    }
1241}
1242
1243impl<'s, T, H> IntoFuture for ConnectBuilder<'s, T, H>
1244where
1245    T: MakeTlsConnect<Socket> + Send + 'static,
1246    T::TlsConnect: Send,
1247    T::Stream: Send,
1248    <T::TlsConnect as TlsConnect<Socket>>::Future: Send,
1249    H: IncludeHandle,
1250{
1251    type Output = Result<H::Output, postgres::Error>;
1252    type IntoFuture = BoxFuture<'static, Self::Output>;
1253
1254    fn into_future(mut self) -> Self::IntoFuture {
1255        Box::pin(async move {
1256            assert!(
1257                self.pg_config.get_ports().is_empty(),
1258                "specifying multiple ports is not supported"
1259            );
1260            self.pg_config.port(self.port);
1261
1262            let (client, mut conn) = self.pg_config.connect(self.tls).await?;
1263            let mut notice_callback = self.notice_callback.take();
1264
1265            let handle = task::spawn(|| "connect", async move {
1266                while let Some(msg) = std::future::poll_fn(|cx| conn.poll_message(cx)).await {
1267                    match msg {
1268                        Ok(AsyncMessage::Notice(notice)) => {
1269                            if let Some(callback) = notice_callback.as_mut() {
1270                                callback(notice);
1271                            }
1272                        }
1273                        Ok(msg) => {
1274                            tracing::debug!(?msg, "Dropping message from database");
1275                        }
1276                        Err(e) => {
1277                            // tokio_postgres::Connection docs say:
1278                            // > Return values of None or Some(Err(_)) are “terminal”; callers
1279                            // > should not invoke this method again after receiving one of those
1280                            // > values.
1281                            tracing::info!("connection error: {e}");
1282                            break;
1283                        }
1284                    }
1285                }
1286                tracing::info!("connection closed");
1287            });
1288
1289            let output = H::transform_result(client, handle);
1290            Ok(output)
1291        })
1292    }
1293}
1294
1295/// A running instance of `environmentd`, that exposes blocking/synchronous test helpers.
1296///
1297/// Note: Ideally you should use a [`TestServer`] which relies on an external runtime, e.g. the
1298/// [`tokio::test`] macro. This struct exists so we can incrementally migrate our existing tests.
1299pub struct TestServerWithRuntime {
1300    server: TestServer,
1301    runtime: Arc<Runtime>,
1302}
1303
1304impl TestServerWithRuntime {
1305    /// Returns the [`Runtime`] owned by this [`TestServerWithRuntime`].
1306    ///
1307    /// Can be used to spawn async tasks.
1308    pub fn runtime(&self) -> &Arc<Runtime> {
1309        &self.runtime
1310    }
1311
1312    /// Returns a referece to the inner running `environmentd` [`crate::Server`]`.
1313    pub fn inner(&self) -> &crate::Server {
1314        &self.server.inner
1315    }
1316
1317    /// Connect to the __public__ SQL port of the running `environmentd` server.
1318    pub fn connect<T>(&self, tls: T) -> Result<postgres::Client, postgres::Error>
1319    where
1320        T: MakeTlsConnect<Socket> + Send + 'static,
1321        T::TlsConnect: Send,
1322        T::Stream: Send,
1323        <T::TlsConnect as TlsConnect<Socket>>::Future: Send,
1324    {
1325        self.pg_config().connect(tls)
1326    }
1327
1328    /// Connect to the __internal__ SQL port of the running `environmentd` server.
1329    pub fn connect_internal<T>(&self, tls: T) -> Result<postgres::Client, anyhow::Error>
1330    where
1331        T: MakeTlsConnect<Socket> + Send + 'static,
1332        T::TlsConnect: Send,
1333        T::Stream: Send,
1334        <T::TlsConnect as TlsConnect<Socket>>::Future: Send,
1335    {
1336        Ok(self.pg_config_internal().connect(tls)?)
1337    }
1338
1339    /// Enable LaunchDarkly feature flags.
1340    pub fn enable_feature_flags(&self, flags: &[&'static str]) {
1341        let mut internal_client = self.connect_internal(postgres::NoTls).unwrap();
1342
1343        for flag in flags {
1344            let query = sql!("ALTER SYSTEM SET {} = true;", Sql::ident(flag));
1345            // This uses the synchronous `postgres::Client`; wrappers are async
1346            // and currently only defined for tokio-postgres clients.
1347            #[allow(clippy::disallowed_methods)]
1348            internal_client.batch_execute(query.as_str()).unwrap();
1349        }
1350    }
1351
1352    /// Disable LaunchDarkly feature flags.
1353    pub fn disable_feature_flags(&self, flags: &[&'static str]) {
1354        let mut internal_client = self.connect_internal(postgres::NoTls).unwrap();
1355
1356        for flag in flags {
1357            let query = sql!("ALTER SYSTEM SET {} = false;", Sql::ident(flag));
1358            // This uses the synchronous `postgres::Client`; wrappers are async
1359            // and currently only defined for tokio-postgres clients.
1360            #[allow(clippy::disallowed_methods)]
1361            internal_client.batch_execute(query.as_str()).unwrap();
1362        }
1363    }
1364
1365    /// Return a [`postgres::Config`] for connecting to the __public__ SQL port of the running
1366    /// `environmentd` server.
1367    pub fn pg_config(&self) -> postgres::Config {
1368        let local_addr = self.server.sql_local_addr();
1369        let mut config = postgres::Config::new();
1370        config
1371            .host(&Ipv4Addr::LOCALHOST.to_string())
1372            .port(local_addr.port())
1373            .user("materialize")
1374            .options("--welcome_message=off");
1375        config
1376    }
1377
1378    /// Return a [`postgres::Config`] for connecting to the __internal__ SQL port of the running
1379    /// `environmentd` server.
1380    pub fn pg_config_internal(&self) -> postgres::Config {
1381        let local_addr = self.server.internal_sql_local_addr();
1382        let mut config = postgres::Config::new();
1383        config
1384            .host(&Ipv4Addr::LOCALHOST.to_string())
1385            .port(local_addr.port())
1386            .user("mz_system")
1387            .options("--welcome_message=off");
1388        config
1389    }
1390
1391    pub fn ws_addr(&self) -> Uri {
1392        self.server.ws_addr()
1393    }
1394
1395    pub fn internal_ws_addr(&self) -> Uri {
1396        self.server.internal_ws_addr()
1397    }
1398
1399    pub fn http_local_addr(&self) -> SocketAddr {
1400        self.server.http_local_addr()
1401    }
1402
1403    pub fn internal_http_local_addr(&self) -> SocketAddr {
1404        self.server.internal_http_local_addr()
1405    }
1406
1407    pub fn sql_local_addr(&self) -> SocketAddr {
1408        self.server.sql_local_addr()
1409    }
1410
1411    pub fn internal_sql_local_addr(&self) -> SocketAddr {
1412        self.server.internal_sql_local_addr()
1413    }
1414
1415    /// Returns the metrics registry for the test server.
1416    pub fn metrics_registry(&self) -> &MetricsRegistry {
1417        &self.server.metrics_registry
1418    }
1419}
1420
1421#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
1422pub struct MzTimestamp(pub u64);
1423
1424impl<'a> FromSql<'a> for MzTimestamp {
1425    fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<MzTimestamp, Box<dyn Error + Sync + Send>> {
1426        let n = mz_pgrepr::Numeric::from_sql(ty, raw)?;
1427        Ok(MzTimestamp(u64::try_from(n.0.0)?))
1428    }
1429
1430    fn accepts(ty: &Type) -> bool {
1431        mz_pgrepr::Numeric::accepts(ty)
1432    }
1433}
1434
1435pub trait PostgresErrorExt {
1436    fn unwrap_db_error(self) -> DbError;
1437}
1438
1439impl PostgresErrorExt for postgres::Error {
1440    fn unwrap_db_error(self) -> DbError {
1441        match self.source().and_then(|e| e.downcast_ref::<DbError>()) {
1442            Some(e) => e.clone(),
1443            None => panic!("expected DbError, but got: {:?}", self),
1444        }
1445    }
1446}
1447
1448impl<T, E> PostgresErrorExt for Result<T, E>
1449where
1450    E: PostgresErrorExt,
1451{
1452    fn unwrap_db_error(self) -> DbError {
1453        match self {
1454            Ok(_) => panic!("expected Err(DbError), but got Ok(_)"),
1455            Err(e) => e.unwrap_db_error(),
1456        }
1457    }
1458}
1459
1460/// Group commit will block writes until the current time has advanced. This can make
1461/// performing inserts while using deterministic time difficult. This is a helper
1462/// method to perform writes and advance the current time.
1463pub async fn insert_with_deterministic_timestamps(
1464    table: &'static str,
1465    values: &'static str,
1466    server: &TestServer,
1467    now: Arc<std::sync::Mutex<EpochMillis>>,
1468) -> Result<(), Box<dyn Error>> {
1469    let client_write = server.connect().await?;
1470    let client_read = server.connect().await?;
1471
1472    let mut current_ts = get_explain_timestamp(table, &client_read).await;
1473
1474    let insert_query = format!("INSERT INTO {} VALUES {values}", Sql::ident(table));
1475
1476    // The `values` fragment is raw SQL text in test code and cannot currently
1477    // be represented as a composable `Sql` fragment.
1478    #[allow(clippy::disallowed_methods)]
1479    let write_future = client_write.execute(&insert_query, &[]);
1480    let timestamp_interval = tokio::time::interval(Duration::from_millis(1));
1481
1482    let mut write_future = std::pin::pin!(write_future);
1483    let mut timestamp_interval = std::pin::pin!(timestamp_interval);
1484
1485    // Keep increasing `now` until the write has executed succeed. Table advancements may
1486    // have increased the global timestamp by an unknown amount.
1487    loop {
1488        tokio::select! {
1489            _ = (&mut write_future) => return Ok(()),
1490            _ = timestamp_interval.tick() => {
1491                current_ts += 1;
1492                *now.lock().expect("lock poisoned") = current_ts;
1493            }
1494        };
1495    }
1496}
1497
1498pub async fn get_explain_timestamp(from_suffix: &str, client: &Client) -> EpochMillis {
1499    try_get_explain_timestamp(from_suffix, client)
1500        .await
1501        .unwrap()
1502}
1503
1504pub async fn try_get_explain_timestamp(
1505    from_suffix: &str,
1506    client: &Client,
1507) -> Result<EpochMillis, anyhow::Error> {
1508    let det = get_explain_timestamp_determination(from_suffix, client).await?;
1509    let ts = det.determination.timestamp_context.timestamp_or_default();
1510    Ok(ts.into())
1511}
1512
1513pub async fn get_explain_timestamp_determination(
1514    from_suffix: &str,
1515    client: &Client,
1516) -> Result<TimestampExplanation, anyhow::Error> {
1517    // `from_suffix` is a raw SQL suffix used by this test helper and cannot
1518    // currently be represented as a composable `Sql` fragment.
1519    #[allow(clippy::disallowed_methods)]
1520    let row = client
1521        .query_one(
1522            &format!("EXPLAIN TIMESTAMP AS JSON FOR SELECT * FROM {from_suffix}"),
1523            &[],
1524        )
1525        .await?;
1526    let explain: String = row.get(0);
1527    Ok(serde_json::from_str(&explain).unwrap())
1528}
1529
1530/// Helper function to create a Postgres source.
1531///
1532/// IMPORTANT: Make sure to call closure that is returned at the end of the test to clean up
1533/// Postgres state.
1534///
1535/// WARNING: If multiple tests use this, and the tests are run in parallel, then make sure the test
1536/// use different postgres tables.
1537pub async fn create_postgres_source_with_table<'a>(
1538    server: &TestServer,
1539    mz_client: &Client,
1540    table_name: &str,
1541    table_schema: &str,
1542    source_name: &str,
1543) -> (
1544    Client,
1545    impl FnOnce(&'a Client, &'a Client) -> LocalBoxFuture<'a, ()>,
1546) {
1547    server
1548        .enable_feature_flags(&["enable_create_table_from_source"])
1549        .await;
1550
1551    let postgres_url = env::var("POSTGRES_URL")
1552        .map_err(|_| anyhow!("POSTGRES_URL environment variable is not set"))
1553        .unwrap();
1554
1555    let (pg_client, connection) = tokio_postgres::connect(&postgres_url, postgres::NoTls)
1556        .await
1557        .unwrap();
1558
1559    let pg_config: tokio_postgres::Config = postgres_url.parse().unwrap();
1560    let user = pg_config.get_user().unwrap_or("postgres");
1561    let db_name = pg_config.get_dbname().unwrap_or(user);
1562    let ports = pg_config.get_ports();
1563    let port = if ports.is_empty() { 5432 } else { ports[0] };
1564    let hosts = pg_config.get_hosts();
1565    let host = if hosts.is_empty() {
1566        "localhost".to_string()
1567    } else {
1568        match &hosts[0] {
1569            Host::Tcp(host) => host.to_string(),
1570            Host::Unix(host) => host.to_str().unwrap().to_string(),
1571        }
1572    };
1573    let password = pg_config.get_password();
1574
1575    mz_ore::task::spawn(|| "postgres-source-connection", async move {
1576        if let Err(e) = connection.await {
1577            panic!("connection error: {}", e);
1578        }
1579    });
1580
1581    // Create table in Postgres with publication.
1582    let _ = pg_execute(
1583        &pg_client,
1584        sql!("DROP TABLE IF EXISTS {};", Sql::ident(table_name)),
1585        &[],
1586    )
1587    .await
1588    .unwrap();
1589    let _ = pg_execute(
1590        &pg_client,
1591        sql!("DROP PUBLICATION IF EXISTS {};", Sql::ident(source_name)),
1592        &[],
1593    )
1594    .await
1595    .unwrap();
1596    // `table_schema` is a raw schema fragment in this test helper and cannot
1597    // currently be represented as a composable `Sql` fragment.
1598    #[allow(clippy::disallowed_methods)]
1599    let _ = pg_client
1600        .execute(
1601            format!("CREATE TABLE {} {table_schema};", Sql::ident(table_name)).as_str(),
1602            &[],
1603        )
1604        .await
1605        .unwrap();
1606    let _ = pg_execute(
1607        &pg_client,
1608        sql!(
1609            "ALTER TABLE {} REPLICA IDENTITY FULL;",
1610            Sql::ident(table_name)
1611        ),
1612        &[],
1613    )
1614    .await
1615    .unwrap();
1616    let _ = pg_execute(
1617        &pg_client,
1618        sql!(
1619            "CREATE PUBLICATION {} FOR TABLE {};",
1620            Sql::ident(source_name),
1621            Sql::ident(table_name)
1622        ),
1623        &[],
1624    )
1625    .await
1626    .unwrap();
1627
1628    // Create postgres source in Materialize.
1629    let mut connection_str = format!("HOST '{host}', PORT {port}, USER {user}, DATABASE {db_name}");
1630    if let Some(password) = password {
1631        let password = std::str::from_utf8(password).unwrap();
1632        pg_batch_execute(
1633            mz_client,
1634            sql!("CREATE SECRET s AS {}", Sql::literal(password)),
1635        )
1636        .await
1637        .unwrap();
1638        connection_str = format!("{connection_str}, PASSWORD SECRET s");
1639    }
1640    // `connection_str` is a raw connection-option fragment generated for tests
1641    // and cannot currently be represented as a composable `Sql` fragment.
1642    #[allow(clippy::disallowed_methods)]
1643    mz_client
1644        .batch_execute(format!("CREATE CONNECTION pgconn TO POSTGRES ({connection_str})").as_str())
1645        .await
1646        .unwrap();
1647    pg_batch_execute(
1648        mz_client,
1649        sql!(
1650            "CREATE SOURCE {} \
1651             FROM POSTGRES \
1652             CONNECTION pgconn \
1653             (PUBLICATION {})",
1654            Sql::ident(source_name),
1655            Sql::literal(source_name),
1656        ),
1657    )
1658    .await
1659    .unwrap();
1660    pg_batch_execute(
1661        mz_client,
1662        sql!(
1663            "CREATE TABLE {} \
1664             FROM SOURCE {} \
1665             (REFERENCE {});",
1666            Sql::ident(table_name),
1667            Sql::ident(source_name),
1668            Sql::ident(table_name),
1669        ),
1670    )
1671    .await
1672    .unwrap();
1673
1674    let table_name = table_name.to_string();
1675    let source_name = source_name.to_string();
1676    (
1677        pg_client,
1678        move |mz_client: &'a Client, pg_client: &'a Client| {
1679            let f: Pin<Box<dyn Future<Output = ()> + 'a>> = Box::pin(async move {
1680                pg_batch_execute(
1681                    mz_client,
1682                    sql!("DROP SOURCE {} CASCADE;", Sql::ident(&source_name)),
1683                )
1684                .await
1685                .unwrap();
1686                pg_batch_execute(mz_client, sql!("DROP CONNECTION pgconn;"))
1687                    .await
1688                    .unwrap();
1689
1690                let _ = pg_execute(
1691                    pg_client,
1692                    sql!("DROP PUBLICATION {};", Sql::ident(&source_name)),
1693                    &[],
1694                )
1695                .await
1696                .unwrap();
1697                let _ = pg_execute(
1698                    pg_client,
1699                    sql!("DROP TABLE {};", Sql::ident(&table_name)),
1700                    &[],
1701                )
1702                .await
1703                .unwrap();
1704            });
1705            f
1706        },
1707    )
1708}
1709
1710pub async fn wait_for_pg_table_population(mz_client: &Client, view_name: &str, source_rows: i64) {
1711    let current_isolation = pg_query_one(mz_client, sql!("SHOW transaction_isolation"), &[])
1712        .await
1713        .unwrap()
1714        .get::<_, String>(0);
1715    pg_batch_execute(mz_client, sql!("SET transaction_isolation = SERIALIZABLE"))
1716        .await
1717        .unwrap();
1718    Retry::default()
1719        .retry_async(|_| async move {
1720            let rows = pg_query_one(
1721                mz_client,
1722                sql!("SELECT COUNT(*) FROM {};", Sql::ident(view_name)),
1723                &[],
1724            )
1725            .await
1726            .unwrap()
1727            .get::<_, i64>(0);
1728            if rows == source_rows {
1729                Ok(())
1730            } else {
1731                Err(format!(
1732                    "Waiting for {source_rows} row to be ingested. Currently at {rows}."
1733                ))
1734            }
1735        })
1736        .await
1737        .unwrap();
1738    pg_batch_execute(
1739        mz_client,
1740        sql!(
1741            "SET transaction_isolation = {}",
1742            Sql::literal(&current_isolation),
1743        ),
1744    )
1745    .await
1746    .unwrap();
1747}
1748
1749// Initializes a websocket connection. Returns the init messages before the initial ReadyForQuery.
1750pub fn auth_with_ws(
1751    ws: &mut WebSocket<MaybeTlsStream<TcpStream>>,
1752    mut options: BTreeMap<String, String>,
1753) -> Result<Vec<WebSocketResponse>, anyhow::Error> {
1754    if !options.contains_key("welcome_message") {
1755        options.insert("welcome_message".into(), "off".into());
1756    }
1757    auth_with_ws_impl(
1758        ws,
1759        Message::Text(
1760            serde_json::to_string(&WebSocketAuth::Basic {
1761                user: "materialize".into(),
1762                password: "".into(),
1763                options,
1764            })
1765            .unwrap()
1766            .into(),
1767        ),
1768    )
1769}
1770
1771pub fn auth_with_ws_impl(
1772    ws: &mut WebSocket<MaybeTlsStream<TcpStream>>,
1773    auth_message: Message,
1774) -> Result<Vec<WebSocketResponse>, anyhow::Error> {
1775    ws.send(auth_message)?;
1776
1777    // Wait for initial ready response.
1778    let mut msgs = Vec::new();
1779    loop {
1780        let resp = ws.read()?;
1781        match resp {
1782            Message::Text(msg) => {
1783                let msg: WebSocketResponse = serde_json::from_str(&msg).unwrap();
1784                match msg {
1785                    WebSocketResponse::ReadyForQuery(_) => break,
1786                    msg => {
1787                        msgs.push(msg);
1788                    }
1789                }
1790            }
1791            Message::Ping(_) => continue,
1792            Message::Close(None) => return Err(anyhow!("ws closed after auth")),
1793            Message::Close(Some(close_frame)) => {
1794                return Err(anyhow!("ws closed after auth").context(close_frame));
1795            }
1796            _ => panic!("unexpected response: {:?}", resp),
1797        }
1798    }
1799    Ok(msgs)
1800}
1801
1802pub fn make_header<H: Header>(h: H) -> HeaderMap {
1803    let mut map = HeaderMap::new();
1804    map.typed_insert(h);
1805    map
1806}
1807
1808pub fn make_pg_tls<F>(configure: F) -> MakeTlsConnector
1809where
1810    F: FnOnce(&mut SslConnectorBuilder) -> Result<(), ErrorStack>,
1811{
1812    let mut connector_builder = SslConnector::builder(SslMethod::tls()).unwrap();
1813    // Disable TLS v1.3 because `postgres` and `hyper` produce stabler error
1814    // messages with TLS v1.2.
1815    //
1816    // Briefly, in TLS v1.3, failing to present a client certificate does not
1817    // error during the TLS handshake, as it does in TLS v1.2, but on the first
1818    // attempt to read from the stream. But both `postgres` and `hyper` write a
1819    // bunch of data before attempting to read from the stream. With a failed
1820    // TLS v1.3 connection, sometimes `postgres` and `hyper` succeed in writing
1821    // out this data, and then return a nice error message on the call to read.
1822    // But sometimes the connection is closed before they write out the data,
1823    // and so they report "connection closed" before they ever call read, never
1824    // noticing the underlying SSL error.
1825    //
1826    // It's unclear who's bug this is. Is it on `hyper`/`postgres` to call read
1827    // if writing to the stream fails to see if a TLS error occured? Is it on
1828    // OpenSSL to provide a better API [1]? Is it a protocol issue that ought to
1829    // be corrected in TLS v1.4? We don't want to answer these questions, so we
1830    // just avoid TLS v1.3 for now.
1831    //
1832    // [1]: https://github.com/openssl/openssl/issues/11118
1833    let options = connector_builder.options() | SslOptions::NO_TLSV1_3;
1834    connector_builder.set_options(options);
1835    configure(&mut connector_builder).unwrap();
1836    MakeTlsConnector::new(connector_builder.build())
1837}
1838
1839/// A certificate authority for use in tests.
1840pub struct Ca {
1841    pub dir: TempDir,
1842    pub name: X509Name,
1843    pub cert: X509,
1844    pub pkey: PKey<Private>,
1845}
1846
1847impl Ca {
1848    fn make_ca(name: &str, parent: Option<&Ca>) -> Result<Ca, Box<dyn Error>> {
1849        let dir = tempfile::tempdir()?;
1850        let rsa = Rsa::generate(2048)?;
1851        let pkey = PKey::from_rsa(rsa)?;
1852        let name = {
1853            let mut builder = X509NameBuilder::new()?;
1854            builder.append_entry_by_nid(Nid::COMMONNAME, name)?;
1855            builder.build()
1856        };
1857        let cert = {
1858            let mut builder = X509::builder()?;
1859            builder.set_version(2)?;
1860            builder.set_pubkey(&pkey)?;
1861            builder.set_issuer_name(parent.map(|ca| &ca.name).unwrap_or(&name))?;
1862            builder.set_subject_name(&name)?;
1863            builder.set_not_before(&*Asn1Time::days_from_now(0)?)?;
1864            builder.set_not_after(&*Asn1Time::days_from_now(365)?)?;
1865            builder.append_extension(BasicConstraints::new().critical().ca().build()?)?;
1866            builder.sign(
1867                parent.map(|ca| &ca.pkey).unwrap_or(&pkey),
1868                MessageDigest::sha256(),
1869            )?;
1870            builder.build()
1871        };
1872        fs::write(dir.path().join("ca.crt"), cert.to_pem()?)?;
1873        Ok(Ca {
1874            dir,
1875            name,
1876            cert,
1877            pkey,
1878        })
1879    }
1880
1881    /// Creates a new root certificate authority.
1882    pub fn new_root(name: &str) -> Result<Ca, Box<dyn Error>> {
1883        Ca::make_ca(name, None)
1884    }
1885
1886    /// Returns the path to the CA's certificate.
1887    pub fn ca_cert_path(&self) -> PathBuf {
1888        self.dir.path().join("ca.crt")
1889    }
1890
1891    /// Requests a new intermediate certificate authority.
1892    pub fn request_ca(&self, name: &str) -> Result<Ca, Box<dyn Error>> {
1893        Ca::make_ca(name, Some(self))
1894    }
1895
1896    /// Generates a certificate with the specified Common Name (CN) that is
1897    /// signed by the CA.
1898    ///
1899    /// Returns the paths to the certificate and key.
1900    pub fn request_client_cert(&self, name: &str) -> Result<(PathBuf, PathBuf), Box<dyn Error>> {
1901        self.request_cert(name, iter::empty())
1902    }
1903
1904    /// Like `request_client_cert`, but permits specifying additional IP
1905    /// addresses to attach as Subject Alternate Names.
1906    pub fn request_cert<I>(&self, name: &str, ips: I) -> Result<(PathBuf, PathBuf), Box<dyn Error>>
1907    where
1908        I: IntoIterator<Item = IpAddr>,
1909    {
1910        let rsa = Rsa::generate(2048)?;
1911        let pkey = PKey::from_rsa(rsa)?;
1912        let subject_name = {
1913            let mut builder = X509NameBuilder::new()?;
1914            builder.append_entry_by_nid(Nid::COMMONNAME, name)?;
1915            builder.build()
1916        };
1917        let cert = {
1918            let mut builder = X509::builder()?;
1919            builder.set_version(2)?;
1920            builder.set_pubkey(&pkey)?;
1921            builder.set_issuer_name(self.cert.subject_name())?;
1922            builder.set_subject_name(&subject_name)?;
1923            builder.set_not_before(&*Asn1Time::days_from_now(0)?)?;
1924            builder.set_not_after(&*Asn1Time::days_from_now(365)?)?;
1925            for ip in ips {
1926                builder.append_extension(
1927                    SubjectAlternativeName::new()
1928                        .ip(&ip.to_string())
1929                        .build(&builder.x509v3_context(None, None))?,
1930                )?;
1931            }
1932            builder.sign(&self.pkey, MessageDigest::sha256())?;
1933            builder.build()
1934        };
1935        let cert_path = self.dir.path().join(Path::new(name).with_extension("crt"));
1936        let key_path = self.dir.path().join(Path::new(name).with_extension("key"));
1937        fs::write(&cert_path, cert.to_pem()?)?;
1938        fs::write(&key_path, pkey.private_key_to_pem_pkcs8()?)?;
1939        Ok((cert_path, key_path))
1940    }
1941}
1942
1943/// Sums the counter series named `name` whose labels include all of `labels`.
1944///
1945/// Returns 0 when no series matches, which is how a labelled counter reads
1946/// before its first increment.
1947pub fn get_counter_value(registry: &MetricsRegistry, name: &str, labels: &[(&str, &str)]) -> u64 {
1948    let Some(family) = registry.gather().into_iter().find(|m| m.name() == name) else {
1949        return 0;
1950    };
1951    family
1952        .get_metric()
1953        .iter()
1954        .filter(|metric| {
1955            labels.iter().all(|(name, value)| {
1956                metric
1957                    .get_label()
1958                    .iter()
1959                    .any(|label| label.name() == *name && label.value() == *value)
1960            })
1961        })
1962        .map(|metric| u64::cast_lossy(metric.get_counter().value()))
1963        .sum()
1964}