ferro_cli/doctor/checks/
db_connection.rs1use 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 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 !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 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}