Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
.env

.claude

/docs

.superpowers
6 changes: 3 additions & 3 deletions docker-compose.databases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ services:

db-mongodb-auth:
container_name: db-mongodb-auth
image: mongo:latest
image: mongo:8.0.4
ports:
- "27082:27017"
environment:
Expand All @@ -75,14 +75,14 @@ services:
volumes:
- mongodb-data-auth:/data/db
healthcheck:
test: [ "CMD", "mongo", "--eval", "db.adminCommand('ping')" ]
test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')" ]
interval: 5s
timeout: 5s
retries: 10

db-mongodb:
container_name: db-mongodb
image: mongo:latest
image: mongo:8.0.4
Comment thread
RambokDev marked this conversation as resolved.
ports:
- "27083:27017"
volumes:
Expand Down
2 changes: 1 addition & 1 deletion src/domain/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pub mod docker_volume;
pub mod factory;
mod mongodb;
pub mod mongodb;
pub mod mysql;
pub mod postgres;
mod redis;
Expand Down
104 changes: 33 additions & 71 deletions src/domain/mongodb/connection.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::services::config::DatabaseConfig;
use anyhow::Result;
use mongodb::Client;
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};

const USERINFO_ENCODE: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'-')
Expand All @@ -27,7 +27,8 @@ pub fn get_mongo_uri(cfg: DatabaseConfig) -> Result<String> {
}

pub fn build_mongo_uri(cfg: &DatabaseConfig, include_db: bool) -> String {
let is_srv = cfg.port == 0;
let is_multi_host = cfg.host.contains(',');
let is_srv = cfg.port == 0 && !is_multi_host;
let scheme = if is_srv { "mongodb+srv" } else { "mongodb" };
let has_auth = !cfg.username.is_empty() && !cfg.password.is_empty();

Expand All @@ -41,7 +42,7 @@ pub fn build_mongo_uri(cfg: &DatabaseConfig, include_db: bool) -> String {
String::new()
};

let authority = if is_srv {
let authority = if is_srv || is_multi_host {
Comment thread
RambokDev marked this conversation as resolved.
cfg.host.clone()
} else {
format!("{}:{}", cfg.host, cfg.port)
Expand All @@ -53,7 +54,35 @@ pub fn build_mongo_uri(cfg: &DatabaseConfig, include_db: bool) -> String {
"/".to_string()
};

let query = if has_auth { "?authSource=admin" } else { "" };
let mut params: Vec<String> = Vec::new();

match cfg.options.get("auth_source").and_then(|v| v.as_str()) {
Some(s) if !s.is_empty() => params.push(format!(
"authSource={}",
utf8_percent_encode(s, USERINFO_ENCODE)
)),
_ if has_auth => params.push("authSource=admin".to_string()),
_ => {}
}

if let Some(rs) = cfg.options.get("replica_set").and_then(|v| v.as_str()) {
if !rs.is_empty() {
params.push(format!(
"replicaSet={}",
utf8_percent_encode(rs, USERINFO_ENCODE)
));
}
}

if cfg.options.get("tls").and_then(|v| v.as_bool()) == Some(true) {
params.push("tls=true".to_string());
}

let query = if params.is_empty() {
String::new()
} else {
format!("?{}", params.join("&"))
};

format!("{}://{}{}{}{}", scheme, credentials, authority, path, query)
}
Expand All @@ -71,70 +100,3 @@ pub fn extract_db_name(dry_output: &str) -> Option<String> {
}
dbs.into_iter().next()
}

#[cfg(test)]
mod tests {
use super::*;
use crate::services::config::{DatabaseConfig, DbType};
use std::collections::HashMap;

fn cfg(host: &str, port: u16, user: &str, pass: &str) -> DatabaseConfig {
DatabaseConfig {
name: "t".into(),
database: "mydb".into(),
db_type: DbType::MongoDB,
username: user.into(),
password: pass.into(),
port,
host: host.into(),
generated_id: "id".into(),
path: String::new(),
max_packet_size: String::new(),
volume_name: String::new(),
container_name: None,
options: HashMap::new(),
}
}

#[test]
fn standard_with_auth() {
let c = cfg("localhost", 27017, "user", "pass");
assert_eq!(
build_mongo_uri(&c, true),
"mongodb://user:pass@localhost:27017/mydb?authSource=admin"
);
}

#[test]
fn standard_no_auth() {
let c = cfg("localhost", 27017, "", "");
assert_eq!(build_mongo_uri(&c, true), "mongodb://localhost:27017/mydb");
}

#[test]
fn srv_with_auth() {
let c = cfg("cluster.example.mongodb.net", 0, "user", "pass");
assert_eq!(
build_mongo_uri(&c, true),
"mongodb+srv://user:pass@cluster.example.mongodb.net/mydb?authSource=admin"
);
}

#[test]
fn srv_no_db_for_dryrun() {
let c = cfg("cluster.example.mongodb.net", 0, "user", "pass");
assert_eq!(
build_mongo_uri(&c, false),
"mongodb+srv://user:pass@cluster.example.mongodb.net/?authSource=admin"
);
}

#[test]
fn encodes_special_chars_in_credentials() {
let c = cfg("cluster.example.mongodb.net", 0, "user", "p@ss:w/rd?");
assert_eq!(
build_mongo_uri(&c, true),
"mongodb+srv://user:p%40ss%3Aw%2Frd%3F@cluster.example.mongodb.net/mydb?authSource=admin"
);
}
}
2 changes: 1 addition & 1 deletion src/domain/mongodb/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod backup;
mod connection;
pub mod connection;
pub mod database;
mod ping;
mod restore;
99 changes: 99 additions & 0 deletions src/tests/domain/mongodb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,102 @@ async fn mongodb_backup_restore_test() {
}
}
}

use crate::domain::mongodb::connection::build_mongo_uri;

fn uri_cfg(host: &str, port: u16, user: &str, pass: &str) -> DatabaseConfig {
DatabaseConfig {
name: "t".into(),
database: "mydb".into(),
db_type: DbType::MongoDB,
username: user.into(),
password: pass.into(),
port,
host: host.into(),
generated_id: "id".into(),
path: String::new(),
max_packet_size: String::new(),
volume_name: String::new(),
container_name: None,
options: std::collections::HashMap::new(),
}
}

#[test]
fn uri_standard_with_auth() {
let c = uri_cfg("localhost", 27017, "user", "pass");
assert_eq!(
build_mongo_uri(&c, true),
"mongodb://user:pass@localhost:27017/mydb?authSource=admin"
);
}

#[test]
fn uri_standard_no_auth() {
let c = uri_cfg("localhost", 27017, "", "");
assert_eq!(build_mongo_uri(&c, true), "mongodb://localhost:27017/mydb");
}

#[test]
fn uri_srv_with_auth() {
let c = uri_cfg("cluster.example.mongodb.net", 0, "user", "pass");
assert_eq!(
build_mongo_uri(&c, true),
"mongodb+srv://user:pass@cluster.example.mongodb.net/mydb?authSource=admin"
);
}

#[test]
fn uri_srv_no_db_for_dryrun() {
let c = uri_cfg("cluster.example.mongodb.net", 0, "user", "pass");
assert_eq!(
build_mongo_uri(&c, false),
"mongodb+srv://user:pass@cluster.example.mongodb.net/?authSource=admin"
);
}

#[test]
fn uri_options_authsource_replicaset_tls() {
let mut c = uri_cfg("localhost", 27017, "user", "pass");
c.options.insert("auth_source".into(), "myauthdb".into());
c.options.insert("replica_set".into(), "rs0".into());
c.options.insert("tls".into(), serde_json::Value::Bool(true));
assert_eq!(
build_mongo_uri(&c, true),
"mongodb://user:pass@localhost:27017/mydb?authSource=myauthdb&replicaSet=rs0&tls=true"
);
}

#[test]
fn uri_multi_host_replica_set() {
let mut c = uri_cfg(
"mongodb0.example.internal:27017,mongodb1.example.internal:27017,mongodb2.example.internal:27017",
0,
"myDatabaseUser",
"D1fficultP@ssw0rd",
);
c.database = "myDB".into();
c.options.insert("replica_set".into(), "myRepl".into());
assert_eq!(
build_mongo_uri(&c, true),
"mongodb://myDatabaseUser:D1fficultP%40ssw0rd@mongodb0.example.internal:27017,mongodb1.example.internal:27017,mongodb2.example.internal:27017/myDB?authSource=admin&replicaSet=myRepl"
);
}

#[test]
fn uri_default_authsource_when_auth() {
let c = uri_cfg("localhost", 27017, "user", "pass");
assert_eq!(
build_mongo_uri(&c, true),
"mongodb://user:pass@localhost:27017/mydb?authSource=admin"
);
}

#[test]
fn uri_encodes_special_chars_in_credentials() {
let c = uri_cfg("cluster.example.mongodb.net", 0, "user", "p@ss:w/rd?");
assert_eq!(
build_mongo_uri(&c, true),
"mongodb+srv://user:p%40ss%3Aw%2Frd%3F@cluster.example.mongodb.net/mydb?authSource=admin"
);
}
Loading