pg_embedded_setup_unpriv/cluster/
connection.rs

1//! Connection helpers for `TestCluster`, including metadata accessors and optional Diesel support.
2use camino::{Utf8Path, Utf8PathBuf};
3#[cfg(feature = "diesel-support")]
4use color_eyre::eyre::WrapErr;
5use postgresql_embedded::Settings;
6
7use crate::TestBootstrapSettings;
8#[cfg(feature = "diesel-support")]
9use crate::error::BootstrapResult;
10
11/// Provides ergonomic accessors for connection-oriented cluster metadata.
12///
13/// # Examples
14/// ```no_run
15/// use pg_embedded_setup_unpriv::TestCluster;
16///
17/// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
18/// let cluster = TestCluster::new()?;
19/// let metadata = cluster.connection().metadata();
20/// assert_eq!(metadata.host(), "localhost");
21/// # Ok(())
22/// # }
23/// ```
24#[derive(Debug, Clone)]
25pub struct ConnectionMetadata {
26    settings: Settings,
27    pgpass_file: Utf8PathBuf,
28}
29
30impl ConnectionMetadata {
31    pub(crate) fn from_settings(settings: &TestBootstrapSettings) -> Self {
32        Self {
33            settings: settings.settings.clone(),
34            pgpass_file: settings.environment.pgpass_file.clone(),
35        }
36    }
37
38    /// Returns the configured database host.
39    #[must_use]
40    pub fn host(&self) -> &str {
41        self.settings.host.as_str()
42    }
43
44    /// Returns the configured port.
45    #[must_use]
46    pub const fn port(&self) -> u16 {
47        self.settings.port
48    }
49
50    /// Returns the configured superuser name.
51    #[must_use]
52    pub fn superuser(&self) -> &str {
53        self.settings.username.as_str()
54    }
55
56    /// Returns the generated superuser password.
57    #[must_use]
58    pub fn password(&self) -> &str {
59        self.settings.password.as_str()
60    }
61
62    /// Returns the prepared `.pgpass` file path.
63    #[must_use]
64    pub fn pgpass_file(&self) -> &Utf8Path {
65        self.pgpass_file.as_ref()
66    }
67
68    /// Constructs a libpq-compatible URL for `database` using the underlying
69    /// `postgresql_embedded` helper.
70    #[must_use]
71    pub fn database_url(&self, database: &str) -> String {
72        self.settings.url(database)
73    }
74}
75
76/// Accessor for connection helpers derived from a
77/// [`TestCluster`](crate::TestCluster).
78///
79/// Enable the `diesel-support` feature to call the Diesel connection helper.
80///
81/// # Examples
82/// ```no_run
83/// use pg_embedded_setup_unpriv::TestCluster;
84///
85/// # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
86/// let cluster = TestCluster::new()?;
87/// let url = cluster.connection().database_url("postgres");
88/// assert!(url.contains("postgresql://"));
89/// # Ok(())
90/// # }
91/// ```
92#[derive(Debug, Clone)]
93pub struct TestClusterConnection {
94    metadata: ConnectionMetadata,
95}
96
97impl TestClusterConnection {
98    pub(crate) fn new(settings: &TestBootstrapSettings) -> Self {
99        Self {
100            metadata: ConnectionMetadata::from_settings(settings),
101        }
102    }
103
104    /// Returns host metadata without exposing internal storage.
105    #[must_use]
106    pub fn host(&self) -> &str {
107        self.metadata.host()
108    }
109
110    /// Returns the configured port.
111    #[must_use]
112    pub const fn port(&self) -> u16 {
113        self.metadata.port()
114    }
115
116    /// Returns the configured superuser account name.
117    #[must_use]
118    pub fn superuser(&self) -> &str {
119        self.metadata.superuser()
120    }
121
122    /// Returns the generated password for the superuser.
123    #[must_use]
124    pub fn password(&self) -> &str {
125        self.metadata.password()
126    }
127
128    /// Returns the `.pgpass` file prepared during bootstrap.
129    #[must_use]
130    pub fn pgpass_file(&self) -> &Utf8Path {
131        self.metadata.pgpass_file()
132    }
133
134    /// Provides an owned snapshot of the connection metadata.
135    #[must_use]
136    pub fn metadata(&self) -> ConnectionMetadata {
137        self.metadata.clone()
138    }
139
140    /// Builds a libpq-compatible database URL for `database`.
141    #[must_use]
142    pub fn database_url(&self, database: &str) -> String {
143        self.metadata.database_url(database)
144    }
145
146    /// Establishes a Diesel connection for the target `database`.
147    ///
148    /// # Errors
149    /// Returns a [`crate::error::BootstrapError`] when Diesel cannot connect.
150    #[cfg(feature = "diesel-support")]
151    pub fn diesel_connection(&self, database: &str) -> BootstrapResult<diesel::PgConnection> {
152        use diesel::Connection;
153
154        diesel::PgConnection::establish(&self.database_url(database))
155            .wrap_err(format!("failed to connect to {database} via Diesel"))
156            .map_err(crate::error::BootstrapError::from)
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::TestBootstrapSettings;
164    use crate::bootstrap::{ExecutionMode, ExecutionPrivileges, TestBootstrapEnvironment};
165    use postgresql_embedded::Settings;
166    use std::time::Duration;
167
168    fn sample_settings() -> TestBootstrapSettings {
169        let settings = Settings {
170            host: "127.0.0.1".into(),
171            port: 55_321,
172            username: "fixture_user".into(),
173            password: "fixture_pass".into(),
174            data_dir: "/tmp/cluster-data".into(),
175            installation_dir: "/tmp/cluster-install".into(),
176            ..Settings::default()
177        };
178
179        TestBootstrapSettings {
180            privileges: ExecutionPrivileges::Unprivileged,
181            execution_mode: ExecutionMode::InProcess,
182            settings,
183            environment: TestBootstrapEnvironment {
184                home: Utf8PathBuf::from("/tmp/home"),
185                xdg_cache_home: Utf8PathBuf::from("/tmp/home/cache"),
186                xdg_runtime_dir: Utf8PathBuf::from("/tmp/home/run"),
187                pgpass_file: Utf8PathBuf::from("/tmp/home/.pgpass"),
188                tz_dir: Some(Utf8PathBuf::from("/usr/share/zoneinfo")),
189                timezone: "UTC".into(),
190            },
191            worker_binary: None,
192            setup_timeout: Duration::from_secs(1),
193            start_timeout: Duration::from_secs(1),
194            shutdown_timeout: Duration::from_secs(1),
195        }
196    }
197
198    #[test]
199    fn metadata_reflects_underlying_settings() {
200        let settings = sample_settings();
201        let connection = TestClusterConnection::new(&settings);
202        let metadata = connection.metadata();
203
204        assert_eq!(metadata.host(), "127.0.0.1");
205        assert_eq!(metadata.port(), 55_321);
206        assert_eq!(metadata.superuser(), "fixture_user");
207        assert_eq!(metadata.password(), "fixture_pass");
208        assert_eq!(metadata.pgpass_file(), Utf8Path::new("/tmp/home/.pgpass"));
209    }
210
211    #[test]
212    fn database_url_matches_postgresql_embedded() {
213        let settings = sample_settings();
214        let connection = TestClusterConnection::new(&settings);
215        let expected = settings.settings.url("postgres");
216
217        assert_eq!(connection.database_url("postgres"), expected);
218    }
219}