""" Pruebas de los lugares y traslados (lugares.py y operaciones.py): un solo activo «Bitcoin» con sus sitios (exchanges, monederos, nodos Lightning). python3 -m unittest discover -s pruebas -v """ from __future__ import annotations import datetime as dt import sys import tempfile import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from cartera import lugares # noqa: E402 from cartera.datos import Datos, ErrorDatos, escribir, leer # noqa: E402 from cartera.operaciones import Cartera, ErrorOperacion # noqa: E402 from cartera.vista import resumen # noqa: E402 def btc(aid="C1", nombre="Bitcoin"): return {"id": aid, "nombre": nombre, "tipo": "Cripto", "codigo": "bitcoin", "unidad": "BTC", "fecha_inicio": "2020-01-01", "estado": "abierta"} class ConCartera(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) self.carpeta = Path(self.tmp.name) escribir(self.carpeta, Datos([btc(), btc("C2", "BTC en Phoenix")], [], [], [])) self.c = Cartera(self.carpeta) self.addCleanup(self.c.cerrar) self.cambium = self.c.guardar_lugar("C1", {"nombre": "Cambium", "clase": "exchange"})["id"] self.bolsillo = self.c.guardar_lugar("C1", {"nombre": "Bolsillo", "clase": "monedero"})["id"] def movs(self): return self.c.datos.vivos("movimientos") def total(self, hoy="2026-01-01"): r = resumen(self.c.datos, dt.date.fromisoformat(hoy)) return next(f for f in r["abiertos"] if f["id"] == "C1") class TestLugares(ConCartera): def test_un_traslado_mueve_de_lugar_sin_cambiar_la_cartera(self): self.c.guardar_movimiento({"activo": "C1", "tipo": "Compra", "fecha": "2023-01-01", "cantidad": 0.01, "importe": 200, "lugar": self.cambium}) antes = self.total() t = self.c.guardar_traslado({"activo": "C1", "fecha": "2023-01-02", "cantidad": 0.0099, "comision": 0.0001, "lugar": self.cambium, "lugar_destino": self.bolsillo}) self.assertEqual(lugares.saldos(self.movs(), "C1"), {self.bolsillo: 0.0099}) despues = self.total() self.assertAlmostEqual(despues["cantidad"], 0.0099) # solo baja la comisión self.assertAlmostEqual(despues["coste"], antes["coste"]) # el coste no se mueve com = next(m for m in self.movs() if m["tipo"] == "Comision") self.assertEqual((com["traslado"], com["lugar"]), (t["id"], self.cambium)) # editar la comisión la cambia; borrar el traslado se lleva su comisión self.c.guardar_traslado({"id": t["id"], "activo": "C1", "fecha": "2023-01-02", "cantidad": 0.0099, "comision": 0.0002, "lugar": self.cambium, "lugar_destino": self.bolsillo}) self.assertEqual([m["cantidad"] for m in self.movs() if m["tipo"] == "Comision"], [0.0002]) self.c.borrar_movimiento(t["id"]) self.assertEqual([m["tipo"] for m in self.movs()], ["Compra"]) self.assertEqual(lugares.saldos(self.movs(), "C1"), {self.cambium: 0.01}) def test_un_extremo_sin_saber_se_completa_despues(self): self.c.guardar_movimiento({"activo": "C1", "tipo": "Compra", "fecha": "2023-01-01", "cantidad": 0.01, "importe": 200, "lugar": self.cambium}) t = self.c.guardar_traslado({"activo": "C1", "fecha": "2023-01-02", "cantidad": 0.01, "lugar": self.cambium, "lugar_destino": ""}) self.assertEqual(lugares.saldos(self.movs(), "C1"), {"": 0.01}) self.c.completar_traslado(t["id"], lugar_destino=self.bolsillo, txid="ab" * 32) self.assertEqual(lugares.saldos(self.movs(), "C1"), {self.bolsillo: 0.01}) self.c.vaciar_extremo(t["id"], "lugar_destino") self.assertNotIn("txid", self._mov(t["id"])) with self.assertRaisesRegex(ErrorOperacion, "mismo"): self.c.completar_traslado(t["id"], lugar_destino=self.cambium) def _mov(self, mid): return next(m for m in self.movs() if m["id"] == mid) def test_comprobaciones(self): with self.assertRaisesRegex(ErrorOperacion, "distintos"): self.c.guardar_traslado({"activo": "C1", "fecha": "2023-01-02", "cantidad": 1, "lugar": self.cambium, "lugar_destino": self.cambium}) with self.assertRaisesRegex(ErrorOperacion, "no es de este activo"): self.c.guardar_movimiento({"activo": "C2", "tipo": "Compra", "fecha": "2023-01-01", "cantidad": 1, "importe": 1, "lugar": self.cambium}) with self.assertRaisesRegex(ErrorOperacion, "Ya hay"): self.c.guardar_lugar("C1", {"nombre": "cambium", "clase": "exchange"}) self.c.guardar_movimiento({"activo": "C1", "tipo": "Compra", "fecha": "2023-01-01", "cantidad": 0.01, "importe": 200, "lugar": self.cambium}) with self.assertRaisesRegex(ErrorOperacion, "operación"): self.c.borrar_lugar("C1", self.cambium) self.c.poner_lugar([self.movs()[0]["id"]], self.bolsillo) self.assertEqual(self.movs()[0]["lugar"], self.bolsillo) def test_se_guarda_y_se_vuelve_a_leer(self): self.c.guardar_traslado({"activo": "C1", "fecha": "2023-01-02", "cantidad": 0.01, "lugar": self.cambium, "lugar_destino": self.bolsillo}) leidos = leer(self.carpeta) self.assertEqual([x["nombre"] for x in leidos.activos[0]["lugares"]], ["Cambium", "Bolsillo"]) self.assertEqual(leidos.movimientos[0]["tipo"], "Traslado") with self.assertRaises(ErrorDatos): escribir(self.carpeta, Datos([{**btc(), "lugares": [{"id": "x", "nombre": "X", "clase": "rara"}]}])) def test_un_traspaso_puede_decir_de_que_lugar_sale_y_a_cual_llega(self): self.c.guardar_movimiento({"activo": "C1", "tipo": "Compra", "fecha": "2023-01-01", "cantidad": 0.02, "importe": 400, "lugar": self.cambium}) alla = self.c.guardar_lugar("C2", {"nombre": "Phoenix", "clase": "lightning"})["id"] sal, ent = self.c.traspaso({"origen": "C1", "destino": "C2", "fecha": "2023-01-05", "cantidad_salida": 0.01, "importe": 200, "lugar_origen": self.cambium, "lugar_destino": alla}) self.assertEqual((sal["lugar"], ent["lugar"]), (self.cambium, alla)) self.assertEqual(lugares.saldos(self.movs(), "C1"), {self.cambium: 0.01}) with self.assertRaises(ErrorOperacion): # un lugar que no es del origen self.c.traspaso({"origen": "C1", "destino": "C2", "fecha": "2023-01-06", "cantidad_salida": 0.001, "importe": 20, "lugar_origen": alla}) def test_convertir_un_activo_en_lugar(self): self.c.guardar_movimiento({"activo": "C1", "tipo": "Compra", "fecha": "2023-01-01", "cantidad": 0.02, "importe": 400, "lugar": self.cambium}) self.c.guardar_movimiento({"activo": "C2", "tipo": "Recompensa", "fecha": "2023-02-01", "cantidad": 0.0001}) self.c.traspaso({"origen": "C1", "destino": "C2", "fecha": "2023-01-05", "cantidad_salida": 0.01, "cantidad_entrada": 0.0098, "importe": 200}) antes = self.total()["cantidad"] + next(f for f in resumen(self.c.datos, dt.date(2026, 1, 1))["abiertos"] if f["id"] == "C2")["cantidad"] lugar = self.c.convertir_en_lugar("C2", "C1", "lightning") self.assertEqual((lugar["nombre"], lugar["clase"]), ("BTC en Phoenix", "lightning")) self.assertNotIn("C2", [a["id"] for a in self.c.datos.vivos("activos")]) tipos = sorted(m["tipo"] for m in self.movs()) self.assertEqual(tipos, ["Comision", "Compra", "Recompensa", "Traslado"]) self.assertAlmostEqual(self.total()["cantidad"], antes) # el traspaso viejo no decía de qué lugar salía: queda «sin lugar» para que lo pongas self.assertEqual(lugares.saldos(self.movs(), "C1"), {self.cambium: 0.02, lugar["id"]: 0.0099, "": -0.01}) class TestUnirLugares(ConCartera): def test_dos_lugares_que_son_el_mismo_sitio_se_juntan(self): viejo = self.c.guardar_lugar("C1", {"nombre": "BTC en Cambium", "clase": "exchange"})["id"] self.c.guardar_movimiento({"activo": "C1", "tipo": "Compra", "fecha": "2023-01-01", "cantidad": 0.02, "importe": 400, "lugar": viejo}) # Un traslado entre los dos (con comisión) y otro desde el viejo a Bolsillo self.c.guardar_traslado({"activo": "C1", "fecha": "2023-01-02", "cantidad": 0.005, "comision": 0.0001, "lugar": viejo, "lugar_destino": self.cambium}) self.c.guardar_traslado({"activo": "C1", "fecha": "2023-01-03", "cantidad": 0.01, "lugar": viejo, "lugar_destino": self.bolsillo}) total = self.total()["cantidad"] r = self.c.unir_lugares("C1", viejo, self.cambium) self.assertEqual(r, {"movidas": 4, "traslados_quitados": 1}) self.assertNotIn(viejo, lugares.por_id(self.c.activo("C1"))) self.assertEqual(lugares.saldos(self.movs(), "C1"), {self.cambium: 0.0099, self.bolsillo: 0.01}) self.assertAlmostEqual(self.total()["cantidad"], total) # la comisión sigue contando com = next(m for m in self.movs() if m["tipo"] == "Comision") self.assertNotIn("traslado", com) with self.assertRaisesRegex(ErrorOperacion, "ya no está"): self.c.unir_lugares("C1", viejo, self.cambium) def test_el_csv_reconoce_btc_en_binance_como_binance(self): viejo = self.c.guardar_lugar("C1", {"nombre": "BTC en Binance", "clase": "exchange"}) self.assertEqual(lugares.buscar_exchange(self.c.activo("C1"), "Binance"), viejo) self.assertIsNone(lugares.buscar_exchange(self.c.activo("C1"), "Bit2Me")) self.assertEqual(lugares.buscar_exchange(self.c.activo("C1"), "cambium")["id"], self.cambium)