use camino::{Utf8Path, Utf8PathBuf};
#[cfg(feature = "diesel-support")]
use color_eyre::eyre::WrapErr;
use postgres::{Client, NoTls};
use postgresql_embedded::Settings;
use crate::{TestBootstrapSettings, error::BootstrapResult};
pub(crate) fn escape_identifier(name: &str) -> String { name.replace('"', "\"\"") }
pub(crate) fn connect_admin(url: &str) -> BootstrapResult<Client> {
Client::connect(url, NoTls).map_err(admin_connect_error)
}
pub(crate) fn admin_connect_error(err: postgres::Error) -> crate::error::BootstrapError {
let reason = std::error::Error::source(&err)
.map_or_else(|| err.to_string(), |source| format!("{err}: {source}"));
let message = format!("failed to connect to admin database: {reason}");
crate::error::BootstrapError::from(color_eyre::Report::new(err).wrap_err(message))
}
#[derive(Debug, Clone)]
pub struct ConnectionMetadata {
settings: Settings,
pgpass_file: Utf8PathBuf,
}
impl ConnectionMetadata {
pub(crate) fn from_settings(settings: &TestBootstrapSettings) -> Self {
Self {
settings: settings.settings.clone(),
pgpass_file: settings.environment.pgpass_file.clone(),
}
}
#[must_use]
pub const fn host(&self) -> &str { self.settings.host.as_str() }
#[must_use]
pub const fn port(&self) -> u16 { self.settings.port }
#[must_use]
pub const fn superuser(&self) -> &str { self.settings.username.as_str() }
#[must_use]
pub const fn password(&self) -> &str { self.settings.password.as_str() }
#[must_use]
pub fn pgpass_file(&self) -> &Utf8Path { self.pgpass_file.as_ref() }
#[must_use]
pub fn database_url(&self, database: &str) -> String { self.settings.url(database) }
}
#[derive(Debug, Clone)]
pub struct TestClusterConnection {
metadata: ConnectionMetadata,
}
impl TestClusterConnection {
pub(crate) fn new(settings: &TestBootstrapSettings) -> Self {
Self {
metadata: ConnectionMetadata::from_settings(settings),
}
}
#[must_use]
pub const fn host(&self) -> &str { self.metadata.host() }
#[must_use]
pub const fn port(&self) -> u16 { self.metadata.port() }
#[must_use]
pub const fn superuser(&self) -> &str { self.metadata.superuser() }
#[must_use]
pub const fn password(&self) -> &str { self.metadata.password() }
#[must_use]
pub fn pgpass_file(&self) -> &Utf8Path { self.metadata.pgpass_file() }
#[must_use]
pub fn metadata(&self) -> ConnectionMetadata { self.metadata.clone() }
#[must_use]
pub fn database_url(&self, database: &str) -> String { self.metadata.database_url(database) }
#[cfg(feature = "diesel-support")]
pub fn diesel_connection(&self, database: &str) -> BootstrapResult<diesel::PgConnection> {
use diesel::Connection;
diesel::PgConnection::establish(&self.database_url(database))
.wrap_err(format!("failed to connect to {database} via Diesel"))
.map_err(crate::error::BootstrapError::from)
}
pub(super) fn admin_client(&self) -> BootstrapResult<Client> {
connect_admin(&self.database_url("postgres"))
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use postgresql_embedded::Settings;
use super::*;
use crate::{
CleanupMode,
TestBootstrapSettings,
bootstrap::{ExecutionMode, ExecutionPrivileges, TestBootstrapEnvironment},
};
fn sample_settings() -> TestBootstrapSettings {
let settings = Settings {
host: "127.0.0.1".into(),
port: 55_321,
username: "fixture_user".into(),
password: "fixture_pass".into(),
data_dir: "/tmp/cluster-data".into(),
installation_dir: "/tmp/cluster-install".into(),
..Settings::default()
};
TestBootstrapSettings {
privileges: ExecutionPrivileges::Unprivileged,
execution_mode: ExecutionMode::InProcess,
settings,
environment: TestBootstrapEnvironment {
home: Utf8PathBuf::from("/tmp/home"),
xdg_cache_home: Utf8PathBuf::from("/tmp/home/cache"),
xdg_runtime_dir: Utf8PathBuf::from("/tmp/home/run"),
pgpass_file: Utf8PathBuf::from("/tmp/home/.pgpass"),
tz_dir: Some(Utf8PathBuf::from("/usr/share/zoneinfo")),
timezone: "UTC".into(),
},
worker_binary: None,
setup_timeout: Duration::from_secs(1),
start_timeout: Duration::from_secs(1),
shutdown_timeout: Duration::from_secs(1),
cleanup_mode: CleanupMode::default(),
binary_cache_dir: None,
extensions: None,
installed_extensions: Vec::new(),
}
}
#[test]
fn metadata_reflects_underlying_settings() {
let settings = sample_settings();
let connection = TestClusterConnection::new(&settings);
let metadata = connection.metadata();
assert_eq!(metadata.host(), "127.0.0.1");
assert_eq!(metadata.port(), 55_321);
assert_eq!(metadata.superuser(), "fixture_user");
assert_eq!(metadata.password(), "fixture_pass");
assert_eq!(metadata.pgpass_file(), Utf8Path::new("/tmp/home/.pgpass"));
}
#[test]
fn database_url_matches_postgresql_embedded() {
let settings = sample_settings();
let connection = TestClusterConnection::new(&settings);
let expected = settings.settings.url("postgres");
assert_eq!(connection.database_url("postgres"), expected);
}
fn reject_password_once() -> std::io::Result<std::net::SocketAddr> {
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
let address = listener.local_addr()?;
std::thread::spawn(move || {
let _served = answer_with_28p01(&listener);
});
Ok(address)
}
#[expect(
clippy::big_endian_bytes,
reason = "the PostgreSQL wire protocol frames every length in network byte order"
)]
fn answer_with_28p01(listener: &std::net::TcpListener) -> std::io::Result<()> {
use std::io::{Error, Read, Write};
let (mut stream, _) = listener.accept()?;
let mut length = [0_u8; 4];
stream.read_exact(&mut length)?;
let body_len = usize::try_from(u32::from_be_bytes(length))
.map_err(Error::other)?
.saturating_sub(4);
let mut body = vec![0_u8; body_len];
stream.read_exact(&mut body)?;
let mut fields = Vec::new();
for (code, value) in [
(b'S', "FATAL"),
(b'V', "FATAL"),
(b'C', "28P01"),
(b'M', "password authentication failed for user \"postgres\""),
] {
fields.push(code);
fields.extend_from_slice(value.as_bytes());
fields.push(0);
}
fields.push(0);
let reply_len = u32::try_from(fields.len() + 4).map_err(Error::other)?;
let mut reply = vec![b'E'];
reply.extend_from_slice(&reply_len.to_be_bytes());
reply.extend_from_slice(&fields);
stream.write_all(&reply)
}
#[test]
fn a_rejected_password_names_its_cause() {
let address = reject_password_once().expect("a loopback port");
let url = format!("postgresql://postgres:wrong@{address}/postgres");
let err = connect_admin(&url)
.err()
.expect("the server rejects the password");
let message = err.to_string();
assert!(
message.contains("db error: FATAL: password authentication failed"),
"the nested cause must reach the message: {message}"
);
let report = err.into_report();
let driver = report
.chain()
.find_map(|cause| cause.downcast_ref::<postgres::Error>())
.expect("the postgres error stays in the chain");
let code = driver.code().map(postgres::error::SqlState::code);
assert_eq!(
code,
Some("28P01"),
"the server's code survives: {report:?}"
);
}
#[test]
fn a_failed_admin_connection_keeps_its_source() {
let err = connect_admin("postgresql://postgres:x@127.0.0.1:1/postgres")
.err()
.expect("nothing listens on port 1");
let message = err.to_string();
let report = err.into_report();
let chain_has_driver_error = report
.chain()
.any(|cause| cause.downcast_ref::<postgres::Error>().is_some());
assert!(chain_has_driver_error, "the source was dropped: {report:?}");
assert!(
message.starts_with("failed to connect to admin database: "),
"{message}"
);
assert!(
message.to_lowercase().contains("refused"),
"the cause must reach the message: {message}"
);
}
}