Skip to main content

btctax_cli/cmd/
init.rs

1//! `init` — create the encrypted vault (`Vault::create`), initialize the core event schema + CLI
2//! config table (via `Session::create`), and FORCE the §8 key-backup step. The key-backup path is a
3//! required argument: a vault with no backed-up key is unrecoverable, so `init` never skips it.
4use crate::{CliError, Session};
5use btctax_store::Passphrase;
6use std::path::Path;
7
8/// Create the vault and force a key backup. Delegates to `run_with_repair(.., false)`.
9/// The ~24 existing callers (tests + main.rs) use this 3-arg form and are UNCHANGED (R0-I1).
10pub fn run(vault_path: &Path, pp: &Passphrase, key_backup_path: &Path) -> Result<(), CliError> {
11    run_with_repair(vault_path, pp, key_backup_path, false)
12}
13
14/// Like `run`, but when `repair` is true delegates to `Session::repair` to clear a half-created
15/// vault (orphan key + no pgp/bak) before initializing fresh. Called by `main.rs` `init --repair`.
16pub fn run_with_repair(
17    vault_path: &Path,
18    pp: &Passphrase,
19    key_backup_path: &Path,
20    repair: bool,
21) -> Result<(), CliError> {
22    let session = if repair {
23        Session::repair(vault_path, pp)?
24    } else {
25        Session::create(vault_path, pp)?
26    };
27    // §8 key lifecycle: a forced backup of the passphrase-protected key (HIGH-security write, owner-only).
28    session.vault().backup_key(key_backup_path)?;
29    Ok(())
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35    use btctax_store::Passphrase;
36
37    #[test]
38    fn init_creates_vault_key_and_forced_backup() {
39        let dir = tempfile::tempdir().unwrap();
40        let vault = dir.path().join("vault.pgp");
41        let backup = dir.path().join("backup/key.asc");
42        run(&vault, &Passphrase::new("pw".into()), &backup).unwrap();
43        assert!(vault.exists(), "vault.pgp written");
44        assert!(dir.path().join("vault.key").exists(), "sidecar key written");
45        assert!(backup.exists(), "forced key backup written");
46    }
47
48    #[test]
49    fn init_refuses_to_clobber_an_existing_vault() {
50        let dir = tempfile::tempdir().unwrap();
51        let vault = dir.path().join("vault.pgp");
52        let backup = dir.path().join("k.asc");
53        run(&vault, &Passphrase::new("pw".into()), &backup).unwrap();
54        let err = run(&vault, &Passphrase::new("pw".into()), &backup).unwrap_err();
55        assert!(matches!(
56            err,
57            CliError::Store(btctax_store::StoreError::AlreadyExists)
58        ));
59    }
60
61    /// `init --repair` on a half-created vault (key present, pgp absent) succeeds;
62    /// `Session::open` round-trips afterwards.
63    #[test]
64    fn init_repair_recovers_a_half_created_vault() {
65        let dir = tempfile::tempdir().unwrap();
66        let vault = dir.path().join("vault.pgp");
67        let backup = dir.path().join("k.asc");
68        let pp = Passphrase::new("pw".into());
69        // Create a proper vault, then remove vault.pgp (and .bak if any) to leave half-created state.
70        run(&vault, &pp, &backup).unwrap();
71        std::fs::remove_file(&vault).unwrap();
72        let bak = btctax_store::paths::bak_of(&vault);
73        if bak.exists() {
74            std::fs::remove_file(&bak).unwrap();
75        }
76        // vault.key still present; vault.pgp absent → half-created
77        let backup2 = dir.path().join("k2.asc");
78        run_with_repair(&vault, &pp, &backup2, true).unwrap();
79        // Must be able to open after repair
80        crate::Session::open(&vault, &pp).unwrap();
81    }
82
83    /// The 3-arg `run` (repair=false) on a half-created vault returns `HalfCreatedVault`, not
84    /// `AlreadyExists`. This verifies the 3-arg wrapper is untouched (R0-I1).
85    #[test]
86    fn init_without_repair_on_half_created_errors() {
87        let dir = tempfile::tempdir().unwrap();
88        let vault = dir.path().join("vault.pgp");
89        let backup = dir.path().join("k.asc");
90        let pp = Passphrase::new("pw".into());
91        run(&vault, &pp, &backup).unwrap();
92        std::fs::remove_file(&vault).unwrap();
93        let bak = btctax_store::paths::bak_of(&vault);
94        if bak.exists() {
95            std::fs::remove_file(&bak).unwrap();
96        }
97        // 3-arg run → repair=false → HalfCreatedVault
98        let err = run(&vault, &pp, &backup).unwrap_err();
99        assert!(
100            matches!(
101                err,
102                CliError::Store(btctax_store::StoreError::HalfCreatedVault(_))
103            ),
104            "expected HalfCreatedVault, got: {err:?}"
105        );
106    }
107}