aster_forge_test/
redis.rs1use crate::state::{ContainerLease, ContainerStateLock};
7use crate::suite::TestContainerSuite;
8use crate::wait::wait_until;
9use aster_forge_utils::url::url_with_credentials;
10use std::net::TcpListener;
11use std::time::Duration;
12use testcontainers::core::{ContainerAsync, IntoContainerPort};
13use testcontainers::{GenericImage, ImageExt, ReuseDirective, runners::AsyncRunner};
14
15pub struct RedisTestContainer {
17 url: String,
18 container: ContainerAsync<GenericImage>,
19 _lease: ContainerLease,
20}
21
22pub struct AuthenticatedRedisTestContainer {
27 base_url: String,
28 _container: ContainerAsync<GenericImage>,
29}
30
31impl AuthenticatedRedisTestContainer {
32 pub async fn start(password: &str) -> Self {
39 let container = GenericImage::new("redis", "7-alpine")
40 .with_exposed_port(IntoContainerPort::tcp(6379))
41 .with_cmd(["redis-server", "--requirepass", password])
42 .start()
43 .await
44 .expect("failed to start authenticated Redis test container");
45 let port = container
46 .get_host_port_ipv4(IntoContainerPort::tcp(6379))
47 .await
48 .expect("authenticated Redis test port should be exposed");
49 let base_url = format!("redis://127.0.0.1:{port}/0");
50 let credential_url = url_with_credentials(
51 &base_url,
52 None,
53 Some(password),
54 "authenticated Redis test container base URL",
55 )
56 .expect("authenticated Redis test URL should accept credentials");
57 let client = redis::Client::open(credential_url)
58 .unwrap_or_else(|_| panic!("failed to build authenticated Redis readiness client"));
59 wait_for_redis(&client, "authenticated Redis test container").await;
60
61 Self {
62 base_url,
63 _container: container,
64 }
65 }
66
67 #[must_use]
69 pub fn base_url(&self) -> &str {
70 &self.base_url
71 }
72}
73
74impl RedisTestContainer {
75 pub async fn start(suite: &TestContainerSuite) -> Self {
82 let lock = ContainerStateLock::acquire(suite, "redis-fixed");
86 let mut state = lock.load();
87 let _ = state.prune_stale_before_current_execution();
88 state.register_current_process();
89 lock.save(&state);
90 let host_port = TcpListener::bind(("127.0.0.1", 0))
91 .expect("reserve Redis test host port")
92 .local_addr()
93 .expect("resolve Redis test host port")
94 .port();
95
96 let container = GenericImage::new("redis", "7-alpine")
97 .with_mapped_port(host_port, IntoContainerPort::tcp(6379))
98 .with_container_name(suite.container_name("redis-fixed"))
99 .with_reuse(ReuseDirective::Always)
100 .start()
101 .await
102 .expect("failed to start Redis test container");
103 let port = container
104 .get_host_port_ipv4(IntoContainerPort::tcp(6379))
105 .await
106 .expect("Redis test port should be exposed");
107
108 let url = format!("redis://127.0.0.1:{port}/0");
109 let client = redis::Client::open(url.as_str())
110 .unwrap_or_else(|_| panic!("failed to build Redis readiness client"));
111 wait_for_redis(&client, "Redis test container").await;
112 drop(lock);
113
114 Self {
115 url,
116 container,
117 _lease: ContainerLease::new(suite.clone(), "redis-fixed"),
118 }
119 }
120
121 #[must_use]
123 pub fn url(&self) -> &str {
124 &self.url
125 }
126
127 pub async fn stop(&self) {
133 self.container
134 .stop_with_timeout(Some(0))
135 .await
136 .expect("failed to stop Redis test container");
137 }
138
139 pub async fn restart(&self) {
145 self.container
146 .start()
147 .await
148 .expect("failed to restart Redis test container");
149 let client = redis::Client::open(self.url.as_str())
150 .unwrap_or_else(|_| panic!("failed to build restarted Redis readiness client"));
151 wait_for_redis(&client, "restarted Redis test container").await;
152 }
153}
154
155async fn wait_for_redis(client: &redis::Client, context: &str) {
156 let ready = wait_until(
157 Duration::from_secs(90),
158 Duration::from_millis(250),
159 || async {
160 let Ok(mut connection) = client.get_multiplexed_async_connection().await else {
161 return false;
162 };
163 redis::cmd("PING")
164 .query_async::<String>(&mut connection)
165 .await
166 .is_ok_and(|response| response == "PONG")
167 },
168 )
169 .await;
170 assert!(ready, "{context} did not answer PING before timeout");
171}