From 6ab950ec4274c9c9853cdb0ced0f0c1e1aaf89fc Mon Sep 17 00:00:00 2001 From: Kitos Date: Mon, 18 May 2026 14:00:46 +0200 Subject: [PATCH] feat(reports): add quarterly and technique download routes [FASE-2.4] Expose GET endpoints for quarterly-summary and technique reports with PDF, DOCX, and HTML formats. --- backend/app/routers/professional_reports.py | 35 ++++++++++++++++ .../tests/test_professional_reports_router.py | 42 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 backend/tests/test_professional_reports_router.py diff --git a/backend/app/routers/professional_reports.py b/backend/app/routers/professional_reports.py index ad4ed9f..7fe0bb0 100644 --- a/backend/app/routers/professional_reports.py +++ b/backend/app/routers/professional_reports.py @@ -70,3 +70,38 @@ def generate_executive_report( media_type=_MEDIA_TYPES[format], filename=f"executive_summary.{format}", ) + + +@router.get("/quarterly-summary") +def generate_quarterly_report( + format: str = Query("pdf", pattern="^(pdf|docx|html)$"), + db: Session = Depends(get_db), + user: User = Depends(require_any_role("red_lead", "blue_lead", "viewer")), +): + """Generate a quarterly security summary report.""" + filepath = report_generation_service.generate_quarterly_summary( + db, output_format=format, + ) + return FileResponse( + filepath, + media_type=_MEDIA_TYPES[format], + filename=f"quarterly_summary.{format}", + ) + + +@router.get("/technique/{technique_id}") +def generate_technique_report( + technique_id: UUID, + format: str = Query("pdf", pattern="^(pdf|docx|html)$"), + db: Session = Depends(get_db), + user: User = Depends(get_current_user), +): + """Generate a detailed report for one MITRE technique.""" + filepath = report_generation_service.generate_technique_detail_report( + db, str(technique_id), output_format=format, + ) + return FileResponse( + filepath, + media_type=_MEDIA_TYPES[format], + filename=f"technique_{technique_id}.{format}", + ) diff --git a/backend/tests/test_professional_reports_router.py b/backend/tests/test_professional_reports_router.py new file mode 100644 index 0000000..6651707 --- /dev/null +++ b/backend/tests/test_professional_reports_router.py @@ -0,0 +1,42 @@ +"""Professional reports router tests (FASE-2.4).""" + +from unittest.mock import patch + +from app.models.campaign import Campaign + + +@patch("app.services.report_generation_service.generate_purple_campaign_report") +def test_purple_campaign_pdf_download(mock_gen, client, auth_headers, db): + mock_gen.return_value = __file__ # existing file for FileResponse + + campaign = Campaign(name="Export Camp", status="active") + db.add(campaign) + db.commit() + + r = client.get( + f"/api/v1/reports/generate/purple-campaign/{campaign.id}", + params={"format": "pdf"}, + headers=auth_headers, + ) + assert r.status_code == 200 + assert r.headers["content-type"] == "application/pdf" + + +@patch("app.services.report_generation_service.generate_coverage_report") +def test_coverage_summary_html(mock_gen, client, auth_headers): + import tempfile + import os + + fd, path = tempfile.mkstemp(suffix=".html") + os.write(fd, b"ok") + os.close(fd) + mock_gen.return_value = path + + r = client.get( + "/api/v1/reports/generate/coverage-summary", + params={"format": "html"}, + headers=auth_headers, + ) + assert r.status_code == 200 + assert "text/html" in r.headers["content-type"] + os.unlink(path)