holdon 0.2.1

Wait for anything. Know why if it doesn't.
Documentation
use std::time::Instant;

use mongodb::Client;
use mongodb::bson::doc;
use mongodb::options::ClientOptions;
use url::Url;

use super::hint::hints;
use super::{AttemptCtx, err_stage, install_rustls_provider_once, ok_stage};
use crate::diagnostic::{Stage, StageKind};
use crate::util::{format_error_chain, redact_in};

pub(super) async fn probe(url: &Url, ctx: AttemptCtx) -> Vec<Stage> {
    install_rustls_provider_once();
    let start = Instant::now();
    let pw = url.password().unwrap_or("").to_owned();
    let conn_str = url.as_str().to_owned();
    let stage = match ping(&conn_str, ctx).await {
        Ok(()) => ok_stage(StageKind::Mongodb, start.elapsed()),
        Err(e) => {
            let mut msg = format_error_chain(&e);
            if !pw.is_empty() {
                msg = redact_in(&msg, &conn_str);
                msg = redact_in(&msg, &pw);
            }
            let hint = hint_for(&msg);
            err_stage(StageKind::Mongodb, start.elapsed(), msg, Some(hint))
        }
    };
    vec![stage]
}

async fn ping(uri: &str, ctx: AttemptCtx) -> mongodb::error::Result<()> {
    let mut opts = ClientOptions::parse(uri).await?;
    opts.connect_timeout = Some(ctx.attempt_timeout);
    opts.server_selection_timeout = Some(ctx.attempt_timeout);
    let client = Client::with_options(opts)?;
    client
        .database("admin")
        .run_command(doc! { "ping": 1 })
        .await?;
    Ok(())
}

fn hint_for(msg: &str) -> &'static str {
    let lower = msg.to_ascii_lowercase();
    if lower.contains("authentication") || lower.contains("auth failed") {
        hints::MONGODB_AUTH
    } else if lower.contains("no primary")
        || lower.contains("replicasetnoprimary")
        || lower.contains("replica set")
    {
        hints::MONGODB_NO_PRIMARY
    } else if lower.contains("tls") || lower.contains("certificate") {
        hints::MONGODB_TLS
    } else {
        hints::MONGODB_NOT_READY
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
    use super::*;

    #[test]
    fn hint_for_classifies_auth_message() {
        assert_eq!(hint_for("Authentication failed"), hints::MONGODB_AUTH);
        assert_eq!(hint_for("auth failed: bad creds"), hints::MONGODB_AUTH);
    }

    #[test]
    fn hint_for_classifies_no_primary() {
        assert_eq!(hint_for("no primary available"), hints::MONGODB_NO_PRIMARY);
        assert_eq!(
            hint_for("replica set election in progress"),
            hints::MONGODB_NO_PRIMARY
        );
        assert_eq!(
            hint_for("Kind: ReplicaSetNoPrimary"),
            hints::MONGODB_NO_PRIMARY
        );
    }

    #[test]
    fn hint_for_routes_generic_selection_to_not_ready() {
        assert_eq!(
            hint_for("Server selection timeout: standalone unreachable"),
            hints::MONGODB_NOT_READY
        );
    }

    #[test]
    fn hint_for_classifies_tls() {
        assert_eq!(hint_for("TLS handshake failed"), hints::MONGODB_TLS);
        assert_eq!(hint_for("certificate verify failed"), hints::MONGODB_TLS);
    }

    #[test]
    fn hint_for_falls_back_to_not_ready() {
        assert_eq!(hint_for("connection refused"), hints::MONGODB_NOT_READY);
    }
}