testcontainers_modules/postgres/
mod.rs

1use std::{borrow::Cow, collections::HashMap};
2
3use testcontainers::{core::WaitFor, CopyDataSource, CopyToContainer, Image};
4
5const NAME: &str = "postgres";
6const TAG: &str = "11-alpine";
7
8/// Module to work with [`Postgres`] inside of tests.
9///
10/// Starts an instance of Postgres.
11/// This module is based on the official [`Postgres docker image`].
12///
13/// Default db name, user and password is `postgres`.
14///
15/// # Example
16/// ```
17/// use testcontainers_modules::{postgres, testcontainers::runners::SyncRunner};
18///
19/// let postgres_instance = postgres::Postgres::default().start().unwrap();
20///
21/// let connection_string = format!(
22///     "postgres://postgres:postgres@{}:{}/postgres",
23///     postgres_instance.get_host().unwrap(),
24///     postgres_instance.get_host_port_ipv4(5432).unwrap()
25/// );
26/// ```
27///
28/// [`Postgres`]: https://www.postgresql.org/
29/// [`Postgres docker image`]: https://hub.docker.com/_/postgres
30#[derive(Debug, Clone)]
31pub struct Postgres {
32    env_vars: HashMap<String, String>,
33    copy_to_sources: Vec<CopyToContainer>,
34    fsync_enabled: bool,
35}
36
37impl Postgres {
38    /// Enables the Postgres instance to be used without authentication on host.
39    /// For more information see the description of `POSTGRES_HOST_AUTH_METHOD` in official [docker image](https://hub.docker.com/_/postgres)
40    pub fn with_host_auth(mut self) -> Self {
41        self.env_vars
42            .insert("POSTGRES_HOST_AUTH_METHOD".to_owned(), "trust".to_owned());
43        self
44    }
45
46    /// Sets the db name for the Postgres instance.
47    pub fn with_db_name(mut self, db_name: &str) -> Self {
48        self.env_vars
49            .insert("POSTGRES_DB".to_owned(), db_name.to_owned());
50        self
51    }
52
53    /// Sets the user for the Postgres instance.
54    pub fn with_user(mut self, user: &str) -> Self {
55        self.env_vars
56            .insert("POSTGRES_USER".to_owned(), user.to_owned());
57        self
58    }
59
60    /// Sets the password for the Postgres instance.
61    pub fn with_password(mut self, password: &str) -> Self {
62        self.env_vars
63            .insert("POSTGRES_PASSWORD".to_owned(), password.to_owned());
64        self
65    }
66
67    /// Registers sql to be executed automatically when the container starts.
68    /// Can be called multiple times to add (not override) scripts.
69    ///
70    /// # Example
71    ///
72    /// ```
73    /// # use testcontainers_modules::postgres::Postgres;
74    /// let postgres_image = Postgres::default().with_init_sql(
75    ///     "CREATE EXTENSION IF NOT EXISTS hstore;"
76    ///         .to_string()
77    ///         .into_bytes(),
78    /// );
79    /// ```
80    ///
81    /// ```rust,ignore
82    /// # use testcontainers_modules::postgres::Postgres;
83    /// let postgres_image = Postgres::default()
84    ///                                .with_init_sql(include_str!("path_to_init.sql").to_string().into_bytes());
85    /// ```
86    pub fn with_init_sql(mut self, init_sql: impl Into<CopyDataSource>) -> Self {
87        let target = format!(
88            "/docker-entrypoint-initdb.d/init_{i}.sql",
89            i = self.copy_to_sources.len()
90        );
91        self.copy_to_sources
92            .push(CopyToContainer::new(init_sql.into(), target));
93        self
94    }
95
96    /// Enables [the fsync-setting](https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-FSYNC) for the Postgres instance.
97    pub fn with_fsync_enabled(mut self) -> Self {
98        self.fsync_enabled = true;
99        self
100    }
101}
102impl Default for Postgres {
103    fn default() -> Self {
104        let mut env_vars = HashMap::new();
105        env_vars.insert("POSTGRES_DB".to_owned(), "postgres".to_owned());
106        env_vars.insert("POSTGRES_USER".to_owned(), "postgres".to_owned());
107        env_vars.insert("POSTGRES_PASSWORD".to_owned(), "postgres".to_owned());
108
109        Self {
110            env_vars,
111            copy_to_sources: Vec::new(),
112            fsync_enabled: false,
113        }
114    }
115}
116
117impl Image for Postgres {
118    fn name(&self) -> &str {
119        NAME
120    }
121
122    fn tag(&self) -> &str {
123        TAG
124    }
125
126    fn ready_conditions(&self) -> Vec<WaitFor> {
127        vec![
128            WaitFor::message_on_stderr("database system is ready to accept connections"),
129            WaitFor::message_on_stdout("database system is ready to accept connections"),
130        ]
131    }
132
133    fn env_vars(
134        &self,
135    ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
136        &self.env_vars
137    }
138
139    fn copy_to_sources(&self) -> impl IntoIterator<Item = &CopyToContainer> {
140        &self.copy_to_sources
141    }
142
143    fn cmd(&self) -> impl IntoIterator<Item = impl Into<std::borrow::Cow<'_, str>>> {
144        if !self.fsync_enabled {
145            vec!["-c", "fsync=off"]
146        } else {
147            vec![]
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use testcontainers::{runners::SyncRunner, ImageExt};
155
156    use super::*;
157
158    #[test]
159    fn postgres_one_plus_one() -> Result<(), Box<dyn std::error::Error + 'static>> {
160        let _ = pretty_env_logger::try_init();
161        let postgres_image = Postgres::default().with_host_auth();
162        let node = postgres_image.start()?;
163
164        let connection_string = &format!(
165            "postgres://postgres@{}:{}/postgres",
166            node.get_host()?,
167            node.get_host_port_ipv4(5432)?
168        );
169        let mut conn = postgres::Client::connect(connection_string, postgres::NoTls).unwrap();
170
171        let rows = conn.query("SELECT 1 + 1", &[]).unwrap();
172        assert_eq!(rows.len(), 1);
173
174        let first_row = &rows[0];
175        let first_column: i32 = first_row.get(0);
176        assert_eq!(first_column, 2);
177        Ok(())
178    }
179
180    #[test]
181    fn postgres_custom_version() -> Result<(), Box<dyn std::error::Error + 'static>> {
182        let node = Postgres::default().with_tag("13-alpine").start()?;
183
184        let connection_string = &format!(
185            "postgres://postgres:postgres@{}:{}/postgres",
186            node.get_host()?,
187            node.get_host_port_ipv4(5432)?
188        );
189        let mut conn = postgres::Client::connect(connection_string, postgres::NoTls).unwrap();
190
191        let rows = conn.query("SELECT version()", &[]).unwrap();
192        assert_eq!(rows.len(), 1);
193
194        let first_row = &rows[0];
195        let first_column: String = first_row.get(0);
196        assert!(first_column.contains("13"));
197        Ok(())
198    }
199
200    #[test]
201    fn postgres_with_init_sql() -> Result<(), Box<dyn std::error::Error + 'static>> {
202        let node = Postgres::default()
203            .with_init_sql(
204                "CREATE TABLE foo (bar varchar(255));"
205                    .to_string()
206                    .into_bytes(),
207            )
208            .start()?;
209
210        let connection_string = &format!(
211            "postgres://postgres:postgres@{}:{}/postgres",
212            node.get_host()?,
213            node.get_host_port_ipv4(5432)?
214        );
215        let mut conn = postgres::Client::connect(connection_string, postgres::NoTls).unwrap();
216
217        let rows = conn
218            .query("INSERT INTO foo(bar) VALUES ($1)", &[&"blub"])
219            .unwrap();
220        assert_eq!(rows.len(), 0);
221
222        let rows = conn.query("SELECT bar FROM foo", &[]).unwrap();
223        assert_eq!(rows.len(), 1);
224        Ok(())
225    }
226}