use btctax_cli::cmd;
use btctax_core::event::EventPayload;
use btctax_core::persistence;
use btctax_core::project::FeeTreatment;
use btctax_core::LotMethod;
use btctax_store::Passphrase;
use std::path::Path;
fn pp() -> Passphrase {
Passphrase::new("pw".into())
}
fn run_config(vault: &Path, args: &[&str]) -> (i32, String) {
let bin = env!("CARGO_BIN_EXE_btctax");
let output = std::process::Command::new(bin)
.arg("--vault")
.arg(vault.to_str().expect("vault path is valid UTF-8"))
.arg("config")
.args(args)
.env("BTCTAX_PASSPHRASE", "pw")
.output()
.expect("btctax binary must execute successfully");
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let code = output
.status
.code()
.expect("btctax process must exit normally (not via signal)");
(code, stdout)
}
#[test]
fn config_binary_apply_all_both_flags_take_effect() {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let (code, stdout) = run_config(
&vault,
&["--set-fee-treatment", "b", "--set-pre2025-method", "lifo"],
);
assert_eq!(
code, 0,
"config --set-fee-treatment b --set-pre2025-method lifo must exit 0; stdout: {stdout}"
);
let cfg = cmd::admin::show_config(&vault, &pp()).unwrap();
assert_eq!(
cfg.fee_treatment,
FeeTreatment::TreatmentB,
"fee_treatment must be B after --set-fee-treatment b (not silently dropped by old dispatch)"
);
assert_eq!(
cfg.pre2025_method,
LotMethod::Lifo,
"pre2025_method must be Lifo after --set-pre2025-method lifo (not silently dropped)"
);
}
#[test]
fn config_binary_set_forward_method_and_fee_treatment_both_apply() {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let (code, stdout) = run_config(
&vault,
&["--set-forward-method", "hifo", "--set-fee-treatment", "b"],
);
assert_eq!(
code, 0,
"config --set-forward-method hifo --set-fee-treatment b must exit 0; stdout: {stdout}"
);
let s = btctax_cli::Session::open(&vault, &pp()).unwrap();
let events = persistence::load_all(s.conn()).unwrap();
let me = events.iter().find_map(|e| match &e.payload {
EventPayload::MethodElection(m) => Some(m.clone()),
_ => None,
});
assert!(
matches!(me, Some(ref m) if m.method == LotMethod::Hifo),
"a HIFO MethodElection must be persisted via the binary dispatch (--set-forward-method not dropped)"
);
let cfg = s.config().unwrap();
assert_eq!(
cfg.fee_treatment,
FeeTreatment::TreatmentB,
"fee_treatment must be B — co-passed --set-fee-treatment must not be silently dropped by an early return"
);
}
#[test]
fn config_binary_attest_without_set_pre2025_method_is_an_error() {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let (code, _) = run_config(&vault, &["--attest-pre2025-method"]);
assert_ne!(
code, 0,
"config --attest-pre2025-method without --set-pre2025-method must exit non-zero (Usage error)"
);
}
#[test]
fn config_set_forward_method_exchange_scoped() {
use btctax_core::event::{Acquire, BasisSource};
use btctax_core::identity::{Source, SourceRef, WalletId};
use btctax_core::persistence::append_import_batch;
use btctax_core::{EventId, LedgerEvent};
use time::macros::datetime;
use time::UtcOffset;
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let acquire = LedgerEvent {
id: EventId::import(Source::Coinbase, SourceRef::new("cb-buy-1")),
utc_timestamp: datetime!(2025-02-01 00:00:00 UTC),
original_tz: UtcOffset::UTC,
wallet: Some(WalletId::Exchange {
provider: "coinbase".into(),
account: "main".into(),
}),
payload: EventPayload::Acquire(Acquire {
sat: 100_000,
usd_cost: rust_decimal_macros::dec!(50.00),
fee_usd: rust_decimal_macros::dec!(0),
basis_source: BasisSource::ExchangeProvided,
}),
};
{
let mut s = btctax_cli::Session::open(&vault, &pp()).unwrap();
append_import_batch(s.conn(), &[acquire]).unwrap();
s.save().unwrap();
}
let (code, stdout) = run_config(
&vault,
&[
"--set-forward-method",
"hifo",
"--exchange",
"exchange:coinbase:main",
],
);
assert_eq!(
code, 0,
"scoped election on a known account must exit 0; stdout: {stdout}"
);
{
let s = btctax_cli::Session::open(&vault, &pp()).unwrap();
let events = persistence::load_all(s.conn()).unwrap();
let me = events
.iter()
.find_map(|e| match &e.payload {
EventPayload::MethodElection(m) => Some(m.clone()),
_ => None,
})
.expect("a MethodElection must be persisted");
assert_eq!(me.method, LotMethod::Hifo);
assert_eq!(
me.wallet,
Some(WalletId::Exchange {
provider: "coinbase".into(),
account: "main".into()
}),
"the per-account scope must ride in the MethodElection PAYLOAD"
);
}
let (code_unknown, _) = run_config(
&vault,
&[
"--set-forward-method",
"hifo",
"--exchange",
"exchange:bogus:acct",
],
);
assert_ne!(
code_unknown, 0,
"an unknown exchange account must be rejected (non-zero exit)"
);
let (code_self, _) = run_config(
&vault,
&["--set-forward-method", "hifo", "--exchange", "self:cold"],
);
assert_ne!(
code_self, 0,
"a self:LABEL scope must be rejected (only exchange accounts are electable)"
);
let (code_orphan, _) = run_config(&vault, &["--exchange", "exchange:coinbase:main"]);
assert_ne!(
code_orphan, 0,
"--exchange without --set-forward-method must exit non-zero"
);
let s = btctax_cli::Session::open(&vault, &pp()).unwrap();
let events = persistence::load_all(s.conn()).unwrap();
let n = events
.iter()
.filter(|e| matches!(e.payload, EventPayload::MethodElection(_)))
.count();
assert_eq!(
n, 1,
"rejected scoped-election attempts must append nothing"
);
}