1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
use serde::Deserialize;
use crate::{
options::{ClientOptions, ResolverConfig},
test::run_spec_test,
};
#[derive(Debug, Deserialize)]
struct TestFile {
uri: String,
seeds: Vec<String>,
hosts: Vec<String>,
options: Option<ResolvedOptions>,
parsed_options: Option<ParsedOptions>,
error: Option<bool>,
comment: Option<String>,
}
#[derive(Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct ResolvedOptions {
replica_set: Option<String>,
auth_source: Option<String>,
ssl: bool,
}
#[derive(Debug, Deserialize, Default, PartialEq)]
struct ParsedOptions {
user: Option<String>,
password: Option<String>,
db: Option<String>,
}
#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn run() {
async fn run_test(mut test_file: TestFile) {
// TODO DRIVERS-796: unskip this test
if test_file.uri == "mongodb+srv://test5.test.build.10gen.cc/?authSource=otherDB" {
return;
}
let result = if cfg!(target_os = "windows") {
ClientOptions::parse_with_resolver_config(&test_file.uri, ResolverConfig::cloudflare())
.await
} else {
ClientOptions::parse(&test_file.uri).await
};
if let Some(true) = test_file.error {
assert!(matches!(result, Err(_)), test_file.comment.unwrap());
return;
}
assert!(matches!(result, Ok(_)));
let options = result.unwrap();
let mut expected_seeds = test_file.seeds.split_off(0);
let mut actual_seeds = options
.hosts
.iter()
.map(|address| address.to_string())
.collect::<Vec<_>>();
expected_seeds.sort();
actual_seeds.sort();
assert_eq!(expected_seeds, actual_seeds,);
// The spec tests use two fields to compare against for the authentication database. In the
// `parsed_options` field, there is a `db` field which is populated with the database
// between the `/` and the querystring of the URI, and in the `options` field, there is an
// `authSource` field that is populated with whatever the driver should resolve `authSource`
// to from both the URI and the TXT options. Because we don't keep track of where we found
// the auth source in ClientOptions and the point of testing this behavior in this spec is
// to ensure that the right database is chosen based on both the URI and TXT options, we
// just determine what that should be and stick that in all of the options parsed from the
// spec JSON.
let resolved_db = test_file
.options
.as_ref()
.and_then(|opts| opts.auth_source.clone())
.or_else(|| {
test_file
.parsed_options
.as_ref()
.and_then(|opts| opts.db.clone())
});
if let Some(ref mut resolved_options) = test_file.options {
resolved_options.auth_source = resolved_db.clone();
let actual_options = ResolvedOptions {
replica_set: options.repl_set_name.clone(),
auth_source: options
.credential
.as_ref()
.and_then(|cred| cred.source.clone()),
ssl: options.tls_options().is_some(),
};
assert_eq!(&actual_options, resolved_options);
}
if let Some(mut parsed_options) = test_file.parsed_options {
parsed_options.db = resolved_db;
let actual_options = options
.credential
.map(|cred| ParsedOptions {
user: cred.username,
password: cred.password,
// In some spec tests, neither the `authSource` or `db` field are given, but in
// order to pass all the auth and URI options tests, the driver populates the
// credential's `source` field with "admin". To make it easier to assert here,
// we only populate the makeshift options with the credential's source if the
// JSON also specifies one of the database fields.
db: parsed_options.db.as_ref().and(cred.source),
})
.unwrap_or_default();
assert_eq!(parsed_options, actual_options);
}
}
run_spec_test(&["initial-dns-seedlist-discovery"], run_test).await;
}