""" Pruebas de las acciones sobre varias operaciones a la vez y de los justificantes adjuntos (guardado, lectura, descarga por el servidor). python3 -m unittest discover -s pruebas -v """ from __future__ import annotations import base64 import hashlib import json import sys import tempfile import threading import unittest import urllib.error import urllib.request from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from cartera import adjuntos # noqa: E402 from cartera.datos import Datos, ErrorDatos, escribir, leer # noqa: E402 from cartera.operaciones import Cartera, ErrorOperacion # noqa: E402 PDF = b"%PDF-1.4 justificante de prueba\n" class Base(unittest.TestCase): def setUp(self): self._tmp = tempfile.TemporaryDirectory() self.addCleanup(self._tmp.cleanup) self.carpeta = Path(self._tmp.name) / "datos" escribir(self.carpeta, Datos([{"id": "F1", "nombre": "Fondo uno", "tipo": "Fondo"}, {"id": "F2", "nombre": "Fondo dos", "tipo": "Fondo"}, {"id": "D1", "nombre": "Hipoteca", "tipo": "Deuda", "fecha_inicio": "2025-01-15", "deuda": {"capital": 1000.0, "tin": 2.0, "cuotas": 12, "primera_cuota": "2025-02-01"}}], [], [], [])) self.c = Cartera(self.carpeta) self.addCleanup(self.c.cerrar) def op(self, tipo, fecha, cantidad, precio=10, activo="F1", **kw): return self.c.guardar_movimiento({"activo": activo, "tipo": tipo, "fecha": fecha, "cantidad": cantidad, "precio": precio, **kw})["id"] def en_disco(self): return {m["id"]: m for m in leer(self.carpeta).vivos("movimientos")} class TestVariasALaVez(Base): def test_borrar_varias(self): a, b, c = self.op("Compra", "2025-01-01", 1), self.op("Compra", "2025-02-01", 1), self.op("Compra", "2025-03-01", 1) r = self.c.borrar_movimientos([a, c, a]) self.assertEqual((r["borradas"], sorted(r["ids"])), (2, sorted([a, c]))) # repetidas cuentan una vez self.assertEqual(set(self.en_disco()), {b}) def test_si_una_no_existe_no_se_toca_ninguna(self): a = self.op("Compra", "2025-01-01", 1) for mal in ([a, "no-existe"], [], None, "texto"): with self.subTest(mal=mal), self.assertRaises(ErrorOperacion): self.c.borrar_movimientos(mal) self.assertIn(a, self.en_disco()) def test_confirmar_solo_las_estimadas_y_avisa_de_pendientes(self): est = self.op("Compra", "2025-01-01", 1, estimado=True) normal = self.op("Compra", "2025-02-01", 1) pend = self.op("Compra", "2025-03-01", 1, estimado=True) self.c._movimiento(pend)["pendiente"] = True r = self.c.confirmar_movimientos([est, normal, pend]) self.assertEqual(r, {"confirmadas": 1, "pendientes": 1}) self.assertFalse(self.en_disco()[est]["estimado"]) def test_mover_de_activo_en_orden(self): """Una venta se mueve después de la compra del mismo día aunque venga antes en la lista.""" compra = self.op("Compra", "2025-01-01", 5) venta = self.op("Venta", "2025-01-01", 3) r = self.c.mover_movimientos([venta, compra], "F2") self.assertEqual(r, {"movidas": 2, "activo": "F2"}) self.assertEqual({m["activo"] for m in self.en_disco().values()}, {"F2"}) def test_mover_es_todo_o_nada(self): self.op("Compra", "2025-01-01", 5) # esta se queda en F1 compra = self.op("Compra", "2025-01-02", 1) venta = self.op("Venta", "2025-02-01", 3) # en F2 solo habría 1: falla la segunda antes = json.dumps(self.en_disco(), sort_keys=True) with self.assertRaisesRegex(ErrorOperacion, "No se ha movido ninguna"): self.c.mover_movimientos([compra, venta], "F2") self.assertEqual(json.dumps(self.en_disco(), sort_keys=True), antes) self.assertEqual({m["activo"] for m in self.c.datos.vivos("movimientos") if m["id"] in (compra, venta)}, {"F1"}) # también en memoria def test_mover_a_un_prestamo_recibido_no_se_puede(self): a = self.op("Compra", "2025-01-01", 1) with self.assertRaises(ErrorOperacion): self.c.mover_movimientos([a], "D1") class TestAdjuntos(Base): def test_adjuntar_leer_y_quitar(self): mid = self.op("Compra", "2025-01-01", 1) m = self.c.adjuntar(mid, "../../carpeta/extracto banco.pdf", PDF) ref = m["adjuntos"][0] self.assertEqual(ref, {"id": hashlib.sha256(PDF).hexdigest(), "nombre": "extracto banco.pdf", "tipo": "application/pdf", "bytes": len(PDF)}) self.assertEqual(self.en_disco()[mid]["adjuntos"], [ref]) self.assertEqual(self.c.leer_adjunto(mid, ref["id"]), (ref, PDF)) self.c.quitar_adjunto(mid, ref["id"]) self.assertEqual(self.en_disco()[mid]["adjuntos"], []) self.assertTrue(adjuntos.tiene(self.carpeta, ref["id"])) # el fichero se queda def test_editar_la_operacion_conserva_los_adjuntos(self): mid = self.op("Compra", "2025-01-01", 1) self.c.adjuntar(mid, "a.pdf", PDF) self.c.guardar_movimiento({"id": mid, "activo": "F1", "tipo": "Compra", "fecha": "2025-01-02", "cantidad": 2, "precio": 10}) self.assertEqual(len(self.en_disco()[mid]["adjuntos"]), 1) def test_el_mismo_fichero_ocupa_una_vez(self): a, b = self.op("Compra", "2025-01-01", 1), self.op("Compra", "2025-02-01", 1) self.c.adjuntar(a, "uno.pdf", PDF) self.c.adjuntar(b, "otro-nombre.pdf", PDF) self.assertEqual(len(list((self.carpeta / "adjuntos").iterdir())), 1) with self.assertRaisesRegex(ErrorOperacion, "ya está adjunto"): self.c.adjuntar(a, "repetido.pdf", PDF) def test_limites_y_tipos(self): mid = self.op("Compra", "2025-01-01", 1) for nombre, contenido, error in (("x.html", b"