"""End-to-end rendering test for the PDF export endpoint. Drives POST /api/v1/research//export/pdf — the endpoint the UI's "Download PDF" button calls — through the real exporter chain (ExporterRegistry → PDFExporter → PDFService → WeasyPrint). Only the data layer (DB session, report assembly) is mocked, so the returned bytes are a genuine WeasyPrint render. Regression coverage for the digit-capture bug (#5050): listing an emoji family in the PDF font stacks made Pango draw every digit 0-9 with Noto Color Emoji, so numbers in exported reports rendered as wide, square emoji glyphs ("2 0 2 6" instead of "2026"). The service-level tests in tests/web/services/test_pdf_service.py assert the same property on PDFService directly; this test pins it to the HTTP surface so a change anywhere in the export chain (route, exporter registry, service) that reintroduces the bug still fails CI. """ import io from contextlib import contextmanager from unittest.mock import MagicMock, patch import pytest from flask import Flask MODULE = "local_deep_research.web.routes.research_routes" ASSEMBLY_MOD = "local_deep_research.web.services.report_assembly_service" # Digit-heavy markdown mirroring a real research report (dates, prices, # token counts, citation markers) — the shapes that were unreadable. REPORT_MARKDOWN = ( "# Pricing report 2026-07-11\n\n" "As of July 11, 2026, plans cost $20/month and allow 44,000 tokens " "per rolling 5-hour window [21][30].\n" ) @pytest.fixture() def app(): flask_app = Flask(__name__) flask_app.secret_key = "test-secret-key" flask_app.config["TESTING"] = True from local_deep_research.web.routes.research_routes import research_bp flask_app.register_blueprint(research_bp) with patch("local_deep_research.web.auth.decorators.db_manager") as mock_db: mock_db.is_user_connected.return_value = True yield flask_app @pytest.fixture() def client(app): return app.test_client() @pytest.fixture(autouse=True) def _inject_session(app): @app.before_request def _set_sess(): from flask import session session["username"] = "testuser" session["session_id"] = "sid-1" @contextmanager def _ctx(session): yield session def _digit_fonts(pdf_bytes): """Return the set of font names used to draw digit glyphs.""" from pdfminer.high_level import extract_pages from pdfminer.layout import LTChar, LTTextContainer, LTTextLine fonts = set() for page in extract_pages(io.BytesIO(pdf_bytes)): for element in page: if not isinstance(element, LTTextContainer): continue for line in element: if not isinstance(line, LTTextLine): continue for char in line: if isinstance(char, LTChar) and char.get_text().isdigit(): fonts.add(char.fontname) return fonts def test_export_pdf_endpoint_renders_digits_with_text_font(client): """The exported PDF must draw digits with a text font, not an emoji font. On hosts with no emoji font installed the emoji-font assertion passes trivially, which is correct — there is no emoji font to capture the digits. """ from local_deep_research.web.services.pdf_service import ( weasyprint_available, ) if not weasyprint_available(): pytest.skip("WeasyPrint system libraries not available") pytest.importorskip("pdfminer.high_level") research = MagicMock() research.id = "res-1" research.title = "Pricing report" research.query = "pricing" mock_session = MagicMock() mock_session.query.return_value.filter_by.return_value.first.return_value = research with ( patch(f"{MODULE}.get_user_db_session", return_value=_ctx(mock_session)), patch( f"{ASSEMBLY_MOD}.assemble_full_report", return_value=REPORT_MARKDOWN, ), ): resp = client.post("/api/v1/research/res-1/export/pdf") assert resp.status_code == 200 assert resp.content_type == "application/pdf" assert resp.data[:4] == b"%PDF" digit_fonts = _digit_fonts(resp.data) assert digit_fonts, "expected the rendered PDF to contain digits" emoji_fonts = {f for f in digit_fonts if "emoji" in f.lower()} assert not emoji_fonts, ( f"digits in the exported PDF were rendered with an emoji font: " f"{emoji_fonts}" )