1use crate::{
2 client::get_mongodb_secret::get_mongodb_secret,
3 docker::{DockerInspectContainer, RunCommandInContainer, RunCommandInContainerError},
4 models::MongoDBPortBinding,
5};
6use bollard::secret::PortBinding;
7
8use super::GetDeploymentError;
9
10#[derive(Debug, thiserror::Error)]
11pub enum GetConnectionStringError {
12 #[error("Failed to get deployment: {0}")]
13 GetDeployment(#[from] GetDeploymentError),
14 #[error("Failed to get MongoDB username: {0}")]
15 GetMongodbUsername(RunCommandInContainerError),
16 #[error("Failed to get MongoDB password: {0}")]
17 GetMongodbPassword(RunCommandInContainerError),
18 #[error("Missing port binding information")]
19 MissingPortBinding,
20}
21
22impl<D: DockerInspectContainer + RunCommandInContainer> crate::client::Client<D> {
23 pub async fn get_connection_string(
25 &self,
26 container_id_or_name: String,
27 ) -> Result<String, GetConnectionStringError> {
28 let deployment = self.get_deployment(&container_id_or_name).await?;
30
31 let port = match &deployment.port_bindings {
33 Some(MongoDBPortBinding { port, .. }) => Some(*port),
34 _ => None,
35 };
36 let port = port
37 .flatten()
38 .ok_or(GetConnectionStringError::MissingPortBinding)?;
39
40 let hostname = PortBinding::from(
41 deployment
42 .port_bindings
43 .as_ref()
44 .ok_or(GetConnectionStringError::MissingPortBinding)?,
45 )
46 .host_ip
47 .ok_or(GetConnectionStringError::MissingPortBinding)?;
48
49 let mongodb_root_username = get_mongodb_secret(
51 self.docker.as_ref(),
52 &deployment,
53 |d| d.mongodb_initdb_root_username.as_deref(),
54 |d| d.mongodb_initdb_root_username_file.as_deref(),
55 )
56 .await
57 .map_err(GetConnectionStringError::GetMongodbUsername)?;
58
59 let mongodb_root_password = get_mongodb_secret(
61 self.docker.as_ref(),
62 &deployment,
63 |d| d.mongodb_initdb_root_password.as_deref(),
64 |d| d.mongodb_initdb_root_password_file.as_deref(),
65 )
66 .await
67 .map_err(GetConnectionStringError::GetMongodbPassword)?;
68
69 let connection_string =
71 format_connection_string(hostname, mongodb_root_username, mongodb_root_password, port);
72
73 Ok(connection_string)
74 }
75}
76
77fn format_connection_string(
79 hostname: String,
80 username: Option<String>,
81 password: Option<String>,
82 port: u16,
83) -> String {
84 let auth_string = match (username, password) {
85 (Some(u), Some(p)) if !u.is_empty() && !p.is_empty() => {
86 format!("{u}:{p}@")
87 }
88 _ => "".to_string(),
89 };
90
91 format!("mongodb://{auth_string}{hostname}:{port}/?directConnection=true",)
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::{
98 client::Client,
99 docker::{
100 CommandOutput, DockerInspectContainer, RunCommandInContainer,
101 RunCommandInContainerError,
102 },
103 test_utils::{
104 create_container_inspect_response_no_auth, create_container_inspect_response_with_auth,
105 },
106 };
107 use bollard::{
108 errors::Error as BollardError,
109 query_parameters::InspectContainerOptions,
110 secret::{
111 ContainerConfig, ContainerInspectResponse, ContainerState, ContainerStateStatusEnum,
112 },
113 };
114 use maplit::hashmap;
115 use mockall::mock;
116
117 mock! {
118 Docker {}
119
120 impl DockerInspectContainer for Docker {
121 async fn inspect_container(
122 &self,
123 container_id: &str,
124 options: Option<InspectContainerOptions>,
125 ) -> Result<ContainerInspectResponse, BollardError>;
126 }
127
128 impl RunCommandInContainer for Docker {
129 async fn run_command_in_container(
130 &self,
131 container_id: &str,
132 command: Vec<String>,
133 ) -> Result<CommandOutput, RunCommandInContainerError>;
134 }
135 }
136
137 #[tokio::test]
138 async fn test_get_connection_string() {
139 let mut mock_docker = MockDocker::new();
141
142 mock_docker
144 .expect_inspect_container()
145 .with(
146 mockall::predicate::eq("test-deployment"),
147 mockall::predicate::eq(None::<InspectContainerOptions>),
148 )
149 .times(1)
150 .returning(move |_, _| Ok(create_container_inspect_response_with_auth(27017)));
151
152 let client = Client::new(mock_docker);
153 let container_id_or_name = "test-deployment".to_string();
154
155 let result = client.get_connection_string(container_id_or_name).await;
157
158 assert!(result.is_ok());
160 assert_eq!(
161 result.unwrap(),
162 "mongodb://testuser:testpass@127.0.0.1:27017/?directConnection=true"
163 );
164 }
165
166 #[tokio::test]
167 async fn test_get_connection_string_no_auth() {
168 let mut mock_docker = MockDocker::new();
170
171 mock_docker
173 .expect_inspect_container()
174 .with(
175 mockall::predicate::eq("test-deployment"),
176 mockall::predicate::eq(None::<InspectContainerOptions>),
177 )
178 .times(1)
179 .returning(move |_, _| Ok(create_container_inspect_response_no_auth(27017)));
180
181 let client = Client::new(mock_docker);
182 let container_id_or_name = "test-deployment".to_string();
183
184 let result = client.get_connection_string(container_id_or_name).await;
186
187 assert!(result.is_ok());
189 assert_eq!(
190 result.unwrap(),
191 "mongodb://127.0.0.1:27017/?directConnection=true"
192 );
193 }
194
195 #[tokio::test]
196 async fn test_get_connection_string_get_deployment_error() {
197 let mut mock_docker = MockDocker::new();
199
200 mock_docker
202 .expect_inspect_container()
203 .with(
204 mockall::predicate::eq("nonexistent-deployment"),
205 mockall::predicate::eq(None::<InspectContainerOptions>),
206 )
207 .times(1)
208 .returning(|_, _| {
209 Err(BollardError::DockerResponseServerError {
210 status_code: 404,
211 message: "No such container".to_string(),
212 })
213 });
214
215 let client = Client::new(mock_docker);
216 let container_id_or_name = "nonexistent-deployment".to_string();
217
218 let result = client.get_connection_string(container_id_or_name).await;
220
221 assert!(result.is_err());
223 assert!(matches!(
224 result.unwrap_err(),
225 GetConnectionStringError::GetDeployment(_)
226 ));
227 }
228
229 #[tokio::test]
230 async fn test_get_connection_string_missing_port_binding() {
231 let mut mock_docker = MockDocker::new();
233 let container_inspect_response = ContainerInspectResponse {
234 id: Some("test_container_id".to_string()),
235 name: Some("/test-deployment".to_string()),
236 config: Some(ContainerConfig {
237 labels: Some(hashmap! {
238 "mongodb-atlas-local".to_string() => "container".to_string(),
239 "version".to_string() => "7.0.0".to_string(),
240 "mongodb-type".to_string() => "community".to_string(),
241 }),
242 env: Some(vec!["TOOL=ATLASCLI".to_string()]),
243 ..Default::default()
244 }),
245 state: Some(ContainerState {
246 status: Some(ContainerStateStatusEnum::RUNNING),
247 ..Default::default()
248 }),
249 network_settings: Some(bollard::secret::NetworkSettings {
250 ports: Some(hashmap! {}), ..Default::default()
252 }),
253 ..Default::default()
254 };
255
256 mock_docker
258 .expect_inspect_container()
259 .with(
260 mockall::predicate::eq("test-deployment"),
261 mockall::predicate::eq(None::<InspectContainerOptions>),
262 )
263 .times(1)
264 .returning(move |_, _| Ok(container_inspect_response.clone()));
265
266 let client = Client::new(mock_docker);
267 let container_id_or_name = "test-deployment".to_string();
268
269 let result = client.get_connection_string(container_id_or_name).await;
271
272 assert!(result.is_err());
274 assert!(matches!(
275 result.unwrap_err(),
276 GetConnectionStringError::MissingPortBinding
277 ));
278 }
279
280 #[tokio::test]
281 async fn test_get_connection_string_verify_success() {
282 let mut mock_docker = MockDocker::new();
284
285 mock_docker
287 .expect_inspect_container()
288 .with(
289 mockall::predicate::eq("test-deployment"),
290 mockall::predicate::eq(None::<InspectContainerOptions>),
291 )
292 .times(1)
293 .returning(move |_, _| Ok(create_container_inspect_response_with_auth(27017)));
294
295 let client = Client::new(mock_docker);
296
297 let container_id_or_name = "test-deployment".to_string();
298
299 let result = client.get_connection_string(container_id_or_name).await;
301
302 assert!(result.is_ok());
304 assert_eq!(
305 result.unwrap(),
306 "mongodb://testuser:testpass@127.0.0.1:27017/?directConnection=true"
307 );
308 }
309}