use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::OnceLock;
use std::sync::PoisonError;
use std::time::Duration;
use reqwest::StatusCode;
use serde::Deserialize;
use serde::Serialize;
use super::Address;
use super::Provider;
use super::ProviderCredentials;
use super::ProviderUrl;
use super::flat_item;
use crate::MonosecretError;
use crate::Result;
use crate::SecretBytes;
use crate::config::NativeAddress;
const API_BASE: &str = "https://api.doppler.com/v3";
const TOKEN: &str = "token";
const DOPPLER_TOKEN_ENV: &str = "DOPPLER_TOKEN";
pub(crate) const RESERVED_NAMES: [&str; 3] =
["DOPPLER_PROJECT", "DOPPLER_CONFIG", "DOPPLER_ENVIRONMENT"];
fn is_reserved(name: &str) -> bool {
RESERVED_NAMES.contains(&name)
}
fn validate_secret_name(name: &str) -> Result<()> {
if name.is_empty() {
return Err(operation_error(
"Invalid Doppler secret name: the name is empty.",
));
}
if name.starts_with(|c: char| c.is_ascii_digit()) {
return Err(operation_error(format!(
"Invalid Doppler secret name '{name}': names may not start with a number. \
Rename the secret in monosecret.toml -- Doppler cannot store this name, so \
Monosecret refuses it rather than storing it under a different one."
)));
}
if !name
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
{
return Err(operation_error(format!(
"Invalid Doppler secret name '{name}': names may only contain uppercase letters, \
numbers, and underscores. Rename the secret in monosecret.toml -- Doppler cannot \
store this name, so Monosecret refuses it rather than storing it under a \
different one."
)));
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ConfigSource {
Uri,
Profile,
Ref,
}
impl ConfigSource {
fn attribution(self) -> &'static str {
match self {
ConfigSource::Uri => "",
ConfigSource::Profile => " (named by the Monosecret profile)",
ConfigSource::Ref => " (named by the secret's ref)",
}
}
fn fix(self) -> &'static str {
match self {
ConfigSource::Uri => {
"Correct the config in the provider URI, e.g. doppler://myapp/prd."
}
ConfigSource::Profile => {
"Rename the profile, or pin a config in the provider URI, e.g. doppler://myapp/prd."
}
ConfigSource::Ref => "Correct the config in the ref, e.g. item = \"prd/API_KEY\".",
}
}
}
fn validate_config_name(config: &str, source: ConfigSource) -> Result<()> {
if is_doppler_slug(config) {
return Ok(());
}
Err(operation_error(format!(
"Invalid Doppler config '{config}'{}: config names may only contain lowercase \
letters, numbers, underscores and hyphens. {}",
source.attribution(),
source.fix()
)))
}
fn parse_item<'i>(
item: &'i str,
fallback: Option<(&'i str, ConfigSource)>,
) -> Result<(&'i str, &'i str)> {
let (config, name, source) = match item.split_once('/') {
Some((config, name)) => (config, name, ConfigSource::Ref),
None => {
match fallback {
Some((config, source)) => (config, item, source),
None => {
return Err(operation_error(format!(
"No Doppler config for the ref '{item}'. Name one in the ref as \
item = \"config/{item}\", pin one in the provider URI, e.g. \
doppler://myapp/prd, or select a profile to name it."
)));
}
}
}
};
validate_config_name(config, source)?;
validate_secret_name(name)?;
Ok((config, name))
}
fn validate_project_name(project: &str) -> Result<()> {
if project.chars().any(|c| c.is_ascii_uppercase()) {
return Err(operation_error(format!(
"Invalid Doppler project '{project}': Doppler lowercases project names, so this \
would address '{}'. Name it in lowercase, so one store has one URI.",
project.to_ascii_lowercase()
)));
}
if is_doppler_slug(project) {
return Ok(());
}
Err(operation_error(format!(
"Invalid Doppler project '{project}': project names may only contain lowercase letters, \
numbers, underscores and hyphens."
)))
}
fn is_doppler_slug(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
}
fn envelope_messages(body: &str) -> Option<String> {
let parsed = serde_json::from_str::<serde_json::Value>(body).ok()?;
let messages: Vec<&str> = parsed
.get("messages")?
.as_array()?
.iter()
.filter_map(serde_json::Value::as_str)
.collect();
(!messages.is_empty()).then(|| messages.join("; "))
}
fn error_message(body: &str) -> String {
envelope_messages(body).unwrap_or_else(|| truncate_chars(body, MAX_ERROR_BODY_BYTES))
}
const MAX_ERROR_BODY_BYTES: usize = 2 * 1024;
fn truncate_chars(text: &str, limit: usize) -> String {
if text.len() <= limit {
return text.to_string();
}
let end = (0..=limit)
.rev()
.find(|&n| text.is_char_boundary(n))
.unwrap_or(0);
format!("{}... (truncated)", &text[..end])
}
const RETRY_ATTEMPTS: u32 = 3;
const MAX_RETRY_DELAY: Duration = Duration::from_secs(10);
fn retry_delay(status: StatusCode, retry_after: Option<&str>, attempt: u32) -> Option<Duration> {
if attempt >= RETRY_ATTEMPTS {
return None;
}
if status != StatusCode::TOO_MANY_REQUESTS && !status.is_server_error() {
return None;
}
let suggested = retry_after
.and_then(|seconds| seconds.trim().parse::<u64>().ok())
.map(Duration::from_secs);
match suggested {
Some(wait) => Some(wait.min(MAX_RETRY_DELAY)),
None => Some(Duration::from_millis(250) * 2u32.pow(attempt - 1)),
}
}
#[cfg(not(test))]
fn retry_pause(wait: Duration) {
std::thread::sleep(wait);
}
#[cfg(test)]
fn retry_pause(wait: Duration) {
RETRY_PAUSES
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(wait);
}
#[cfg(test)]
static RETRY_PAUSES: Mutex<Vec<Duration>> = Mutex::new(Vec::new());
fn quoted_messages(body: &str) -> String {
envelope_messages(body)
.map(|messages| format!(" Doppler said: {messages}"))
.unwrap_or_default()
}
fn parse_secret_value(body: &str, name: &str) -> Result<Option<SecretBytes>> {
let parsed: serde_json::Value = serde_json::from_str(body).map_err(|e| {
operation_error(format!(
"Failed to parse Doppler's response for '{name}': {e}"
))
})?;
let value = parsed
.get("value")
.filter(|value| value.is_object())
.ok_or_else(|| {
operation_error(format!(
"Doppler's response for '{name}' carries no `value` object.{}",
quoted_messages(body)
))
})?;
secret_value(value, name)
}
fn parse_config_secrets(body: &str, config: &str) -> Result<HashMap<String, SecretBytes>> {
let parsed: serde_json::Value = serde_json::from_str(body).map_err(|e| {
operation_error(format!(
"Failed to parse Doppler's listing of config '{config}': {e}"
))
})?;
let secrets = parsed
.get("secrets")
.and_then(serde_json::Value::as_object)
.ok_or_else(|| {
operation_error(format!(
"Doppler's listing of config '{config}' carries no `secrets` object."
))
})?;
let mut listed = HashMap::with_capacity(secrets.len());
for (name, value) in secrets {
if is_reserved(name) {
continue;
}
if !value.is_object() {
return Err(operation_error(format!(
"Doppler's listing of config '{config}' carries {} rather than a secret \
object under '{name}'.",
json_type(value)
)));
}
if let Some(value) = secret_value(value, name)? {
listed.insert(name.clone(), value);
}
}
Ok(listed)
}
fn parse_secret_names(body: &str, config: &str) -> Result<Vec<String>> {
let parsed: serde_json::Value = serde_json::from_str(body).map_err(|e| {
operation_error(format!(
"Failed to parse Doppler's secret names for config '{config}': {e}"
))
})?;
let names = parsed
.get("names")
.and_then(serde_json::Value::as_array)
.ok_or_else(|| {
operation_error(format!(
"Doppler's secret names for config '{config}' carry no `names` array.{}",
quoted_messages(body)
))
})?;
let mut listed = Vec::with_capacity(names.len());
for name in names {
let name = name.as_str().ok_or_else(|| {
operation_error(format!(
"Doppler's secret names for config '{config}' carry {} where a name was \
expected.",
json_type(name)
))
})?;
if !is_reserved(name) {
listed.push(name.to_string());
}
}
Ok(listed)
}
fn validate_secret_value(name: &str, value: &str) -> Result<()> {
if !value.contains("${") {
return Ok(());
}
Err(operation_error(format!(
"Doppler cannot store the value of '{name}' unchanged: it contains '${{', which Doppler \
reads as a reference to another secret. Doppler refuses a reference it cannot resolve \
and rewrites one it can, so such a value never reads back as it was written. Store the \
value in a provider that keeps it verbatim, or remove the '${{'."
)))
}
const MAX_FILTER_BYTES: usize = 4 * 1024;
fn filter_chunks(wanted: &[String]) -> Vec<String> {
let mut chunks = Vec::new();
let mut chunk = String::new();
for name in wanted {
if !chunk.is_empty() && chunk.len() + 1 + name.len() > MAX_FILTER_BYTES {
chunks.push(std::mem::take(&mut chunk));
}
if !chunk.is_empty() {
chunk.push(',');
}
chunk.push_str(name);
}
chunks.push(chunk);
chunks
}
fn secret_value(value: &serde_json::Value, name: &str) -> Result<Option<SecretBytes>> {
let object = value.as_object().ok_or_else(|| {
operation_error(format!(
"Doppler's answer for '{name}' is not a secret object."
))
})?;
let Some(computed) = object.get("computed") else {
return Err(operation_error(format!(
"Doppler's answer for '{name}' carries no `computed` field, which is not a shape this \
provider recognizes."
)));
};
match computed {
serde_json::Value::String(computed) => Ok(Some(SecretBytes::from_utf8(computed.clone()))),
serde_json::Value::Null => {
let visibility = object
.get("computedVisibility")
.and_then(serde_json::Value::as_str)
.or_else(|| {
object
.get("rawVisibility")
.and_then(serde_json::Value::as_str)
});
match visibility {
Some(visibility) => {
Err(operation_error(format!(
"Doppler withheld the value of '{name}': the secret exists with visibility \
'{visibility}' but Doppler returned no value for it, so this token may see that \
it exists but not read it. Doppler does not serve a 'restricted' value to a \
token tied to a user identity, so use a service token (dp.st.) or a service \
account token (dp.sa.) rather than a personal (dp.pt.) or CLI (dp.ct.) one, or \
lower the secret's visibility in Doppler."
)))
}
None if object.get("raw").is_some_and(serde_json::Value::is_string) => {
Err(operation_error(format!(
"Doppler's answer for '{name}' carries a raw value but no computed one, which \
is not a shape this provider recognizes -- an absent secret nulls every field. \
Monosecret refuses it rather than reading it as a secret that is not set."
)))
}
None => Ok(None),
}
}
other => {
Err(operation_error(format!(
"Doppler returned {json_type} rather than a string as the value of '{name}', \
which Monosecret cannot hand to a process as an environment variable. Store \
the secret as a string in Doppler.",
json_type = json_type(other),
)))
}
}
}
fn json_type(value: &serde_json::Value) -> &'static str {
match value {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "a boolean",
serde_json::Value::Number(_) => "a number",
serde_json::Value::String(_) => "a string",
serde_json::Value::Array(_) => "an array",
serde_json::Value::Object(_) => "an object",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AbsentConfig {
IsAnError,
HoldsNothing,
}
fn http_error(project: &str, status: StatusCode, body: &str, action: &str) -> MonosecretError {
let message = error_message(body);
let detail = match status {
StatusCode::UNAUTHORIZED => {
format!(
"Doppler rejected the token (401) while {action}: {message}. Check the {TOKEN} \
credential or {DOPPLER_TOKEN_ENV}."
)
}
StatusCode::BAD_REQUEST => {
format!(
"Doppler refused the request (400) while {action}: {message}. This provider \
addresses project '{project}' explicitly; a service account token reaches only \
what its grants cover, and a service token (dp.st.) only the one project and \
config it is pinned to."
)
}
StatusCode::NOT_FOUND => {
format!(
"Doppler has no such project or config (404) while {action}: {message}. \
Projects and configs must already exist -- Monosecret does not create them."
)
}
StatusCode::TOO_MANY_REQUESTS => {
format!("Doppler rate limit exceeded (429) while {action}: {message}.")
}
_ => format!("Doppler returned HTTP {status} while {action}: {message}"),
};
operation_error(detail)
}
fn operation_error(message: impl Into<String>) -> MonosecretError {
MonosecretError::ProviderOperationFailed(message.into())
}
fn interpret_read(
loc: &Location,
status: StatusCode,
body: &str,
absent_config: AbsentConfig,
) -> Result<Option<SecretBytes>> {
if status == StatusCode::OK {
return parse_secret_value(body, &loc.name);
}
if status == StatusCode::NOT_FOUND && absent_config == AbsentConfig::HoldsNothing {
return Ok(None);
}
Err(http_error(
&loc.project,
status,
body,
&format!("reading '{}'", loc.name),
))
}
fn interpret_listing<T>(
project: &str,
config: &str,
status: StatusCode,
body: &str,
parse: impl FnOnce(&str, &str) -> Result<T>,
) -> Result<T> {
if status != StatusCode::OK {
return Err(http_error(
project,
status,
body,
&format!("listing config '{config}'"),
));
}
parse(body, config)
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DopplerConfig {
pub project: String,
pub config: Option<String>,
}
impl TryFrom<&ProviderUrl> for DopplerConfig {
type Error = MonosecretError;
fn try_from(url: &ProviderUrl) -> std::result::Result<Self, Self::Error> {
let scheme = url.scheme();
if scheme != "doppler" {
return Err(operation_error(format!(
"Invalid scheme '{scheme}' for doppler provider. Expected 'doppler'."
)));
}
if !url.username().is_empty()
|| url.password().is_some()
|| url.has_port()
|| url.has_query()
|| url.has_fragment()
{
return Err(operation_error(
"doppler:// takes only a project and an optional config, as the URI host \
and path, e.g. doppler://myapp/prd. User information, ports, query \
parameters and fragments are not supported.",
));
}
let project = url.host().filter(|host| !host.is_empty()).ok_or_else(|| {
operation_error(
"No Doppler project given. Name it in the URI, e.g. doppler://myapp or \
doppler://myapp/prd. The project is required: Monosecret never lets a \
pinned token choose its own coordinates, because a token repointed at \
another config would then change which secrets are served silently.",
)
})?;
validate_project_name(&project)?;
let path = url.path();
let config = match path.trim_matches('/') {
"" => None,
config => {
if config.contains('/') {
return Err(operation_error(format!(
"Invalid Doppler config '{config}': expected a single config name, \
e.g. doppler://myapp/prd. Doppler configs do not nest."
)));
}
validate_config_name(config, ConfigSource::Uri)?;
Some(config.to_string())
}
};
Ok(Self { project, config })
}
}
#[derive(Debug)]
struct Location {
project: String,
config: String,
name: String,
}
enum Call<'a> {
Read(&'a Location),
List {
project: &'a str,
config: &'a str,
names: &'a str,
},
Names { project: &'a str, config: &'a str },
Write(&'a Location, Option<&'a str>),
}
struct Request<'a> {
method: reqwest::Method,
path: &'static str,
query: Vec<(&'static str, &'a str)>,
body: Option<serde_json::Value>,
}
impl Call<'_> {
fn request(&self) -> Request<'_> {
match self {
Call::Read(loc) => {
Request {
method: reqwest::Method::GET,
path: "/configs/config/secret",
query: vec![
("project", loc.project.as_str()),
("config", loc.config.as_str()),
("name", loc.name.as_str()),
],
body: None,
}
}
Call::List {
project,
config,
names,
} => {
let mut query = vec![
("project", *project),
("config", *config),
("include_managed_secrets", "false"),
];
if !names.is_empty() {
query.push(("secrets", *names));
}
Request {
method: reqwest::Method::GET,
path: "/configs/config/secrets",
query,
body: None,
}
}
Call::Names { project, config } => {
Request {
method: reqwest::Method::GET,
path: "/configs/config/secrets/names",
query: vec![
("project", *project),
("config", *config),
("include_managed_secrets", "false"),
],
body: None,
}
}
Call::Write(loc, value) => {
Request {
method: reqwest::Method::POST,
path: "/configs/config/secrets",
query: Vec::new(),
body: Some(serde_json::json!({
"project": loc.project,
"config": loc.config,
"secrets": {
&loc.name: value,
},
})),
}
}
}
}
}
pub struct DopplerProvider {
config: DopplerConfig,
credentials: ProviderCredentials,
http: OnceLock<reqwest::Client>,
profile: Mutex<Option<String>>,
api_base: String,
#[cfg(test)]
allow_insecure_loopback: bool,
}
crate::register_provider! {
struct: DopplerProvider,
config: DopplerConfig,
metadata: &super::catalog::DOPPLER,
}
impl DopplerProvider {
pub fn new(config: DopplerConfig) -> Self {
Self {
config,
credentials: ProviderCredentials::new(),
http: OnceLock::new(),
profile: Mutex::new(None),
api_base: API_BASE.to_string(),
#[cfg(test)]
allow_insecure_loopback: false,
}
}
fn session_profile(&self) -> Option<String> {
self.profile
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
.filter(|profile| !profile.is_empty())
}
fn http(&self) -> Result<&reqwest::Client> {
if let Some(client) = self.http.get() {
return Ok(client);
}
#[cfg(not(test))]
let https_only = true;
#[cfg(test)]
let https_only = !self.allow_insecure_loopback;
let client = super::http::client_builder()
.redirect(reqwest::redirect::Policy::none())
.https_only(https_only)
.build()
.map_err(|e| {
operation_error(format!(
"Failed to build the Doppler HTTP client: {}",
crate::error::display_error_chain(&e)
))
})?;
Ok(self.http.get_or_init(|| client))
}
fn token(&self) -> Result<SecretBytes> {
super::credential_or_env(&self.credentials, TOKEN, DOPPLER_TOKEN_ENV)
.map(|token| SecretBytes::from_slice(token.expose_secret().trim_ascii()))
.filter(|token| !token.expose_secret().is_empty())
.ok_or_else(|| {
operation_error(format!(
"No Doppler token found. Configure the {TOKEN} provider credential, or set \
{DOPPLER_TOKEN_ENV}. Any Doppler token works; prefer a service account \
token (dp.sa.) or a service token (dp.st.), because Doppler does not serve \
a 'restricted' secret's value to a personal (dp.pt.) or CLI (dp.ct.) token."
))
})
}
fn locate(&self, addr: Address<'_>) -> Result<Location> {
let item = flat_item(self, addr)?;
let session = self.session_profile();
let fallback = self.implied_config(session.as_deref());
let (config, name) = parse_item(&item, fallback)?;
Ok(Location {
project: self.config.project.clone(),
config: config.to_string(),
name: name.to_string(),
})
}
fn implied_config<'a>(&'a self, profile: Option<&'a str>) -> Option<(&'a str, ConfigSource)> {
match (&self.config.config, profile) {
(Some(config), _) => Some((config.as_str(), ConfigSource::Uri)),
(None, Some(profile)) => Some((profile, ConfigSource::Profile)),
(None, None) => None,
}
}
fn config_for_profile(&self, profile: &str) -> Result<String> {
let (config, source) = self
.implied_config(Some(profile))
.unwrap_or((profile, ConfigSource::Profile));
if source == ConfigSource::Profile {
validate_config_name(config, source)?;
}
Ok(config.to_string())
}
fn locate_writable(&self, addr: Address<'_>) -> Result<Location> {
let loc = self.locate(addr)?;
if is_reserved(&loc.name) {
return Err(operation_error(format!(
"Doppler reserves the secret name '{}' for itself and refuses to store one \
under it, so Monosecret cannot either -- and a read of it can only ever \
return Doppler's own injected value. Rename the secret in monosecret.toml.",
loc.name
)));
}
Ok(loc)
}
async fn dispatch(&self, call: &Call<'_>) -> Result<reqwest::Response> {
let token = self.token()?;
let Request {
method,
path,
query,
body,
} = call.request();
let mut request = self
.http()?
.request(method, format!("{}{}", self.api_base, path))
.header(
reqwest::header::AUTHORIZATION,
super::credentials::credential_bearer_header(token.expose_secret())?,
)
.query(&query);
if let Some(body) = body {
request = request.json(&body);
}
request.send().await.map_err(|e| {
operation_error(format!(
"Failed to connect to Doppler at {}: {}",
self.api_base,
crate::error::display_error_chain(&e)
))
})
}
async fn send(&self, call: &Call<'_>) -> Result<reqwest::Response> {
let mut attempt = 1;
loop {
let response = self.dispatch(call).await?;
let retry_after = response
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|value| value.to_str().ok());
match retry_delay(response.status(), retry_after, attempt) {
Some(wait) => {
retry_pause(wait);
attempt += 1;
}
None => return Ok(response),
}
}
}
async fn execute(&self, call: &Call<'_>) -> Result<(StatusCode, String)> {
let response = self.send(call).await?;
let status = response.status();
let body = Self::response_body(response).await?;
Ok((status, body))
}
async fn response_body(response: reqwest::Response) -> Result<String> {
let status = response.status();
response.text().await.map_err(|e| {
operation_error(format!(
"Failed to read Doppler's HTTP {status} response body: {}",
crate::error::display_error_chain(&e)
))
})
}
async fn get_async(
&self,
loc: &Location,
absent_config: AbsentConfig,
) -> Result<Option<SecretBytes>> {
if is_reserved(&loc.name) {
return Ok(None);
}
let (status, body) = self.execute(&Call::Read(loc)).await?;
interpret_read(loc, status, &body, absent_config)
}
async fn list_async(
&self,
config: &str,
wanted: &[String],
) -> Result<HashMap<String, SecretBytes>> {
let mut listed = HashMap::with_capacity(wanted.len());
for names in filter_chunks(wanted) {
let call = Call::List {
project: &self.config.project,
config,
names: &names,
};
let (status, body) = self.execute(&call).await?;
listed.extend(interpret_listing(
&self.config.project,
config,
status,
&body,
parse_config_secrets,
)?);
}
Ok(listed)
}
async fn names_async(&self, config: &str) -> Result<Vec<String>> {
let call = Call::Names {
project: &self.config.project,
config,
};
let (status, body) = self.execute(&call).await?;
interpret_listing(
&self.config.project,
config,
status,
&body,
parse_secret_names,
)
}
async fn write_async(&self, loc: &Location, value: Option<&str>) -> Result<()> {
let response = self.send(&Call::Write(loc, value)).await?;
let status = response.status();
if status.is_success() {
return Ok(());
}
let body = Self::response_body(response).await?;
let action = match value {
Some(_) => format!("writing '{}'", loc.name),
None => format!("deleting '{}'", loc.name),
};
Err(http_error(&loc.project, status, &body, &action))
}
}
impl Provider for DopplerProvider {
fn convention_address(
&self,
_project: &str,
profile: &str,
key: &str,
) -> Result<NativeAddress> {
validate_secret_name(key)?;
let config = self.config_for_profile(profile)?;
Ok(NativeAddress {
item: format!("{config}/{key}"),
..Default::default()
})
}
fn configured_entry_coordinates<'a>(
&self,
addr: Address<'a>,
) -> Result<Cow<'a, NativeAddress>> {
let loc = self.locate(addr)?;
Ok(Cow::Owned(NativeAddress {
item: format!("{}/{}", loc.config, loc.name),
..Default::default()
}))
}
fn check_deletable(&self, addr: Address<'_>) -> Result<()> {
self.locate_writable(addr).map(|_| ())
}
fn supports_delete(&self) -> bool {
true
}
fn describe_write_target(&self, addr: Address<'_>) -> Result<String> {
let loc = self.locate(addr)?;
Ok(format!(
"Doppler project '{}' config '{}' secret '{}'",
loc.project, loc.config, loc.name
))
}
fn with_credentials(&mut self, credentials: ProviderCredentials) {
self.credentials = credentials;
}
fn set_profile(&self, profile: &str) {
*self.profile.lock().unwrap_or_else(PoisonError::into_inner) = Some(profile.to_string());
}
fn name(&self) -> &'static str {
Self::PROVIDER_NAME
}
fn uri(&self) -> String {
match &self.config.config {
Some(config) => format!("doppler://{}/{config}", self.config.project),
None => format!("doppler://{}", self.config.project),
}
}
fn entry_container_identity(&self) -> String {
format!("doppler://{}", self.config.project)
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretBytes>> {
let loc = self.locate(addr)?;
super::block_on(self.get_async(&loc, AbsentConfig::IsAnError))
}
fn get_many(&self, requests: &[(&str, Address<'_>)]) -> Result<HashMap<String, SecretBytes>> {
if requests.is_empty() {
return Ok(HashMap::new());
}
let mut by_config: HashMap<String, Vec<(&str, String)>> = HashMap::new();
for (name, addr) in requests {
let loc = self.locate(*addr)?;
by_config
.entry(loc.config)
.or_default()
.push((name, loc.name));
}
let groups: Vec<(String, Vec<(&str, String)>)> = by_config.into_iter().collect();
let listings = super::map_concurrently(&groups, super::get_each_concurrency(), |group| {
let (config, wanted) = group;
let mut names: Vec<String> = wanted
.iter()
.map(|(_, key)| key.clone())
.filter(|key| !is_reserved(key))
.collect();
names.sort_unstable();
names.dedup();
if names.is_empty() {
return Ok(HashMap::new());
}
super::block_on(self.list_async(config, &names))
});
let mut resolved = HashMap::new();
for ((_, wanted), listed) in groups.iter().zip(listings) {
let listed = listed?;
for (name, key) in wanted {
if let Some(value) = listed.get(key) {
resolved.insert((*name).to_string(), value.clone());
}
}
}
Ok(resolved)
}
fn check_writable(&self, addr: Address<'_>) -> Result<()> {
self.locate_writable(addr).map(|_| ())
}
fn set(&self, addr: Address<'_>, value: &SecretBytes) -> Result<()> {
let loc = self.locate_writable(addr)?;
let value = super::require_utf8("doppler", value)?;
validate_secret_value(&loc.name, value)?;
super::block_on(self.write_async(&loc, Some(value)))
}
fn delete(&self, addr: Address<'_>) -> Result<bool> {
let loc = self.locate_writable(addr)?;
super::block_on(async {
if self
.get_async(&loc, AbsentConfig::HoldsNothing)
.await?
.is_none()
{
return Ok(false);
}
self.write_async(&loc, None).await?;
Ok(true)
})
}
fn reflect(
&self,
context: super::DiscoveryContext<'_>,
) -> Result<HashMap<String, crate::Secret>> {
let config = self.config_for_profile(context.profile)?;
let names = super::block_on(self.names_async(&config))?;
Ok(names
.into_iter()
.map(|name| {
let secret = crate::Secret::required(format!("{name} Doppler secret"));
(name, secret)
})
.collect())
}
}
#[cfg(test)]
mod tests {
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::net::SocketAddr;
use std::net::TcpListener;
use url::Url;
use super::*;
fn config(s: &str) -> DopplerConfig {
DopplerConfig::try_from(&ProviderUrl::new(Url::parse(s).unwrap())).unwrap()
}
fn provider(s: &str) -> DopplerProvider {
DopplerProvider::new(config(s))
}
fn native(item: &str) -> NativeAddress {
NativeAddress {
item: item.into(),
..Default::default()
}
}
fn location(project: &str, config: &str, name: &str) -> Location {
Location {
project: project.into(),
config: config.into(),
name: name.into(),
}
}
fn single_read(raw: &str, computed: &str) -> String {
serde_json::json!({
"name": "MONGO_CONNECTION",
"value": {
"raw": raw,
"computed": computed,
"note": "",
"rawVisibility": "masked",
"computedVisibility": "masked",
"rawValueType": { "type": "string" },
"computedValueType": { "type": "string" },
},
"success": true,
})
.to_string()
}
fn restricted_read() -> String {
serde_json::json!({
"name": "MONGO_CONNECTION",
"value": {
"raw": null,
"computed": null,
"note": "",
"rawVisibility": "restricted",
"computedVisibility": "restricted",
"rawValueType": { "type": "string" },
"computedValueType": { "type": "string" },
},
"success": true,
})
.to_string()
}
#[test]
fn uri_names_the_project_and_optionally_the_config() {
let c = config("doppler://myapp");
assert_eq!(c.project, "myapp");
assert_eq!(c.config, None);
let c = config("doppler://myapp/prd");
assert_eq!(c.project, "myapp");
assert_eq!(c.config.as_deref(), Some("prd"));
}
#[test]
fn uri_without_a_project_is_rejected() {
let err = DopplerConfig::try_from(&ProviderUrl::new(Url::parse("doppler://").unwrap()))
.unwrap_err();
assert!(err.to_string().contains("No Doppler project"), "{err}");
}
#[test]
fn uri_with_a_nested_config_is_rejected() {
let err = DopplerConfig::try_from(&ProviderUrl::new(
Url::parse("doppler://myapp/prd/extra").unwrap(),
))
.unwrap_err();
assert!(err.to_string().contains("do not nest"), "{err}");
}
#[test]
fn wrong_scheme_is_rejected() {
let err = DopplerConfig::try_from(&ProviderUrl::new(Url::parse("bws://myapp").unwrap()))
.unwrap_err();
assert!(err.to_string().contains("Invalid scheme"), "{err}");
}
#[test]
fn two_spellings_of_one_config_are_the_same_entry() {
let unpinned = provider("doppler://myapp");
let pinned = provider("doppler://myapp/dev");
let addr = Address::convention("proj", "dev", "API_KEY");
assert!(
unpinned.same_entry(&pinned, addr).unwrap(),
"doppler://myapp under profile dev is the same secret as doppler://myapp/dev"
);
assert!(
pinned.same_entry(&unpinned, addr).unwrap(),
"and vice versa"
);
let other = provider("doppler://myapp/prd");
assert!(
!pinned.same_entry(&other, addr).unwrap(),
"dev and prd hold distinct secrets"
);
let elsewhere = provider("doppler://other/dev");
assert!(
!pinned.same_entry(&elsewhere, addr).unwrap(),
"one config name in two projects is two secrets"
);
}
#[test]
fn sibling_configs_are_distinct_stores_for_cache_routing() {
assert_ne!(
provider("doppler://myapp/cache").storage_identity(),
provider("doppler://myapp/prd").storage_identity()
);
}
#[test]
fn uri_round_trips() {
for spec in ["doppler://myapp", "doppler://myapp/prd"] {
assert_eq!(provider(spec).uri(), spec);
}
}
#[test]
fn a_project_name_that_would_break_the_uri_is_rejected() {
for spec in [
"doppler://my%2Fapp",
"doppler://my.app",
"doppler://my%20app",
] {
let err = DopplerConfig::try_from(&ProviderUrl::new(Url::parse(spec).unwrap()))
.expect_err("a project name Doppler cannot spell must be refused");
assert!(
err.to_string().contains("lowercase letters"),
"{spec}: {err}"
);
}
}
#[test]
fn an_uppercase_project_is_refused_because_doppler_lowercases_it() {
let err = DopplerConfig::try_from(&ProviderUrl::new(
Url::parse("doppler://MyApp/prd").unwrap(),
))
.expect_err("an uppercase project must be refused");
let err = err.to_string();
assert!(err.contains("lowercases"), "{err}");
assert!(err.contains("myapp"), "{err}");
}
#[test]
fn a_uri_userinfo_or_port_is_rejected() {
for spec in [
"doppler://user@myapp/prd",
"doppler://user:pw@myapp/prd",
"doppler://myapp:8080/prd",
] {
let err = DopplerConfig::try_from(&ProviderUrl::new(Url::parse(spec).unwrap()))
.expect_err("userinfo and ports must be refused");
assert!(err.to_string().contains("not supported"), "{spec}: {err}");
}
}
#[test]
fn the_profile_names_the_config_and_the_key_is_verbatim() {
let p = provider("doppler://myapp");
let addr = p
.convention_address("myapp", "production", "MONGO_CONNECTION")
.unwrap();
assert_eq!(addr.item, "production/MONGO_CONNECTION");
let loc = p
.locate(Address::convention(
"myapp",
"production",
"MONGO_CONNECTION",
))
.unwrap();
assert_eq!(loc.project, "myapp");
assert_eq!(loc.config, "production");
assert_eq!(loc.name, "MONGO_CONNECTION");
}
#[test]
fn a_pinned_config_wins_over_the_profile() {
let p = provider("doppler://myapp/prd");
let coords = p
.locate(Address::convention("myapp", "production", "API_KEY"))
.unwrap();
assert_eq!(coords.config, "prd");
assert_eq!(coords.name, "API_KEY");
}
#[test]
fn every_profile_gets_its_own_config() {
let p = provider("doppler://myapp");
let dev = p.convention_address("myapp", "dev", "API_KEY").unwrap();
let prod = p.convention_address("myapp", "prod", "API_KEY").unwrap();
assert_ne!(dev.item, prod.item);
}
#[test]
fn monosecret_projects_share_one_doppler_namespace() {
let p = provider("doppler://myapp");
let one = p.convention_address("one", "dev", "API_KEY").unwrap();
let other = p.convention_address("other", "dev", "API_KEY").unwrap();
assert_eq!(one.item, other.item);
}
#[test]
fn unspellable_secret_names_are_refused() {
let p = provider("doppler://myapp");
for key in [
"lowercase_name",
"Mixed_Case",
"WITH-HYPHEN",
"HAS SPACE",
"HAS.DOT",
"WITH/SLASH",
"UNICODE_ÑAME",
] {
let err = p.convention_address("myapp", "dev", key).unwrap_err();
assert!(
err.to_string()
.contains("may only contain uppercase letters, numbers, and underscores"),
"{key}: {err}"
);
}
let err = p
.convention_address("myapp", "dev", "1STARTS_DIGIT")
.unwrap_err();
assert!(
err.to_string().contains("may not start with a number"),
"{err}"
);
}
#[test]
fn a_leading_underscore_is_accepted() {
let p = provider("doppler://myapp");
for key in ["_LEADING_UNDERSCORE", "UPPER_OK", "WITH_9_DIGIT"] {
assert!(p.convention_address("myapp", "dev", key).is_ok(), "{key}");
}
}
#[test]
fn a_bad_config_names_the_place_it_came_from() {
let p = provider("doppler://myapp");
let addr = native("Shared/SHARED_CA");
let err = p.locate(Address::Native(&addr)).unwrap_err().to_string();
assert!(err.contains("named by the secret's ref"), "{err}");
assert!(err.contains("item = \"prd/API_KEY\""), "{err}");
assert!(
!err.contains("provider URI"),
"a ref's config is not fixed in the URI: {err}"
);
let err = p
.convention_address("myapp", "Production", "API_KEY")
.unwrap_err()
.to_string();
assert!(err.contains("named by the Monosecret profile"), "{err}");
assert!(err.contains("Rename the profile"), "{err}");
let err = DopplerConfig::try_from(&ProviderUrl::new(
Url::parse("doppler://myapp/Production").unwrap(),
))
.unwrap_err()
.to_string();
assert!(
err.contains("Correct the config in the provider URI"),
"{err}"
);
assert!(!err.contains("named by"), "{err}");
}
#[test]
fn a_ref_naming_an_unspellable_secret_is_refused() {
let p = provider("doppler://myapp");
let addr = native("dev/lowercase");
let err = p.locate(Address::Native(&addr)).unwrap_err();
assert!(
err.to_string()
.contains("may only contain uppercase letters"),
"{err}"
);
}
#[test]
fn a_profile_that_cannot_name_a_config_is_refused() {
let p = provider("doppler://myapp");
let err = p
.convention_address("myapp", "Production", "API_KEY")
.unwrap_err();
assert!(err.to_string().contains("lowercase letters"), "{err}");
assert!(err.to_string().contains("doppler://myapp/prd"), "{err}");
let pinned = provider("doppler://myapp/prd");
assert!(
pinned
.convention_address("myapp", "Production", "API_KEY")
.is_ok()
);
}
#[test]
fn a_ref_names_its_own_config() {
let p = provider("doppler://myapp");
let addr = native("stg/DATABASE_URL");
let coords = p.locate(Address::Native(&addr)).unwrap();
assert_eq!(coords.config, "stg");
assert_eq!(coords.name, "DATABASE_URL");
let bare = native("DATABASE_URL");
let err = p.locate(Address::Native(&bare)).unwrap_err();
assert!(err.to_string().contains("No Doppler config"), "{err}");
p.set_profile("dev");
let coords = p.locate(Address::Native(&bare)).unwrap();
assert_eq!(coords.config, "dev");
assert_eq!(coords.name, "DATABASE_URL");
p.set_profile("Production");
let err = p.locate(Address::Native(&bare)).unwrap_err();
assert!(
err.to_string().contains("named by the Monosecret profile"),
"{err}"
);
let pinned = provider("doppler://myapp/prd");
pinned.set_profile("dev");
let coords = pinned.locate(Address::Native(&bare)).unwrap();
assert_eq!(coords.config, "prd");
assert_eq!(coords.name, "DATABASE_URL");
}
#[test]
fn native_address_rejects_field() {
let p = provider("doppler://myapp/prd");
let addr = NativeAddress {
item: "DATABASE_URL".into(),
field: Some("password".into()),
..Default::default()
};
let err = p.locate(Address::Native(&addr)).unwrap_err();
assert!(err.to_string().contains("`field`"), "{err}");
let versioned = NativeAddress {
item: "DATABASE_URL".into(),
version: Some("3".into()),
..Default::default()
};
let err = p.locate(Address::Native(&versioned)).unwrap_err();
assert!(err.to_string().contains("`version`"), "{err}");
}
#[test]
fn a_reference_is_read_resolved() {
let body = single_read("postgres://${PLAIN_HOST}/app", "postgres://db.internal/app");
let value = parse_secret_value(&body, "MONGO_CONNECTION")
.unwrap()
.expect("a stored secret");
assert_eq!(value.expose_secret(), b"postgres://db.internal/app");
assert!(
!value.try_as_utf8().unwrap().contains("${"),
"the reference stayed"
);
}
#[test]
fn set_refuses_whatever_the_pre_check_refuses() {
let p = provider("doppler://myapp/prd");
let value = SecretBytes::from_utf8("v");
let field = NativeAddress {
item: "DATABASE_URL".into(),
field: Some("password".into()),
..Default::default()
};
let unpinned = provider("doppler://myapp");
let bare = native("DATABASE_URL");
let cases: [(&DopplerProvider, Address<'_>); 4] = [
(&p, Address::convention("proj", "dev", "lowercase")),
(&p, Address::convention("proj", "dev", "DOPPLER_CONFIG")),
(&p, Address::Native(&field)),
(&unpinned, Address::Native(&bare)),
];
for (provider, addr) in cases {
let refusal = provider
.check_writable(addr)
.expect_err("the pre-check must refuse this address");
let from_set = provider
.set(addr, &value)
.expect_err("set must refuse whatever the pre-check refuses");
assert_eq!(
from_set.to_string(),
refusal.to_string(),
"set and check_writable must give one reason, not two"
);
}
}
#[test]
fn a_value_doppler_would_rewrite_is_refused() {
let p = provider("doppler://myapp/prd");
let referencing = SecretBytes::from_utf8("postgres://${DB_HOST}/app");
let refusal = validate_secret_value("DATABASE_URL", referencing.try_as_utf8().unwrap())
.expect_err("a value Doppler would rewrite must be refused");
let refusal = refusal.to_string();
assert!(refusal.contains("DATABASE_URL"), "{refusal}");
assert!(
!refusal.contains("postgres://") && !refusal.contains("DB_HOST"),
"the refusal must not echo the secret value: {refusal}"
);
let from_set = p
.set(
Address::convention("proj", "dev", "DATABASE_URL"),
&referencing,
)
.expect_err("set must refuse a value Doppler would rewrite");
assert_eq!(from_set.to_string(), refusal);
validate_secret_value("DATABASE_URL", "postgres://db.internal/app")
.expect("a value with no reference is storable");
}
#[test]
fn reflected_names_exclude_the_reserved_ones() {
let body = serde_json::json!({
"names": [
"DOPPLER_CONFIG", "DOPPLER_ENVIRONMENT", "DOPPLER_PROJECT",
"DATABASE_URL", "API_KEY",
],
"success": true,
})
.to_string();
let mut names = parse_secret_names(&body, "prd").unwrap();
names.sort();
assert_eq!(names, ["API_KEY", "DATABASE_URL"]);
}
#[test]
fn an_unrecognized_name_entry_is_an_error() {
let body = r#"{"names":["API_KEY",{"name":"DATABASE_URL"}],"success":true}"#;
let err = parse_secret_names(body, "prd").unwrap_err().to_string();
assert!(err.contains("an object"), "{err}");
assert!(err.contains("prd"), "{err}");
}
#[test]
fn a_dangling_reference_reads_as_the_template_doppler_serves() {
let dangling = single_read("pg://${PROBE_TARGET}/app", "pg://${PROBE_TARGET}/app");
let value = parse_secret_value(&dangling, "PROBE_REF")
.expect("a dangling reference is a value, not a refusal")
.expect("Doppler serves the template");
assert_eq!(value.expose_secret(), b"pg://${PROBE_TARGET}/app");
}
#[test]
fn a_masked_value_is_not_withheld() {
let body = single_read("s3cret", "s3cret");
let value = parse_secret_value(&body, "MONGO_CONNECTION")
.unwrap()
.expect("a masked secret is still readable");
assert_eq!(value.expose_secret(), b"s3cret");
}
#[test]
fn a_missing_secret_is_none_and_an_empty_one_is_not() {
let missing = serde_json::json!({
"name": "NONEXISTENT_SECRET_XYZ",
"value": {
"raw": null, "computed": null, "note": null,
"rawVisibility": null, "computedVisibility": null,
"rawValueType": null, "computedValueType": null,
},
"success": true,
})
.to_string();
assert!(
parse_secret_value(&missing, "NONEXISTENT_SECRET_XYZ")
.unwrap()
.is_none()
);
let empty = single_read("", "");
let value = parse_secret_value(&empty, "MONGO_CONNECTION")
.unwrap()
.expect("an empty string is a stored value");
assert_eq!(value.expose_secret(), b"");
}
#[test]
fn a_withheld_value_is_refused_not_dropped() {
let err = parse_secret_value(&restricted_read(), "MONGO_CONNECTION").unwrap_err();
assert!(err.to_string().contains("withheld"), "{err}");
assert!(err.to_string().contains("restricted"), "{err}");
let body = serde_json::json!({
"secrets": {
"MONGO_CONNECTION": {
"raw": null, "computed": null,
"rawVisibility": "restricted", "computedVisibility": "restricted",
},
},
"success": true,
})
.to_string();
let err = parse_config_secrets(&body, "prd").unwrap_err();
assert!(err.to_string().contains("withheld"), "{err}");
}
#[test]
fn an_unrecognized_200_body_is_an_error() {
for body in [
r#"{"messages":["Invalid Auth"],"success":false}"#,
r#"{"name":"MONGO_CONNECTION","value":"a plain string"}"#,
"{}",
] {
let err = parse_secret_value(body, "MONGO_CONNECTION")
.unwrap_err()
.to_string();
assert!(err.contains("no `value` object"), "{body}: {err}");
}
let err = parse_secret_value(r#"{"messages":["Invalid Auth"]}"#, "MONGO_CONNECTION")
.unwrap_err()
.to_string();
assert!(err.contains("Invalid Auth"), "{err}");
}
#[test]
fn an_unrecognized_200_body_is_never_echoed() {
let renamed_field = r#"{"name":"MONGO_CONNECTION","secret":{"raw":"mongodb://leaked_pw_DO_NOT_ECHO@h/db","computed":"mongodb://leaked_pw_DO_NOT_ECHO@h/db"},"success":true}"#;
let err = parse_secret_value(renamed_field, "MONGO_CONNECTION")
.unwrap_err()
.to_string();
assert!(err.contains("no `value` object"), "{err}");
assert!(
!err.contains("leaked_pw_DO_NOT_ECHO"),
"a 200 body may hold the secret's plaintext and must not be echoed: {err}"
);
assert_eq!(error_message("upstream timeout"), "upstream timeout");
}
#[test]
fn a_non_string_value_is_named_rather_than_called_withheld() {
let body = serde_json::json!({
"name": "PORT",
"value": {
"raw": 5432,
"computed": 5432,
"rawVisibility": "masked",
"computedVisibility": "masked",
},
"success": true,
})
.to_string();
let err = parse_secret_value(&body, "PORT").unwrap_err().to_string();
assert!(err.contains("a number"), "{err}");
assert!(err.contains("PORT"), "{err}");
assert!(!err.contains("withheld"), "{err}");
assert!(
!err.contains("5432"),
"the refusal must not echo the value: {err}"
);
}
#[test]
fn a_uri_query_is_rejected() {
let err = DopplerConfig::try_from(&ProviderUrl::new(
Url::parse("doppler://myapp?config=prd").unwrap(),
))
.unwrap_err();
assert!(err.to_string().contains("query parameters"), "{err}");
}
#[test]
fn a_uri_fragment_is_rejected() {
for spec in ["doppler://myapp#prd", "doppler://myapp/prd#dev"] {
let err = DopplerConfig::try_from(&ProviderUrl::new(Url::parse(spec).unwrap()))
.expect_err("a fragment must be refused");
assert!(err.to_string().contains("fragments"), "{spec}: {err}");
}
}
#[test]
fn check_writable_refuses_what_set_would() {
let p = provider("doppler://myapp");
for (what, addr) in [
("a key Doppler cannot spell", ("dev", "lowercase_key")),
(
"a profile that cannot name a config",
("Production", "API_KEY"),
),
] {
let addr = Address::convention("myapp", addr.0, addr.1);
assert!(p.check_writable(addr).is_err(), "{what} was accepted");
}
let bare = native("DATABASE_URL");
assert!(p.check_writable(Address::Native(&bare)).is_err());
let err = p
.check_writable(Address::convention("myapp", "dev", "DOPPLER_CONFIG"))
.unwrap_err();
assert!(
err.to_string().contains("reserves the secret name"),
"{err}"
);
assert!(
p.check_writable(Address::convention("myapp", "dev", "API_KEY"))
.is_ok()
);
}
#[test]
fn check_deletable_refuses_what_delete_would() {
let p = provider("doppler://myapp/prd");
for reserved in RESERVED_NAMES {
let addr = native(reserved);
let err = p
.check_deletable(Address::Native(&addr))
.expect_err("a reserved name must be refused before anything is removed");
assert!(
err.to_string().contains("reserves the secret name"),
"{reserved}: {err}"
);
}
let ok = native("DATABASE_URL");
assert!(p.check_deletable(Address::Native(&ok)).is_ok());
}
#[test]
fn entry_coordinates_carry_the_resolved_config() {
let pinned = provider("doppler://myapp/prd");
let bare = native("DATABASE_URL");
let coords = pinned.entry_coordinates(Address::Native(&bare)).unwrap();
assert_eq!(coords.item, "prd/DATABASE_URL");
let qualified = native("stg/DATABASE_URL");
let coords = pinned
.entry_coordinates(Address::Native(&qualified))
.unwrap();
assert_eq!(coords.item, "stg/DATABASE_URL");
let unpinned = provider("doppler://myapp");
let coords = unpinned
.entry_coordinates(Address::convention("myapp", "dev", "DATABASE_URL"))
.unwrap();
assert_eq!(coords.item, "dev/DATABASE_URL");
}
#[test]
fn reserved_names_are_filtered_from_a_listing() {
let body = serde_json::json!({
"secrets": {
"DOPPLER_PROJECT": { "raw": "", "computed": "myapp" },
"DOPPLER_CONFIG": { "raw": "", "computed": "dev" },
"DOPPLER_ENVIRONMENT": { "raw": "", "computed": "dev" },
"MONGO_CONNECTION": { "raw": "mongodb://h/db", "computed": "mongodb://h/db" },
},
"success": true,
})
.to_string();
let listed = parse_config_secrets(&body, "dev").unwrap();
assert_eq!(listed.keys().collect::<Vec<_>>(), ["MONGO_CONNECTION"]);
for reserved in RESERVED_NAMES {
assert!(!listed.contains_key(reserved), "{reserved} survived");
}
}
#[test]
fn an_unrecognized_listing_entry_is_an_error() {
let body = r#"{"secrets":{"API_KEY":"plaintext_DO_NOT_ECHO"},"success":true}"#;
let err = parse_config_secrets(body, "prd").unwrap_err().to_string();
assert!(err.contains("a string"), "{err}");
assert!(err.contains("API_KEY"), "{err}");
assert!(
!err.contains("DO_NOT_ECHO"),
"the entry's content must not be echoed: {err}"
);
}
#[test]
fn a_reserved_name_reads_as_missing() {
let p = provider("doppler://myapp/dev");
for name in RESERVED_NAMES {
assert!(
p.get(Address::convention("unused", "dev", name))
.expect("a reserved name is answered locally")
.is_none(),
"Doppler's own injected value must never be served as a secret: {name}"
);
}
}
#[test]
fn a_listing_reads_computed() {
let body = serde_json::json!({
"secrets": {
"PLAIN_HOST": { "raw": "db.internal", "computed": "db.internal" },
"DERIVED_URL": {
"raw": "postgres://${PLAIN_HOST}/app",
"computed": "postgres://db.internal/app",
},
},
"success": true,
})
.to_string();
let listed = parse_config_secrets(&body, "dev").unwrap();
assert_eq!(
listed.get("DERIVED_URL").map(SecretBytes::expose_secret),
Some(b"postgres://db.internal/app".as_slice())
);
}
#[test]
fn doppler_error_messages_are_quoted_verbatim() {
let envelope = r#"{"messages":["This token does not have access to requested config 'prod'"],"success":false}"#;
assert_eq!(
error_message(envelope),
"This token does not have access to requested config 'prod'"
);
let two = r#"{"messages":["first thing","second thing"],"success":false}"#;
assert_eq!(error_message(two), "first thing; second thing");
assert_eq!(error_message("upstream timeout"), "upstream timeout");
}
#[test]
fn http_errors_explain_the_status() {
let pinned_elsewhere = r#"{"messages":["This token does not have access to requested config 'prd'"],"success":false}"#;
let err = http_error(
"myapp",
StatusCode::BAD_REQUEST,
pinned_elsewhere,
"reading 'API_KEY'",
);
let err = err.to_string();
assert!(
err.contains("does not have access to requested config"),
"{err}"
);
assert!(err.contains("dp.st."), "{err}");
assert!(err.contains("myapp"), "{err}");
let err = http_error(
"myapp",
StatusCode::UNAUTHORIZED,
r#"{"messages":["Invalid Auth"]}"#,
"reading",
)
.to_string();
assert!(err.contains(DOPPLER_TOKEN_ENV), "{err}");
let err = http_error(
"myapp",
StatusCode::NOT_FOUND,
r#"{"messages":["Could not find requested config 'nope'"]}"#,
"reading",
)
.to_string();
assert!(err.contains("must already exist"), "{err}");
}
#[test]
fn an_absent_config_is_an_error_for_get_and_nothing_for_delete() {
let loc = location("myapp", "nope", "API_KEY");
let body = r#"{"messages":["Could not find requested config 'nope'"],"success":false}"#;
let err = interpret_read(&loc, StatusCode::NOT_FOUND, body, AbsentConfig::IsAnError)
.expect_err("an absent config must be reported to a read");
assert!(err.to_string().contains("must already exist"), "{err}");
let probed = interpret_read(
&loc,
StatusCode::NOT_FOUND,
body,
AbsentConfig::HoldsNothing,
)
.expect("delete probes an absent config as empty");
assert!(probed.is_none());
let refused = r#"{"messages":["This token does not have access to requested project 'myapp'"],"success":false}"#;
let err = interpret_read(
&loc,
StatusCode::BAD_REQUEST,
refused,
AbsentConfig::HoldsNothing,
)
.expect_err("a 400 is an error whichever policy reads it");
assert!(err.to_string().contains("myapp"), "{err}");
}
#[test]
fn interpretations_parse_200_and_report_the_rest() {
let loc = location("myapp", "prd", "MONGO_CONNECTION");
let value = interpret_read(
&loc,
StatusCode::OK,
&single_read("v", "v"),
AbsentConfig::IsAnError,
)
.unwrap()
.expect("a stored secret");
assert_eq!(value.expose_secret(), b"v");
let denied = r#"{"messages":["Invalid Auth"],"success":false}"#;
let err = interpret_listing(
"myapp",
"prd",
StatusCode::UNAUTHORIZED,
denied,
parse_config_secrets,
)
.expect_err("a failed listing is an error");
assert!(err.to_string().contains("listing config 'prd'"), "{err}");
let err = interpret_listing(
"myapp",
"prd",
StatusCode::UNAUTHORIZED,
denied,
parse_secret_names,
)
.expect_err("a failed names listing is an error");
assert!(err.to_string().contains("listing config 'prd'"), "{err}");
}
#[test]
fn a_missing_token_names_both_sources() {
if std::env::var(DOPPLER_TOKEN_ENV).is_ok() {
return;
}
let err = provider("doppler://myapp/prd").token().unwrap_err();
assert!(err.to_string().contains(DOPPLER_TOKEN_ENV), "{err}");
assert!(err.to_string().contains("dp.sa."), "{err}");
}
#[test]
fn a_supplied_credential_is_used_and_trimmed() {
let mut p = provider("doppler://myapp/prd");
let mut credentials = ProviderCredentials::new();
credentials.insert(
TOKEN.to_string(),
SecretBytes::from_utf8(" dp.sa.from-credential\n"),
);
p.with_credentials(credentials);
assert_eq!(p.token().unwrap().expose_secret(), b"dp.sa.from-credential");
}
#[test]
fn reflect_reads_the_config_resolution_would() {
assert_eq!(
provider("doppler://myapp")
.config_for_profile("production")
.unwrap(),
"production"
);
assert_eq!(
provider("doppler://myapp/prd")
.config_for_profile("production")
.unwrap(),
"prd"
);
let err = provider("doppler://myapp")
.config_for_profile("Production")
.unwrap_err();
assert!(err.to_string().contains("lowercase letters"), "{err}");
}
#[test]
fn every_call_names_its_coordinates() {
let loc = location("myapp", "prd", "API_KEY");
assert_eq!(
Call::Read(&loc).request().query,
[("project", "myapp"), ("config", "prd"), ("name", "API_KEY")]
);
assert_eq!(
Call::List {
project: "myapp",
config: "prd",
names: "API_KEY,DATABASE_URL",
}
.request()
.query,
[
("project", "myapp"),
("config", "prd"),
("include_managed_secrets", "false"),
("secrets", "API_KEY,DATABASE_URL"),
]
);
assert_eq!(
Call::List {
project: "myapp",
config: "prd",
names: "",
}
.request()
.query,
[
("project", "myapp"),
("config", "prd"),
("include_managed_secrets", "false"),
]
);
assert_eq!(
Call::Names {
project: "myapp",
config: "prd",
}
.request()
.query,
[
("project", "myapp"),
("config", "prd"),
("include_managed_secrets", "false"),
]
);
let write = Call::Write(&loc, Some("v"));
assert!(
write.request().query.is_empty(),
"a write's coordinates ride in its body"
);
let body = write.request().body.expect("a write has a body");
assert_eq!(
body.get("project").and_then(serde_json::Value::as_str),
Some("myapp")
);
assert_eq!(
body.get("config").and_then(serde_json::Value::as_str),
Some("prd")
);
assert_eq!(
body.get("secrets")
.and_then(|secrets| secrets.get("API_KEY"))
.and_then(serde_json::Value::as_str),
Some("v")
);
let body = Call::Write(&loc, None)
.request()
.body
.expect("a delete has a body");
assert!(
body.get("secrets")
.and_then(|secrets| secrets.get("API_KEY"))
.is_some_and(serde_json::Value::is_null)
);
}
#[test]
fn each_call_names_its_endpoint() {
let loc = location("myapp", "prd", "API_KEY");
let cases = [
(
Call::Read(&loc),
reqwest::Method::GET,
"/configs/config/secret",
),
(
Call::List {
project: "myapp",
config: "prd",
names: "",
},
reqwest::Method::GET,
"/configs/config/secrets",
),
(
Call::Names {
project: "myapp",
config: "prd",
},
reqwest::Method::GET,
"/configs/config/secrets/names",
),
(
Call::Write(&loc, Some("v")),
reqwest::Method::POST,
"/configs/config/secrets",
),
];
for (call, method, path) in &cases {
let request = call.request();
assert_eq!(request.method, *method, "{path}");
assert_eq!(request.path, *path);
}
}
struct RecordedRequest {
line: String,
headers: HashMap<String, String>,
body: String,
}
type FixtureResponse = (&'static str, String, Option<(&'static str, String)>);
fn response_server(
responses: Vec<FixtureResponse>,
) -> (SocketAddr, std::thread::JoinHandle<Vec<RecordedRequest>>) {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let endpoint = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
let mut recorded = Vec::new();
for (status, body, header) in responses {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(&mut stream);
let mut line = String::new();
reader.read_line(&mut line).unwrap();
let mut headers = HashMap::new();
loop {
let mut header = String::new();
reader.read_line(&mut header).unwrap();
if header == "\r\n" || header.is_empty() {
break;
}
if let Some((name, value)) = header.trim_end().split_once(':') {
headers.insert(name.to_ascii_lowercase(), value.trim().to_string());
}
}
let content_length = headers
.get("content-length")
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(0);
let mut request_body = vec![0; content_length];
reader.read_exact(&mut request_body).unwrap();
recorded.push(RecordedRequest {
line: line.trim_end().to_string(),
headers,
body: String::from_utf8(request_body).unwrap(),
});
let header = header
.map(|(name, value)| format!("{name}: {value}\r\n"))
.unwrap_or_default();
write!(
stream,
"HTTP/1.1 {status}\r\nContent-Type: application/json\r\n{header}Content-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
)
.unwrap();
}
recorded
});
(endpoint, server)
}
fn fixture_provider(spec: &str, endpoint: SocketAddr) -> DopplerProvider {
let mut p = provider(spec);
p.api_base = format!("http://{endpoint}");
p.allow_insecure_loopback = true;
let mut credentials = ProviderCredentials::new();
credentials.insert(TOKEN.to_string(), SecretBytes::from_utf8("dp.sa.test"));
p.with_credentials(credentials);
p
}
#[test]
fn a_list_names_its_coordinates_on_the_wire() {
let (endpoint, server) = response_server(vec![(
"200 OK",
r#"{"secrets":{},"success":true}"#.to_string(),
None,
)]);
let p = fixture_provider("doppler://myapp/prd", endpoint);
let requests = [
("API_KEY", Address::convention("unused", "prd", "API_KEY")),
("DB_URL", Address::convention("unused", "prd", "DB_URL")),
];
let read = p.get_many(&requests).unwrap();
assert!(read.is_empty());
let recorded = server.join().unwrap();
let [request] = recorded.as_slice() else {
panic!("expected one recorded request");
};
assert!(
request.line.starts_with("GET /configs/config/secrets?"),
"{}",
request.line
);
assert!(request.line.contains("project=myapp"), "{}", request.line);
assert!(request.line.contains("config=prd"), "{}", request.line);
assert!(
request.line.contains("secrets=API_KEY%2CDB_URL")
|| request.line.contains("secrets=DB_URL%2CAPI_KEY"),
"{}",
request.line
);
assert_eq!(
request.headers.get("authorization").map(String::as_str),
Some("Bearer dp.sa.test")
);
}
#[test]
fn a_write_puts_the_value_in_the_body() {
let (endpoint, server) =
response_server(vec![("200 OK", r#"{"success":true}"#.to_string(), None)]);
let p = fixture_provider("doppler://myapp/prd", endpoint);
p.set(
Address::convention("unused", "prd", "API_KEY"),
&SecretBytes::from_utf8("v1"),
)
.unwrap();
let recorded = server.join().unwrap();
let [request] = recorded.as_slice() else {
panic!("expected one recorded request");
};
assert_eq!(request.line, "POST /configs/config/secrets HTTP/1.1");
assert_eq!(
request.headers.get("authorization").map(String::as_str),
Some("Bearer dp.sa.test")
);
let body: serde_json::Value = serde_json::from_str(&request.body).unwrap();
assert_eq!(
body.get("project").and_then(serde_json::Value::as_str),
Some("myapp")
);
assert_eq!(
body.get("config").and_then(serde_json::Value::as_str),
Some("prd")
);
assert_eq!(
body.get("secrets")
.and_then(|secrets| secrets.get("API_KEY"))
.and_then(serde_json::Value::as_str),
Some("v1")
);
}
#[test]
fn a_redirect_is_not_followed() {
let elsewhere = TcpListener::bind("127.0.0.1:0").unwrap();
elsewhere.set_nonblocking(true).unwrap();
let target = format!(
"http://{}/configs/config/secrets",
elsewhere.local_addr().unwrap()
);
let (endpoint, server) = response_server(vec![(
"307 Temporary Redirect",
String::new(),
Some(("Location", target)),
)]);
let p = fixture_provider("doppler://myapp/prd", endpoint);
let err = p
.set(
Address::convention("unused", "prd", "API_KEY"),
&SecretBytes::from_utf8("v1"),
)
.expect_err("a redirect must surface as an error");
assert!(err.to_string().contains("HTTP 307"), "{err}");
assert_eq!(server.join().unwrap().len(), 1);
assert_eq!(
elsewhere.accept().map(|_| ()).unwrap_err().kind(),
std::io::ErrorKind::WouldBlock,
"the redirect target must never see the request"
);
}
#[test]
fn a_failed_write_reports_dopplers_message() {
let (endpoint, server) = response_server(vec![(
"400 Bad Request",
r#"{"messages":["This token does not have access to requested config 'prd'"],"success":false}"#.to_string(),
None,
)]);
let p = fixture_provider("doppler://myapp/prd", endpoint);
let err = p
.set(
Address::convention("unused", "prd", "API_KEY"),
&SecretBytes::from_utf8("v1"),
)
.expect_err("a 400 must be an error")
.to_string();
assert!(err.contains("(400)"), "{err}");
assert!(
err.contains("does not have access to requested config 'prd'"),
"{err}"
);
assert!(err.contains("writing 'API_KEY'"), "{err}");
server.join().unwrap();
}
#[test]
fn a_batch_read_maps_values_back_onto_their_request_names() {
let listing = serde_json::json!({
"secrets": {
"API_KEY": { "raw": "k", "computed": "k" },
"DB_URL": { "raw": "u", "computed": "u" },
},
"success": true,
})
.to_string();
let (endpoint, server) = response_server(vec![("200 OK", listing.clone(), None)]);
let p = fixture_provider("doppler://myapp/prd", endpoint);
let shared = NativeAddress {
item: "prd/API_KEY".into(),
..Default::default()
};
let requests = [
("API_KEY", Address::convention("unused", "prd", "API_KEY")),
("DB_URL", Address::convention("unused", "prd", "DB_URL")),
("ABSENT", Address::convention("unused", "prd", "ABSENT")),
("ALIAS", Address::Native(&shared)),
];
let read = p.get_many(&requests).unwrap();
assert_eq!(
read.get("API_KEY").map(SecretBytes::expose_secret),
Some(b"k".as_slice())
);
assert_eq!(
read.get("DB_URL").map(SecretBytes::expose_secret),
Some(b"u".as_slice())
);
assert_eq!(
read.get("ALIAS").map(SecretBytes::expose_secret),
Some(b"k".as_slice())
);
assert!(!read.contains_key("ABSENT"), "an absent name is omitted");
assert_eq!(read.len(), 3);
let recorded = server.join().unwrap();
let [request] = recorded.as_slice() else {
panic!("expected one recorded request");
};
let line = &request.line;
assert_eq!(line.matches("API_KEY").count(), 1, "{line}");
}
#[test]
fn a_batch_read_never_asks_doppler_for_a_reserved_name() {
let (endpoint, server) = response_server(vec![(
"200 OK",
r#"{"secrets":{"API_KEY":{"raw":"k","computed":"k"}},"success":true}"#.to_string(),
None,
)]);
let p = fixture_provider("doppler://myapp/prd", endpoint);
let requests = [
("API_KEY", Address::convention("unused", "prd", "API_KEY")),
(
"DOPPLER_PROJECT",
Address::convention("unused", "prd", "DOPPLER_PROJECT"),
),
];
let read = p.get_many(&requests).unwrap();
assert_eq!(
read.get("API_KEY").map(SecretBytes::expose_secret),
Some(b"k".as_slice())
);
assert!(!read.contains_key("DOPPLER_PROJECT"));
let recorded = server.join().unwrap();
let [request] = recorded.as_slice() else {
panic!("expected one recorded request");
};
assert!(
!request.line.contains("DOPPLER_PROJECT"),
"a reserved name must not be asked for: {}",
request.line
);
let (endpoint, server) = response_server(vec![]);
let p = fixture_provider("doppler://myapp/prd", endpoint);
let requests = [(
"DOPPLER_CONFIG",
Address::convention("unused", "prd", "DOPPLER_CONFIG"),
)];
assert!(p.get_many(&requests).unwrap().is_empty());
assert!(server.join().unwrap().is_empty(), "no request was sent");
}
#[test]
fn a_delete_probes_before_it_writes() {
let absent = serde_json::json!({
"name": "API_KEY",
"value": { "raw": null, "computed": null,
"rawVisibility": null, "computedVisibility": null },
"success": true,
})
.to_string();
let (endpoint, server) = response_server(vec![("200 OK", absent.clone(), None)]);
let p = fixture_provider("doppler://myapp/prd", endpoint);
assert!(
!p.delete(Address::convention("unused", "prd", "API_KEY"))
.unwrap(),
"nothing was there to delete"
);
let recorded = server.join().unwrap();
let [probe_request] = recorded.as_slice() else {
panic!("expected one recorded request");
};
assert!(
probe_request
.line
.starts_with("GET /configs/config/secret?")
);
let (endpoint, server) = response_server(vec![
("200 OK", single_read("k", "k").clone(), None),
("200 OK", r#"{"success":true}"#.to_string(), None),
]);
let p = fixture_provider("doppler://myapp/prd", endpoint);
assert!(
p.delete(Address::convention("unused", "prd", "API_KEY"))
.unwrap(),
"a stored secret was invalidated"
);
let recorded = server.join().unwrap();
let [_probe_request, delete_request] = recorded.as_slice() else {
panic!("expected two recorded requests");
};
assert_eq!(delete_request.line, "POST /configs/config/secrets HTTP/1.1");
let body: serde_json::Value = serde_json::from_str(&delete_request.body).unwrap();
assert!(
body.get("secrets")
.and_then(|secrets| secrets.get("API_KEY"))
.is_some_and(serde_json::Value::is_null)
);
}
#[test]
fn a_delete_refuses_a_secret_it_cannot_read() {
let (endpoint, server) = response_server(vec![("200 OK", restricted_read().clone(), None)]);
let p = fixture_provider("doppler://myapp/prd", endpoint);
let err = p
.delete(Address::convention("unused", "prd", "MONGO_CONNECTION"))
.expect_err("an unreadable secret is not deleted")
.to_string();
assert!(err.contains("withheld"), "{err}");
assert!(err.contains("MONGO_CONNECTION"), "{err}");
assert_eq!(server.join().unwrap().len(), 1, "the probe, and no write");
}
#[test]
fn an_unrecognized_failure_body_is_bounded() {
let page = format!("<html>{}</html>", "x".repeat(64 * 1024));
let message = error_message(&page);
assert!(
message.len() < MAX_ERROR_BODY_BYTES + 64,
"{} bytes",
message.len()
);
assert!(message.starts_with("<html>xxx"), "{message}");
assert!(message.ends_with("... (truncated)"), "{message}");
assert_eq!(error_message("upstream timeout"), "upstream timeout");
}
#[test]
fn a_secret_object_missing_its_computed_value_is_an_error() {
for value in [
serde_json::json!({ "raw": "s3cret_DO_NOT_ECHO", "computed": null }),
serde_json::json!({ "raw": "s3cret_DO_NOT_ECHO", "secretValue": "s3cret" }),
] {
let body = serde_json::json!({ "value": value, "success": true }).to_string();
let err = parse_secret_value(&body, "MONGO_CONNECTION")
.expect_err("an unrecognized secret object must be reported")
.to_string();
assert!(err.contains("MONGO_CONNECTION"), "{err}");
assert!(
!err.contains("DO_NOT_ECHO"),
"the value must not leak: {err}"
);
}
}
#[test]
fn a_large_filter_is_split_across_requests() {
let names: Vec<String> = (0..500)
.map(|n| format!("MONOSECRET_NAME_{n:04}"))
.collect();
let chunks = filter_chunks(&names);
assert!(chunks.len() > 1, "500 names exceed one filter's budget");
for chunk in &chunks {
assert!(chunk.len() <= MAX_FILTER_BYTES, "{} bytes", chunk.len());
}
let rejoined: Vec<&str> = chunks.iter().flat_map(|c| c.split(',')).collect();
assert_eq!(
rejoined,
names.iter().map(String::as_str).collect::<Vec<_>>()
);
assert_eq!(filter_chunks(&[]), [""]);
}
#[test]
fn retry_policy_follows_dopplers_rate_limit_contract() {
let limited = StatusCode::TOO_MANY_REQUESTS;
assert_eq!(
retry_delay(limited, Some("2"), 1),
Some(Duration::from_secs(2))
);
assert_eq!(
retry_delay(limited, None, 1),
Some(Duration::from_millis(250)),
"no usable header falls back to a backoff"
);
assert_eq!(
retry_delay(limited, Some("soon"), 2),
Some(Duration::from_millis(500)),
"an unparseable header is a missing one, and the backoff grows"
);
assert_eq!(
retry_delay(limited, Some("60"), 1),
Some(MAX_RETRY_DELAY),
"a wait past the cap is shortened to it rather than given up on"
);
assert_eq!(
retry_delay(limited, Some("60"), RETRY_ATTEMPTS),
None,
"a long wait never extends the attempt budget"
);
assert_eq!(
retry_delay(limited, Some("0"), RETRY_ATTEMPTS),
None,
"attempts run out"
);
assert_eq!(
retry_delay(StatusCode::SERVICE_UNAVAILABLE, None, 1),
Some(Duration::from_millis(250))
);
for final_word in [
StatusCode::BAD_REQUEST,
StatusCode::UNAUTHORIZED,
StatusCode::NOT_FOUND,
StatusCode::TEMPORARY_REDIRECT,
] {
assert_eq!(retry_delay(final_word, Some("1"), 1), None, "{final_word}");
}
}
#[test]
fn http_client_bounds_request_time() {
crate::provider::http::assert_bounded(provider("doppler://myapp").http().unwrap());
}
#[test]
fn a_rate_limited_read_is_retried_after_the_suggested_wait() {
let (endpoint, server) = response_server(vec![
(
"429 Too Many Requests",
r#"{"messages":["Too many requests"],"success":false}"#.to_string(),
Some(("Retry-After", "7".to_string())),
),
("200 OK", single_read("k", "k").clone(), None),
]);
let p = fixture_provider("doppler://myapp/prd", endpoint);
let value = p
.get(Address::convention("unused", "prd", "API_KEY"))
.unwrap()
.expect("the retry read the value");
assert_eq!(value.expose_secret(), b"k");
assert!(
RETRY_PAUSES
.lock()
.unwrap()
.contains(&Duration::from_secs(7)),
"the suggested wait was honored"
);
let recorded = server.join().unwrap();
let [first_request, second_request] = recorded.as_slice() else {
panic!("expected two recorded requests");
};
assert_eq!(
first_request.line, second_request.line,
"the same request again"
);
}
#[test]
fn a_poisoned_profile_lock_keeps_the_profile() {
let p = provider("doppler://myapp");
p.set_profile("prd");
let poisoning = std::thread::scope(|scope| {
scope
.spawn(|| {
let _held = p.profile.lock().unwrap();
panic!("poison the profile lock");
})
.join()
});
assert!(poisoning.is_err(), "the lock was poisoned");
assert!(p.profile.lock().is_err(), "the lock is poisoned");
assert_eq!(p.session_profile().as_deref(), Some("prd"));
p.set_profile("stg");
assert_eq!(
p.session_profile().as_deref(),
Some("stg"),
"a poisoned lock still records a new profile"
);
}
#[test]
fn a_persistent_server_error_is_reported_after_the_last_attempt() {
let outage = || {
(
"503 Service Unavailable",
r#"{"messages":["Doppler is briefly unavailable"],"success":false}"#.to_string(),
None,
)
};
let (endpoint, server) = response_server(vec![outage(), outage(), outage()]);
let p = fixture_provider("doppler://myapp/prd", endpoint);
let err = p
.set(
Address::convention("unused", "prd", "API_KEY"),
&SecretBytes::from_utf8("v1"),
)
.expect_err("three outages exhaust the attempts")
.to_string();
assert!(err.contains("HTTP 503"), "{err}");
assert!(err.contains("Doppler is briefly unavailable"), "{err}");
assert_eq!(
server.join().unwrap().len(),
RETRY_ATTEMPTS as usize,
"every attempt was made"
);
}
}