fix/de los tickets T2026-05-027, T2025-09-004 y T2025-09-056
This commit is contained in:
@@ -224,3 +224,275 @@ class BulkCreateDocumentReplaceTests(APITestCase):
|
||||
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_207_MULTI_STATUS, status.HTTP_201_CREATED])
|
||||
data = response.json()
|
||||
self.assertGreaterEqual(data.get("already_existing_count", 0), 1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests del comando fix_partidas_error
|
||||
# Una partida descargado=True solo es válida si alguno de sus documentos
|
||||
# contiene consultarPartidaRespuesta sin tieneError=true. Partidas que solo
|
||||
# tienen el REQUEST (o errores) deben volver a descargado=False.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
from io import StringIO
|
||||
from types import SimpleNamespace
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
XML_RESPUESTA_VALIDA = (
|
||||
"<?xml version='1.0' encoding='UTF-8'?>"
|
||||
'<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body>'
|
||||
'<ns9:consultarPartidaRespuesta xmlns:ns9="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpartida">'
|
||||
"<tieneError>false</tieneError><ns9:partida/></ns9:consultarPartidaRespuesta>"
|
||||
"</S:Body></S:Envelope>"
|
||||
)
|
||||
|
||||
XML_ERROR_VUCEM = (
|
||||
"<?xml version='1.0' encoding='UTF-8'?>"
|
||||
'<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body>'
|
||||
'<ns9:consultarPartidaRespuesta xmlns:ns9="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpartida">'
|
||||
"<tieneError>true</tieneError></ns9:consultarPartidaRespuesta>"
|
||||
"</S:Body></S:Envelope>"
|
||||
)
|
||||
|
||||
XML_ECO_REQUEST = (
|
||||
"<?xml version='1.0' encoding='UTF-8'?>"
|
||||
'<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"'
|
||||
' xmlns:con="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpartida"><soapenv:Body>'
|
||||
"<con:consultarPartidaPeticion><con:peticion/></con:consultarPartidaPeticion>"
|
||||
"</soapenv:Body></soapenv:Envelope>"
|
||||
)
|
||||
|
||||
|
||||
class _FakeMinioObject:
|
||||
"""Simula el objeto retornado por minio get_object."""
|
||||
|
||||
def __init__(self, content):
|
||||
self._content = content
|
||||
|
||||
def read(self):
|
||||
return self._content
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def release_conn(self):
|
||||
pass
|
||||
|
||||
|
||||
class FixPartidasErrorCommandTests(TestCase):
|
||||
PED_APP = "24-01-3420-1234567"
|
||||
|
||||
def setUp(self):
|
||||
from api.customs.models import Partida
|
||||
from api.record.models import DocumentType
|
||||
|
||||
self.licencia = Licencia.objects.create(nombre="LicFixPartidas", almacenamiento=100)
|
||||
self.org = Organizacion.objects.create(
|
||||
nombre="OrgFixPartidas", licencia=self.licencia, is_active=True, is_verified=True
|
||||
)
|
||||
# Pedimento VÁLIDO (no malformado): el comando ya no se limita a malformados
|
||||
self.pedimento = Pedimento.objects.create(
|
||||
organizacion=self.org,
|
||||
pedimento="1234567",
|
||||
pedimento_app=self.PED_APP,
|
||||
aduana="034",
|
||||
patente="3420",
|
||||
numero_operacion="12345678",
|
||||
)
|
||||
self.partida = Partida.objects.create(
|
||||
pedimento=self.pedimento,
|
||||
organizacion=self.org,
|
||||
numero_partida=1,
|
||||
descargado=True,
|
||||
)
|
||||
self.type_resp = DocumentType.objects.get_or_create(id=1, defaults={"nombre": "XML"})[0]
|
||||
self.type_req = DocumentType.objects.get_or_create(id=17, defaults={"nombre": "PT Request"})[0]
|
||||
self.type_err = DocumentType.objects.get_or_create(id=18, defaults={"nombre": "PT Error"})[0]
|
||||
|
||||
# Storage simulado: dict path -> bytes
|
||||
self.storage = {}
|
||||
patcher = patch("api.customs.management.commands.fix_partidas_error.minio_client")
|
||||
self.minio = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
self.minio._bucket_name = "test-bucket"
|
||||
self.minio.file_exists.side_effect = lambda name: name in self.storage
|
||||
self.minio._client.get_object.side_effect = (
|
||||
lambda bucket, name: _FakeMinioObject(self.storage[name])
|
||||
)
|
||||
self.minio.upload_file.side_effect = (
|
||||
lambda name, file_data=None, content_type=None: self.storage.__setitem__(
|
||||
name, file_data.read()
|
||||
)
|
||||
)
|
||||
self.minio.delete_file.side_effect = lambda name: self.storage.pop(name, None)
|
||||
|
||||
def _doc(self, filename, doc_type, content=None):
|
||||
from api.record.models import Document
|
||||
|
||||
path = f"org/{self.PED_APP}/{filename}"
|
||||
doc = Document.objects.create(
|
||||
organizacion=self.org,
|
||||
pedimento=self.pedimento,
|
||||
document_type=doc_type,
|
||||
archivo=path,
|
||||
size=100,
|
||||
extension="xml",
|
||||
)
|
||||
if content is not None:
|
||||
self.storage[path] = content.encode("utf-8")
|
||||
return doc
|
||||
|
||||
def _run(self, **kwargs):
|
||||
out = StringIO()
|
||||
call_command("fix_partidas_error", stdout=out, stderr=StringIO(), **kwargs)
|
||||
return out.getvalue()
|
||||
|
||||
def test_partida_solo_request_se_marca_no_descargada(self):
|
||||
"""El caso reportado: descargado=True pero solo existe el XML del REQUEST."""
|
||||
self._doc(f"vu_PT_{self.PED_APP}_1_REQUEST.xml", self.type_req, XML_ECO_REQUEST)
|
||||
|
||||
self._run(pedimento=str(self.pedimento.id))
|
||||
|
||||
self.partida.refresh_from_db()
|
||||
self.assertFalse(self.partida.descargado)
|
||||
|
||||
def test_partida_sin_documentos_se_marca_no_descargada(self):
|
||||
"""descargado=True sin ningún documento tampoco es una descarga real."""
|
||||
self._run(pedimento=str(self.pedimento.id))
|
||||
|
||||
self.partida.refresh_from_db()
|
||||
self.assertFalse(self.partida.descargado)
|
||||
|
||||
def test_partida_con_respuesta_valida_permanece_descargada(self):
|
||||
"""Con consultarPartidaRespuesta sin error la partida no se toca."""
|
||||
self._doc(f"vu_PT_{self.PED_APP}_1_REQUEST.xml", self.type_req, XML_ECO_REQUEST)
|
||||
self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, XML_RESPUESTA_VALIDA)
|
||||
|
||||
self._run(pedimento=str(self.pedimento.id))
|
||||
|
||||
self.partida.refresh_from_db()
|
||||
self.assertTrue(self.partida.descargado)
|
||||
|
||||
def test_doc_con_error_vucem_se_renombra_y_marca_no_descargada(self):
|
||||
"""tieneError=true: doc → type 18 con sufijo _ERROR y partida → False."""
|
||||
doc = self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, XML_ERROR_VUCEM)
|
||||
old_path = doc.archivo.name
|
||||
|
||||
self._run(pedimento=str(self.pedimento.id))
|
||||
|
||||
self.partida.refresh_from_db()
|
||||
doc.refresh_from_db()
|
||||
self.assertFalse(self.partida.descargado)
|
||||
self.assertEqual(doc.document_type_id, 18)
|
||||
self.assertTrue(doc.archivo.name.endswith(f"vu_PT_{self.PED_APP}_1_ERROR.xml"))
|
||||
self.assertTrue(doc.vu)
|
||||
self.assertNotIn(old_path, self.storage)
|
||||
self.assertIn(doc.archivo.name, self.storage)
|
||||
|
||||
def test_eco_de_request_guardado_como_respuesta_se_reclasifica(self):
|
||||
"""Un eco de consultarPartidaPeticion guardado como respuesta se
|
||||
reclasifica a type 17 sin chocar con el REQUEST real existente."""
|
||||
self._doc(f"vu_PT_{self.PED_APP}_1_REQUEST.xml", self.type_req, XML_ECO_REQUEST)
|
||||
doc = self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, XML_ECO_REQUEST)
|
||||
|
||||
self._run(pedimento=str(self.pedimento.id))
|
||||
|
||||
self.partida.refresh_from_db()
|
||||
doc.refresh_from_db()
|
||||
self.assertFalse(self.partida.descargado)
|
||||
self.assertEqual(doc.document_type_id, 17)
|
||||
# El nombre sin índice ya lo usa el REQUEST real → debe ir con _1
|
||||
self.assertTrue(doc.archivo.name.endswith(f"vu_PT_{self.PED_APP}_1_REQUEST_1.xml"))
|
||||
|
||||
def test_doc_ausente_sin_canario_no_cambia_partida(self):
|
||||
"""Archivo ausente y NINGÚN archivo del pedimento en storage: posible
|
||||
storage equivocado (p. ej. dev) → sin cambios."""
|
||||
self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, content=None)
|
||||
|
||||
self._run(pedimento=str(self.pedimento.id))
|
||||
|
||||
self.partida.refresh_from_db()
|
||||
self.assertTrue(self.partida.descargado)
|
||||
|
||||
def test_registro_fantasma_con_storage_real_se_marca_no_descargada(self):
|
||||
"""Document type 1 en BD sin archivo en storage, pero el REQUEST sí
|
||||
existe físicamente (canario): el storage es el correcto, el registro es
|
||||
fantasma → la partida no tiene XML de partida → descargado=False."""
|
||||
self._doc(f"vu_PT_{self.PED_APP}_1_REQUEST.xml", self.type_req, XML_ECO_REQUEST)
|
||||
fantasma = self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, content=None)
|
||||
|
||||
self._run(pedimento=str(self.pedimento.id))
|
||||
|
||||
self.partida.refresh_from_db()
|
||||
fantasma.refresh_from_db()
|
||||
self.assertFalse(self.partida.descargado)
|
||||
# El registro fantasma se reporta pero no se modifica ni se borra
|
||||
self.assertEqual(fantasma.document_type_id, 1)
|
||||
|
||||
def test_storage_inaccesible_no_cambia_partida(self):
|
||||
"""Excepción al consultar storage (conexión caída): sin cambios."""
|
||||
self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, XML_ERROR_VUCEM)
|
||||
self.minio.file_exists.side_effect = Exception("connection refused")
|
||||
|
||||
self._run(pedimento=str(self.pedimento.id))
|
||||
|
||||
self.partida.refresh_from_db()
|
||||
self.assertTrue(self.partida.descargado)
|
||||
|
||||
def test_naming_legacy_valida_partida(self):
|
||||
"""Documentos con nomenclatura legacy (partida al final) también validan."""
|
||||
self._doc("vu_PT_010Imp_034_3420_1234567_1.xml", self.type_resp, XML_RESPUESTA_VALIDA)
|
||||
|
||||
self._run(pedimento=str(self.pedimento.id))
|
||||
|
||||
self.partida.refresh_from_db()
|
||||
self.assertTrue(self.partida.descargado)
|
||||
|
||||
def test_dry_run_no_modifica(self):
|
||||
"""--dry-run reporta pero no toca BD ni storage."""
|
||||
doc = self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, XML_ERROR_VUCEM)
|
||||
|
||||
self._run(pedimento=str(self.pedimento.id), dry_run=True)
|
||||
|
||||
self.partida.refresh_from_db()
|
||||
doc.refresh_from_db()
|
||||
self.assertTrue(self.partida.descargado)
|
||||
self.assertEqual(doc.document_type_id, 1)
|
||||
self.assertIn(doc.archivo.name, self.storage)
|
||||
|
||||
def test_universo_general_incluye_pedimentos_validos(self):
|
||||
"""Sin --pedimento ni --solo-malformados también procesa pedimentos bien formados."""
|
||||
self._doc(f"vu_PT_{self.PED_APP}_1_REQUEST.xml", self.type_req, XML_ECO_REQUEST)
|
||||
|
||||
self._run()
|
||||
|
||||
self.partida.refresh_from_db()
|
||||
self.assertFalse(self.partida.descargado)
|
||||
|
||||
def test_solo_malformados_excluye_pedimentos_validos(self):
|
||||
"""Con --solo-malformados un pedimento bien formado no se procesa."""
|
||||
self._doc(f"vu_PT_{self.PED_APP}_1_REQUEST.xml", self.type_req, XML_ECO_REQUEST)
|
||||
|
||||
self._run(solo_malformados=True)
|
||||
|
||||
self.partida.refresh_from_db()
|
||||
self.assertTrue(self.partida.descargado)
|
||||
|
||||
def test_no_confunde_partida_1_con_11(self):
|
||||
"""La asignación por nombre no debe mezclar partida 1 con partida 11."""
|
||||
from api.customs.management.commands.fix_partidas_error import Command
|
||||
|
||||
docs = [
|
||||
SimpleNamespace(id=1, document_type_id=1, archivo=SimpleNamespace(name=f"org/x/vu_PT_{self.PED_APP}_1.xml")),
|
||||
SimpleNamespace(id=2, document_type_id=17, archivo=SimpleNamespace(name=f"org/x/vu_PT_{self.PED_APP}_1_REQUEST.xml")),
|
||||
SimpleNamespace(id=3, document_type_id=1, archivo=SimpleNamespace(name=f"org/x/vu_PT_{self.PED_APP}_11.xml")),
|
||||
SimpleNamespace(id=4, document_type_id=17, archivo=SimpleNamespace(name=f"org/x/vu_PT_{self.PED_APP}_11_REQUEST.xml")),
|
||||
]
|
||||
cmd = Command()
|
||||
|
||||
ids_p1 = {d.id for d in cmd._docs_de_partida(docs, self.PED_APP, 1)}
|
||||
ids_p11 = {d.id for d in cmd._docs_de_partida(docs, self.PED_APP, 11)}
|
||||
|
||||
self.assertEqual(ids_p1, {1, 2})
|
||||
self.assertEqual(ids_p11, {3, 4})
|
||||
|
||||
Reference in New Issue
Block a user