testing new features
This commit is contained in:
+13
-2
@@ -5,6 +5,8 @@ import pytest
|
||||
from cashflow_model import (
|
||||
Account,
|
||||
Asset,
|
||||
CurrencyConverter,
|
||||
ExchangeRate,
|
||||
FinancialModel,
|
||||
Liability,
|
||||
RecurringCashflow,
|
||||
@@ -16,9 +18,10 @@ from cashflow_model import (
|
||||
def sample_model() -> FinancialModel:
|
||||
acc_id = uuid4()
|
||||
return FinancialModel(
|
||||
base_currency="RUB",
|
||||
accounts=[
|
||||
Account(id=acc_id, name="Основной счёт", currency="USD", balance=5000.0),
|
||||
Account(name="Сбережения", currency="USD", balance=10000.0),
|
||||
Account(id=acc_id, name="Основной счёт", currency="RUB", balance=5000.0),
|
||||
Account(name="Сбережения", currency="RUB", balance=10000.0),
|
||||
],
|
||||
transactions=[
|
||||
Transaction(
|
||||
@@ -48,9 +51,17 @@ def sample_model() -> FinancialModel:
|
||||
liabilities=[
|
||||
Liability(name="Кредит", balance=20000.0, interest=5.0, payment=500.0),
|
||||
],
|
||||
exchange_rates=[
|
||||
ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def empty_model() -> FinancialModel:
|
||||
return FinancialModel()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_converter() -> CurrencyConverter:
|
||||
return CurrencyConverter([ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0)])
|
||||
|
||||
+144
-1
@@ -1,21 +1,35 @@
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cli.main import app
|
||||
from cli.main import MODEL_PATH, app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def _cleanup():
|
||||
if MODEL_PATH.exists():
|
||||
MODEL_PATH.unlink()
|
||||
|
||||
|
||||
class TestCli:
|
||||
def test_init(self):
|
||||
_cleanup()
|
||||
result = runner.invoke(app, ["init"])
|
||||
assert result.exit_code == 0
|
||||
assert "Пустая модель" in result.stdout
|
||||
|
||||
def test_forecast_after_init(self):
|
||||
_cleanup()
|
||||
runner.invoke(app, ["init"])
|
||||
result = runner.invoke(app, ["forecast", "--months", "3"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_forecast_with_currency(self):
|
||||
_cleanup()
|
||||
runner.invoke(app, ["init"])
|
||||
result = runner.invoke(app, ["forecast", "--months", "3", "--currency", "USD"])
|
||||
assert result.exit_code == 0
|
||||
assert "$" in result.stdout
|
||||
|
||||
def test_unknown_scenario(self):
|
||||
result = runner.invoke(app, ["scenario", "unknown"])
|
||||
assert result.exit_code != 0
|
||||
@@ -23,3 +37,132 @@ class TestCli:
|
||||
def test_help(self):
|
||||
result = runner.invoke(app, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_info_empty(self):
|
||||
_cleanup()
|
||||
runner.invoke(app, ["init"])
|
||||
result = runner.invoke(app, ["info"])
|
||||
assert result.exit_code == 0
|
||||
assert "Базовая валюта" in result.stdout
|
||||
|
||||
def test_config_base_currency_set(self):
|
||||
_cleanup()
|
||||
runner.invoke(app, ["init"])
|
||||
result = runner.invoke(app, ["config", "base-currency", "EUR"])
|
||||
assert result.exit_code == 0
|
||||
assert "EUR" in result.stdout
|
||||
|
||||
info = runner.invoke(app, ["info"])
|
||||
assert "EUR" in info.stdout
|
||||
|
||||
def test_config_base_currency_show(self):
|
||||
_cleanup()
|
||||
runner.invoke(app, ["init"])
|
||||
result = runner.invoke(app, ["config", "base-currency"])
|
||||
assert result.exit_code == 0
|
||||
assert "RUB" in result.stdout
|
||||
|
||||
def test_config_account_crud(self):
|
||||
_cleanup()
|
||||
runner.invoke(app, ["init"])
|
||||
|
||||
args = ["config", "account-add", "--name", "Тестовый", "--balance", "1000"]
|
||||
add = runner.invoke(app, args)
|
||||
assert add.exit_code == 0
|
||||
assert "Тестовый" in add.stdout
|
||||
|
||||
lst = runner.invoke(app, ["config", "account-list"])
|
||||
assert "Тестовый" in lst.stdout
|
||||
|
||||
remove = runner.invoke(app, ["config", "account-remove", "Тестовый"])
|
||||
assert remove.exit_code == 0
|
||||
|
||||
lst2 = runner.invoke(app, ["config", "account-list"])
|
||||
assert "Тестовый" not in lst2.stdout
|
||||
|
||||
def test_config_transaction_crud(self):
|
||||
_cleanup()
|
||||
runner.invoke(app, ["init"])
|
||||
runner.invoke(app, ["config", "account-add", "--name", "Счёт", "--balance", "0"])
|
||||
|
||||
add = runner.invoke(app, [
|
||||
"config", "transaction-add",
|
||||
"--account", "Счёт",
|
||||
"--amount", "5000",
|
||||
"--category", "income",
|
||||
"--description", "Зарплата",
|
||||
])
|
||||
assert add.exit_code == 0
|
||||
assert "доход" in add.stdout
|
||||
|
||||
lst = runner.invoke(app, ["config", "transaction-list"])
|
||||
assert "Зарплата" in lst.stdout
|
||||
|
||||
def test_config_asset_crud(self):
|
||||
_cleanup()
|
||||
runner.invoke(app, ["init"])
|
||||
|
||||
add = runner.invoke(app, [
|
||||
"config", "asset-add",
|
||||
"--name", "Квартира",
|
||||
"--value", "5000000",
|
||||
"--growth", "5",
|
||||
])
|
||||
assert add.exit_code == 0
|
||||
assert "Квартира" in add.stdout
|
||||
|
||||
lst = runner.invoke(app, ["config", "asset-list"])
|
||||
assert "Квартира" in lst.stdout
|
||||
|
||||
def test_config_liability_crud(self):
|
||||
_cleanup()
|
||||
runner.invoke(app, ["init"])
|
||||
|
||||
add = runner.invoke(app, [
|
||||
"config", "liability-add",
|
||||
"--name", "Кредит",
|
||||
"--balance", "100000",
|
||||
"--interest", "10",
|
||||
"--payment", "5000",
|
||||
])
|
||||
assert add.exit_code == 0
|
||||
assert "Кредит" in add.stdout
|
||||
|
||||
def test_config_recurring_crud(self):
|
||||
_cleanup()
|
||||
runner.invoke(app, ["init"])
|
||||
|
||||
add = runner.invoke(app, [
|
||||
"config", "recurring-add",
|
||||
"--amount", "-500",
|
||||
"--category", "аренда",
|
||||
"--frequency", "monthly",
|
||||
])
|
||||
assert add.exit_code == 0
|
||||
|
||||
lst = runner.invoke(app, ["config", "recurring-list"])
|
||||
assert "аренда" in lst.stdout
|
||||
|
||||
def test_config_rate_crud(self):
|
||||
_cleanup()
|
||||
runner.invoke(app, ["init"])
|
||||
|
||||
set_r = runner.invoke(app, ["config", "rate-set", "EUR", "RUB", "90"])
|
||||
assert set_r.exit_code == 0
|
||||
|
||||
lst = runner.invoke(app, ["config", "rate-list"])
|
||||
assert "EUR" in lst.stdout
|
||||
assert "90" in lst.stdout
|
||||
|
||||
def test_info_with_data(self):
|
||||
_cleanup()
|
||||
runner.invoke(app, ["init"])
|
||||
acc_args = ["config", "account-add", "--name", "Основной", "--balance", "50000"]
|
||||
runner.invoke(app, acc_args)
|
||||
asset_args = ["config", "asset-add", "--name", "Акции",
|
||||
"--value", "100000", "--growth", "8.0"]
|
||||
runner.invoke(app, asset_args)
|
||||
|
||||
result = runner.invoke(app, ["info"])
|
||||
assert "Основной" in result.stdout
|
||||
assert "Акции" in result.stdout
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import pytest
|
||||
|
||||
from cashflow_model import CurrencyConverter, CurrencyError, ExchangeRate
|
||||
|
||||
|
||||
class TestExchangeRate:
|
||||
def test_to_dict_roundtrip(self):
|
||||
rate = ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0)
|
||||
d = rate.to_dict()
|
||||
r2 = ExchangeRate.from_dict(d)
|
||||
assert r2.from_currency == "USD"
|
||||
assert r2.to_currency == "RUB"
|
||||
assert r2.rate == 80.0
|
||||
|
||||
def test_defaults(self):
|
||||
r = ExchangeRate()
|
||||
assert r.from_currency == "USD"
|
||||
assert r.to_currency == "RUB"
|
||||
assert r.rate == 80.0
|
||||
|
||||
|
||||
class TestCurrencyConverter:
|
||||
def test_convert_usd_to_rub(self, sample_converter):
|
||||
result = sample_converter.convert(100, "USD", "RUB")
|
||||
assert result == 8000.0
|
||||
|
||||
def test_convert_rub_to_usd(self, sample_converter):
|
||||
result = sample_converter.convert(8000, "RUB", "USD")
|
||||
assert result == 100.0
|
||||
|
||||
def test_same_currency(self, sample_converter):
|
||||
result = sample_converter.convert(500, "USD", "USD")
|
||||
assert result == 500.0
|
||||
|
||||
def test_unknown_pair(self):
|
||||
converter = CurrencyConverter()
|
||||
with pytest.raises(CurrencyError):
|
||||
converter.convert(100, "USD", "RUB")
|
||||
|
||||
def test_negative_rate(self):
|
||||
converter = CurrencyConverter()
|
||||
with pytest.raises(CurrencyError):
|
||||
converter.set_rate("USD", "RUB", -1)
|
||||
|
||||
def test_zero_rate(self):
|
||||
converter = CurrencyConverter()
|
||||
with pytest.raises(CurrencyError):
|
||||
converter.set_rate("USD", "RUB", 0)
|
||||
|
||||
def test_inverse_auto(self):
|
||||
rate = ExchangeRate(from_currency="EUR", to_currency="RUB", rate=90.0)
|
||||
converter = CurrencyConverter([rate])
|
||||
assert converter.convert(900, "RUB", "EUR") == 10.0
|
||||
|
||||
def test_get_symbol(self, sample_converter):
|
||||
assert sample_converter.get_symbol("USD") == "$"
|
||||
assert sample_converter.get_symbol("RUB") == "₽"
|
||||
assert sample_converter.get_symbol("XYZ") == "XYZ"
|
||||
|
||||
def test_with_defaults(self):
|
||||
converter = CurrencyConverter.with_defaults()
|
||||
assert converter.convert(10, "USD", "RUB") == 800.0
|
||||
|
||||
def test_multiple_rates(self):
|
||||
rates = [
|
||||
ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0),
|
||||
ExchangeRate(from_currency="EUR", to_currency="RUB", rate=90.0),
|
||||
]
|
||||
converter = CurrencyConverter(rates)
|
||||
assert converter.convert(10, "USD", "RUB") == 800.0
|
||||
assert converter.convert(10, "EUR", "RUB") == 900.0
|
||||
assert converter.convert(900, "RUB", "EUR") == 10.0
|
||||
@@ -22,6 +22,16 @@ class TestExcelSync:
|
||||
assert len(loaded.recurring) == len(sample_model.recurring)
|
||||
assert len(loaded.assets) == len(sample_model.assets)
|
||||
assert len(loaded.liabilities) == len(sample_model.liabilities)
|
||||
assert len(loaded.exchange_rates) == len(sample_model.exchange_rates)
|
||||
|
||||
def test_roundtrip_preserves_base_currency(self, tmp_path: Path):
|
||||
sync = ExcelSync()
|
||||
model = FinancialModel(base_currency="EUR")
|
||||
p = tmp_path / "eur_model.xlsx"
|
||||
sync.export_model(model, p)
|
||||
|
||||
loaded = sync.import_model(p)
|
||||
assert loaded.base_currency == "EUR"
|
||||
|
||||
def test_import_missing_file(self):
|
||||
sync = ExcelSync()
|
||||
|
||||
@@ -3,6 +3,7 @@ from pathlib import Path
|
||||
from cashflow_model import (
|
||||
Account,
|
||||
Asset,
|
||||
ExchangeRate,
|
||||
FinancialModel,
|
||||
ForecastScenario,
|
||||
Liability,
|
||||
@@ -42,6 +43,32 @@ class TestTransaction:
|
||||
|
||||
|
||||
class TestFinancialModel:
|
||||
def test_default_base_currency(self):
|
||||
model = FinancialModel()
|
||||
assert model.base_currency == "RUB"
|
||||
|
||||
def test_base_currency_roundtrip(self, tmp_path: Path):
|
||||
model = FinancialModel(base_currency="EUR")
|
||||
p = tmp_path / "model.json"
|
||||
model.save(p)
|
||||
loaded = FinancialModel.load(p)
|
||||
assert loaded.base_currency == "EUR"
|
||||
|
||||
def test_base_currency_backward_compat(self, tmp_path: Path):
|
||||
import json
|
||||
p = tmp_path / "legacy.json"
|
||||
with open(p, "w") as f:
|
||||
json.dump({"accounts": []}, f)
|
||||
loaded = FinancialModel.load(p)
|
||||
assert loaded.base_currency == "RUB"
|
||||
|
||||
def test_exchange_rates_default(self):
|
||||
model = FinancialModel()
|
||||
assert len(model.exchange_rates) == 1
|
||||
assert model.exchange_rates[0].from_currency == "USD"
|
||||
assert model.exchange_rates[0].to_currency == "RUB"
|
||||
assert model.exchange_rates[0].rate == 80.0
|
||||
|
||||
def test_save_load(self, tmp_path: Path):
|
||||
model = FinancialModel()
|
||||
model.accounts.append(Account(name="Test", balance=100.0))
|
||||
@@ -61,6 +88,8 @@ class TestFinancialModel:
|
||||
d = model.to_dict()
|
||||
assert d["accounts"] == []
|
||||
assert d["transactions"] == []
|
||||
assert d["base_currency"] == "RUB"
|
||||
assert "exchange_rates" in d
|
||||
|
||||
def test_all_entities_roundtrip(self, tmp_path: Path):
|
||||
model = FinancialModel(
|
||||
@@ -70,6 +99,7 @@ class TestFinancialModel:
|
||||
assets=[Asset(name="Stock", value=1000.0)],
|
||||
liabilities=[Liability(name="Loan", balance=500.0, interest=5.0, payment=100.0)],
|
||||
scenarios=[ForecastScenario(name="test")],
|
||||
exchange_rates=[ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0)],
|
||||
)
|
||||
p = tmp_path / "full.json"
|
||||
model.save(p)
|
||||
@@ -80,3 +110,4 @@ class TestFinancialModel:
|
||||
assert len(loaded.assets) == 1
|
||||
assert len(loaded.liabilities) == 1
|
||||
assert len(loaded.scenarios) == 1
|
||||
assert len(loaded.exchange_rates) == 1
|
||||
|
||||
Reference in New Issue
Block a user