Skip to main content

ferro_cli/doctor/checks/
db_connection.rs

1//! DB connection check (D-03): inspect `DATABASE_URL` and probe via the
2//! project's existing `db:status` subprocess (mirrors `commands::db_status`).
3
4use crate::doctor::check::{CheckResult, DoctorCheck};
5use crate::doctor::checks::run_cargo_subcommand;
6use std::path::Path;
7
8pub struct DbConnectionCheck;
9
10const NAME: &str = "db_connection";
11
12impl DoctorCheck for DbConnectionCheck {
13    fn name(&self) -> &'static str {
14        NAME
15    }
16    fn run(&self, root: &Path) -> CheckResult {
17        check_impl(root)
18    }
19}
20
21pub(crate) fn check_impl(root: &Path) -> CheckResult {
22    // Load .env if present so DATABASE_URL becomes visible.
23    let _ = dotenvy::from_path(root.join(".env"));
24
25    let url = match std::env::var("DATABASE_URL") {
26        Ok(v) if !v.is_empty() => v,
27        _ => {
28            return CheckResult::warn(NAME, "DATABASE_URL not set")
29                .with_details("Set DATABASE_URL in .env to enable connectivity checks");
30        }
31    };
32
33    // If the project has no migrations dir, we can't shell out to db:status.
34    if !root.join("src/migrations").exists() {
35        return CheckResult::warn(NAME, "no migrations crate to probe connectivity");
36    }
37
38    match run_cargo_subcommand(root, &["db:status"]) {
39        Ok(out) if out.status.success() => {
40            CheckResult::ok(NAME, format!("connected ({})", redact_url(&url)))
41        }
42        Ok(out) => {
43            let stderr = String::from_utf8_lossy(&out.stderr).to_string();
44            CheckResult::error(NAME, "db:status failed").with_details(stderr)
45        }
46        Err(e) => CheckResult::error(NAME, format!("could not invoke cargo: {e}")),
47    }
48}
49
50fn redact_url(url: &str) -> String {
51    // Strip credentials between `://` and `@`.
52    if let Some(scheme_end) = url.find("://") {
53        let rest = &url[scheme_end + 3..];
54        if let Some(at) = rest.find('@') {
55            return format!("{}://***@{}", &url[..scheme_end], &rest[at + 1..]);
56        }
57    }
58    url.to_string()
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn name_is_db_connection() {
67        assert_eq!(DbConnectionCheck.name(), "db_connection");
68    }
69
70    #[test]
71    fn redact_url_strips_credentials() {
72        assert_eq!(
73            redact_url("postgres://user:pass@localhost/db"),
74            "postgres://***@localhost/db"
75        );
76        assert_eq!(redact_url("sqlite::memory:"), "sqlite::memory:");
77    }
78}