Skip to main content

ferro_cli/doctor/checks/
database_url_sqlite_in_prod.rs

1//! Database URL not sqlite in prod (SCOPE ยง12.8): if `.env.production` exists
2//! and its `DATABASE_URL` starts with `sqlite:`, hard-error.
3
4use crate::doctor::check::{CheckResult, DoctorCheck};
5use std::fs;
6use std::path::Path;
7
8pub struct DatabaseUrlSqliteInProdCheck;
9
10const NAME: &str = "database_url_sqlite_in_prod";
11
12impl DoctorCheck for DatabaseUrlSqliteInProdCheck {
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 path = root.join(".env.production");
23    if !path.is_file() {
24        return CheckResult::ok(NAME, "skipped (.env.production absent)");
25    }
26    let content = match fs::read_to_string(&path) {
27        Ok(s) => s,
28        Err(e) => return CheckResult::error(NAME, format!("failed to read .env.production: {e}")),
29    };
30    for raw in content.lines() {
31        let line = raw.trim();
32        if line.is_empty() || line.starts_with('#') {
33            continue;
34        }
35        let Some((k, v)) = line.split_once('=') else {
36            continue;
37        };
38        if k.trim() == "DATABASE_URL" {
39            let value = v.trim().trim_matches('"').trim_matches('\'');
40            if value.starts_with("sqlite:") {
41                return CheckResult::error(NAME, "DATABASE_URL in .env.production uses sqlite:")
42                    .with_details(
43                        "Production must use a network-accessible database (postgres, mysql)",
44                    );
45            }
46            return CheckResult::ok(NAME, "DATABASE_URL is non-sqlite");
47        }
48    }
49    CheckResult::warn(NAME, "DATABASE_URL not declared in .env.production")
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use tempfile::TempDir;
56
57    #[test]
58    fn name_is_database_url_sqlite_in_prod() {
59        assert_eq!(
60            DatabaseUrlSqliteInProdCheck.name(),
61            "database_url_sqlite_in_prod"
62        );
63    }
64
65    #[test]
66    fn skipped_when_env_production_absent() {
67        let tmp = TempDir::new().unwrap();
68        let r = check_impl(tmp.path());
69        assert_eq!(r.status, crate::doctor::check::CheckStatus::Ok);
70    }
71
72    #[test]
73    fn errors_on_sqlite_url() {
74        let tmp = TempDir::new().unwrap();
75        fs::write(
76            tmp.path().join(".env.production"),
77            "DATABASE_URL=sqlite:./db.sqlite\n",
78        )
79        .unwrap();
80        let r = check_impl(tmp.path());
81        assert_eq!(r.status, crate::doctor::check::CheckStatus::Error);
82    }
83
84    #[test]
85    fn ok_on_postgres_url() {
86        let tmp = TempDir::new().unwrap();
87        fs::write(
88            tmp.path().join(".env.production"),
89            "DATABASE_URL=postgres://u:p@h/db\n",
90        )
91        .unwrap();
92        let r = check_impl(tmp.path());
93        assert_eq!(r.status, crate::doctor::check::CheckStatus::Ok);
94    }
95}