Skip to main content

aster_forge_test/
smtp.rs

1//! Shared Mailpit container for integration tests.
2
3use crate::state::{ContainerLease, ContainerStateLock};
4use crate::suite::TestContainerSuite;
5use crate::wait::wait_until;
6use std::net::{SocketAddr, TcpStream};
7use std::time::Duration;
8use testcontainers::core::{ContainerAsync, IntoContainerPort};
9use testcontainers::{GenericImage, ImageExt, ReuseDirective, runners::AsyncRunner};
10
11/// Reusable Mailpit SMTP/API container.
12pub struct SmtpTestContainer {
13    smtp_address: SocketAddr,
14    api_base_url: String,
15    client: reqwest::Client,
16    _container: ContainerAsync<GenericImage>,
17    _lease: ContainerLease,
18}
19
20impl SmtpTestContainer {
21    /// Starts or reuses a Mailpit container for the test suite.
22    pub async fn start(suite: &TestContainerSuite) -> Self {
23        let lock = ContainerStateLock::acquire(suite, "mailpit");
24        let mut state = lock.load();
25        let _ = state.prune_stale();
26        state.register_pid(std::process::id());
27        lock.save(&state);
28        let container = GenericImage::new("axllent/mailpit", "v1.21.8")
29            .with_exposed_port(IntoContainerPort::tcp(1025))
30            .with_exposed_port(IntoContainerPort::tcp(8025))
31            .with_container_name(suite.container_name("mailpit"))
32            .with_reuse(ReuseDirective::Always)
33            .start()
34            .await
35            .expect("failed to start Mailpit test container");
36        let smtp_port = container
37            .get_host_port_ipv4(IntoContainerPort::tcp(1025))
38            .await
39            .expect("Mailpit SMTP port should be exposed");
40        let api_port = container
41            .get_host_port_ipv4(IntoContainerPort::tcp(8025))
42            .await
43            .expect("Mailpit API port should be exposed");
44        drop(lock);
45        let smtp_address = SocketAddr::from(([127, 0, 0, 1], smtp_port));
46        let smtp_ready = wait_until(
47            Duration::from_secs(90),
48            Duration::from_millis(250),
49            || async {
50                TcpStream::connect_timeout(&smtp_address, Duration::from_millis(500)).is_ok()
51            },
52        )
53        .await;
54        assert!(smtp_ready, "Mailpit SMTP endpoint did not become ready");
55
56        let api_base_url = format!("http://127.0.0.1:{api_port}");
57        let client = reqwest::Client::new();
58        let api_ready = wait_until(
59            Duration::from_secs(90),
60            Duration::from_millis(250),
61            || async {
62                client
63                    .get(format!("{api_base_url}/api/v1/messages"))
64                    .send()
65                    .await
66                    .is_ok_and(|response| response.status().is_success())
67            },
68        )
69        .await;
70        assert!(api_ready, "Mailpit API endpoint did not become ready");
71
72        Self {
73            smtp_address,
74            api_base_url,
75            client,
76            _container: container,
77            _lease: ContainerLease::new(suite.clone(), "mailpit"),
78        }
79    }
80
81    /// Returns the SMTP endpoint host and port.
82    pub fn smtp_address(&self) -> SocketAddr {
83        self.smtp_address
84    }
85
86    /// Deletes all messages currently stored by Mailpit.
87    pub async fn clear_messages(&self) {
88        let response = self
89            .client
90            .delete(format!("{}/api/v1/messages", self.api_base_url))
91            .send()
92            .await
93            .expect("failed to clear Mailpit messages");
94        assert!(
95            response.status().is_success(),
96            "Mailpit message cleanup failed with {}",
97            response.status()
98        );
99    }
100
101    /// Returns the number of messages currently stored by Mailpit.
102    pub async fn message_count(&self) -> u64 {
103        let response = self
104            .client
105            .get(format!("{}/api/v1/messages", self.api_base_url))
106            .send()
107            .await
108            .expect("failed to query Mailpit messages");
109        let status = response.status();
110        let body: serde_json::Value = response
111            .json()
112            .await
113            .expect("failed to decode Mailpit messages response");
114        assert!(status.is_success(), "Mailpit API failed: {body}");
115        body["total"]
116            .as_u64()
117            .expect("Mailpit response should include total")
118    }
119}