import json import os import pytest import plantillas from plantillas import PlantillaError, renderizar EJEMPLOS = os.path.join(plantillas.CARPETA_PLANTILLAS, "ejemplos.json") def _ejemplos() -> dict: with open(EJEMPLOS, encoding="utf-8") as f: return json.load(f) # ============================================================================ # Motor de plantillas # ============================================================================ def test_variable_con_escape_de_html(): assert renderizar("{{x}}", {"x": 'A & B "c" \'d\''}) == "A & B <i> "c" 'd'" def test_variable_none_queda_vacia_y_numeros_se_convierten(): assert renderizar("[{{a}}][{{b}}]", {"a": None, "b": 12.5}) == "[][12.5]" def test_variable_faltante_lanza_error(): with pytest.raises(PlantillaError, match="Falta 'x'"): renderizar("{{x}}", {}) def test_seccion_faltante_tambien_lanza_error(): with pytest.raises(PlantillaError, match="Falta 'opcional'"): renderizar("{{#opcional}}x{{/opcional}}", {}) with pytest.raises(PlantillaError, match="Falta 'opcional'"): renderizar("{{^opcional}}x{{/opcional}}", {}) def test_seccion_de_lista_se_repite_y_ve_el_contexto_externo(): texto = "{{titulo}}:{{#items}}[{{titulo}}-{{n}}]{{/items}}" assert renderizar(texto, {"titulo": "T", "items": [{"n": 1}, {"n": 2}]}) == "T:[T-1][T-2]" def test_lista_vacia_no_muestra_nada_y_la_invertida_si(): texto = "{{#items}}x{{/items}}{{^items}}vacío{{/items}}" assert renderizar(texto, {"items": []}) == "vacío" assert renderizar(texto, {"items": [{}]}) == "x" @pytest.mark.parametrize("valor, visible", [(True, True), (False, False), (None, False), ("", False), ("a", True), (0, False)]) def test_seccion_booleana(valor, visible): texto = "{{#f}}SI{{/f}}{{^f}}NO{{/f}}" assert renderizar(texto, {"f": valor}) == ("SI" if visible else "NO") def test_seccion_con_texto_muestra_una_vez_y_la_variable_sigue_disponible(): """Caso de ``fecha_estimada_pago``: la sección usa un texto como condición y lo imprime dentro.""" texto = "{{#pago}}Pago: {{pago}}{{/pago}}" assert renderizar(texto, {"pago": "18/11/2026"}) == "Pago: 18/11/2026" assert renderizar(texto, {"pago": None}) == "" def test_secciones_con_el_mismo_nombre_seguidas_en_una_linea(): """Caso de ``conforme``: ``{{#conforme}}..{{/conforme}}{{^conforme}}..{{/conforme}}``.""" texto = "{{#guias}}<{{#ok}}Conforme{{/ok}}{{^ok}}No coincide{{/ok}}>{{/guias}}" assert renderizar(texto, {"guias": [{"ok": True}, {"ok": False}]}) == "" def test_secciones_anidadas(): texto = "{{#obs}}{{t}}({{#filas}}{{k}}={{v}};{{/filas}}){{/obs}}" contexto = {"obs": [{"t": "A", "filas": [{"k": "x", "v": 1}, {"k": "y", "v": 2}]}, {"t": "B", "filas": []}]} assert renderizar(texto, contexto) == "A(x=1;y=2;)B()" def test_seccion_sin_cerrar_o_cierre_sin_abrir(): with pytest.raises(PlantillaError, match="no se cierra"): renderizar("{{#a}}x", {"a": True}) with pytest.raises(PlantillaError, match="no corresponde"): renderizar("x{{/a}}", {"a": True}) with pytest.raises(PlantillaError, match="no corresponde"): renderizar("{{#a}}{{#b}}x{{/a}}{{/b}}", {"a": True, "b": True}) def test_llaves_sueltas_de_css_no_se_confunden_con_etiquetas(): assert renderizar("a{color:red;}{{x}}", {"x": "1"}) == "a{color:red;}1" def test_nombres_usados(): assert plantillas.nombres_usados("{{a}}{{#b}}{{c}}{{/b}}{{^d}}x{{/d}}") == {"a", "b", "c", "d"} # ============================================================================ # Plantillas reales con los ejemplos de plantilla/ejemplos.json # ============================================================================ ESCENARIOS = sorted(_ejemplos()) def _claves(obj) -> set[str]: if isinstance(obj, dict): return set(obj) | {c for v in obj.values() for c in _claves(v)} if isinstance(obj, list): return {c for v in obj for c in _claves(v)} return set() @pytest.mark.parametrize("escenario", ESCENARIOS) def test_todos_los_escenarios_se_renderizan_completos(escenario): ejemplo = _ejemplos()[escenario] html = plantillas.renderizar_plantilla(ejemplo["plantilla"], ejemplo["contexto"]) assert "{{" not in html and "}}" not in html # nada sin resolver assert ejemplo["contexto"]["codigo_seguimiento"] in html assert html.lstrip().startswith("") @pytest.mark.parametrize("nombre", ["aceptado", "rechazado"]) def test_los_ejemplos_cubren_las_variables_de_la_plantilla(nombre): """Si la plantilla pide algo nuevo (o un ejemplo sobra), el desfase se nota aquí.""" usados = plantillas.nombres_usados(plantillas.cargar_plantilla(nombre)) ejemplos = [e["contexto"] for e in _ejemplos().values() if e["plantilla"] == nombre] assert ejemplos, f"No hay ejemplos para la plantilla {nombre}" cubiertos = set().union(*(_claves(c) for c in ejemplos)) # el ejemplo sin guías no trae las claves de cada guía assert usados <= cubiertos, f"Ningún ejemplo trae: {sorted(usados - cubiertos)}" for contexto in ejemplos: sobran = _claves(contexto) - usados assert not sobran, f"Sobran en el ejemplo: {sorted(sobran)}" def test_razon_social_con_caracteres_especiales_se_escapa(): ejemplo = _ejemplos()["rechazado_una_observacion"] html = plantillas.renderizar_plantilla("rechazado", ejemplo["contexto"]) assert "<Lima>" in html and ""EL RÁPIDO"" in html assert "" not in html def test_aceptado_muestra_pago_estimado_solo_si_hay_fecha(): ejemplos = _ejemplos() con = plantillas.renderizar_plantilla("aceptado", ejemplos["aceptado_con_pago"]["contexto"]) sin = plantillas.renderizar_plantilla("aceptado", ejemplos["aceptado_sin_pago"]["contexto"]) assert "Pago estimado" in con and "18/11/2026" in con assert "Pago estimado" not in sin def test_rechazado_numera_las_observaciones_y_marca_las_guias(): html = plantillas.renderizar_plantilla("rechazado", _ejemplos()["rechazado_dos_observaciones"]["contexto"]) assert "OBSERVACIÓN 1" in html and "OBSERVACIÓN 2" in html assert "OBSERVACIÓN 3" not in html assert html.count("Conforme") == 1 and html.count("No coincide") == 1 assert "Rechazado · 2 observaciones" in html def test_enlaces_de_contacto_llevan_el_codigo_y_el_comprobante(): contexto = _ejemplos()["rechazado_dos_observaciones"]["contexto"] html = plantillas.renderizar_plantilla("rechazado", contexto) assert f"mailto:{contexto['contacto_correo']}?subject=Consulta%20F002-486%20ABR-261019-00045" in html assert f'href="mailto:{contexto["envio_correo"]}"' in html def test_rechazo_sin_comprobante_oculta_lo_que_no_existe(): ejemplo = _ejemplos()["rechazado_sin_comprobante"] html = plantillas.renderizar_plantilla("rechazado", ejemplo["contexto"]) assert "Guías revisadas" not in html # sin tabla de guías vacía assert "Comprobante" not in html # sin la fila "Comprobante —" assert "RUC —" not in html and html.count("RUC") == 1 # solo el RUC del pie (empresa) assert "No pudimos registrar su comprobante" in html def test_rechazo_con_comprobante_si_muestra_guias_ruc_y_fila_comprobante(): html = plantillas.renderizar_plantilla("rechazado", _ejemplos()["rechazado_dos_observaciones"]["contexto"]) assert "Guías revisadas" in html and "RUC 20537929520" in html and "Comprobante" in html