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