1use crate::{
2 client::get_mongodb_secret::get_mongodb_secret,
3 docker::{DockerInspectContainer, RunCommandInContainer, RunCommandInContainerError},
4 models::MongoDBPortBinding,
5};
6use bollard::models::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::docker::DockerError;
98 use crate::{
99 client::Client,
100 docker::{
101 CommandOutput, DockerInspectContainer, RunCommandInContainer,
102 RunCommandInContainerError,
103 },
104 test_utils::{
105 create_container_inspect_response_no_auth, create_container_inspect_response_with_auth,
106 },
107 };
108 use bollard::{
109 models::{
110 ContainerConfig, ContainerInspectResponse, ContainerState, ContainerStateStatusEnum,
111 },
112 query_parameters::InspectContainerOptions,
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, DockerError>;
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(|_, _| Err(DockerError::NotFound));
209
210 let client = Client::new(mock_docker);
211 let container_id_or_name = "nonexistent-deployment".to_string();
212
213 let result = client.get_connection_string(container_id_or_name).await;
215
216 assert!(result.is_err());
218 assert!(matches!(
219 result.unwrap_err(),
220 GetConnectionStringError::GetDeployment(_)
221 ));
222 }
223
224 #[tokio::test]
225 async fn test_get_connection_string_missing_port_binding() {
226 let mut mock_docker = MockDocker::new();
228 let container_inspect_response = ContainerInspectResponse {
229 id: Some("test_container_id".to_string()),
230 name: Some("/test-deployment".to_string()),
231 config: Some(ContainerConfig {
232 labels: Some(hashmap! {
233 "mongodb-atlas-local".to_string() => "container".to_string(),
234 "version".to_string() => "7.0.0".to_string(),
235 "mongodb-type".to_string() => "community".to_string(),
236 }),
237 env: Some(vec!["TOOL=ATLASCLI".to_string()]),
238 ..Default::default()
239 }),
240 state: Some(ContainerState {
241 status: Some(ContainerStateStatusEnum::RUNNING),
242 ..Default::default()
243 }),
244 network_settings: Some(bollard::models::NetworkSettings {
245 ports: Some(hashmap! {}), ..Default::default()
247 }),
248 ..Default::default()
249 };
250
251 mock_docker
253 .expect_inspect_container()
254 .with(
255 mockall::predicate::eq("test-deployment"),
256 mockall::predicate::eq(None::<InspectContainerOptions>),
257 )
258 .times(1)
259 .returning(move |_, _| Ok(container_inspect_response.clone()));
260
261 let client = Client::new(mock_docker);
262 let container_id_or_name = "test-deployment".to_string();
263
264 let result = client.get_connection_string(container_id_or_name).await;
266
267 assert!(result.is_err());
269 assert!(matches!(
270 result.unwrap_err(),
271 GetConnectionStringError::MissingPortBinding
272 ));
273 }
274
275 #[tokio::test]
276 async fn test_get_connection_string_verify_success() {
277 let mut mock_docker = MockDocker::new();
279
280 mock_docker
282 .expect_inspect_container()
283 .with(
284 mockall::predicate::eq("test-deployment"),
285 mockall::predicate::eq(None::<InspectContainerOptions>),
286 )
287 .times(1)
288 .returning(move |_, _| Ok(create_container_inspect_response_with_auth(27017)));
289
290 let client = Client::new(mock_docker);
291
292 let container_id_or_name = "test-deployment".to_string();
293
294 let result = client.get_connection_string(container_id_or_name).await;
296
297 assert!(result.is_ok());
299 assert_eq!(
300 result.unwrap(),
301 "mongodb://testuser:testpass@127.0.0.1:27017/?directConnection=true"
302 );
303 }
304}