diff --git a/backend/app/routers/pdf.py b/backend/app/routers/pdf.py index 2da165e..a68beed 100644 --- a/backend/app/routers/pdf.py +++ b/backend/app/routers/pdf.py @@ -5,7 +5,7 @@ import logging from fastapi import ( - APIRouter, BackgroundTasks, Depends, File, + APIRouter, Depends, File, Form, HTTPException, UploadFile, status, ) from fastapi.responses import FileResponse @@ -154,7 +154,6 @@ async def sign_pdf( async def download_signed( task_id: str, token: str, - background: BackgroundTasks, ss: StorageService = Depends(get_storage_service), ) -> FileResponse: @@ -180,11 +179,11 @@ async def download_signed( if not path.is_file(): _raise_http(SignedPdfNotReadyError()) - background.add_task( - ss.remove_task, - task_id, - ) - + # The task dir is deliberately NOT removed after download. A signed + # internship agreement is a lasting record: the dashboard previews it, the + # coordinator may re-download it, and it is emailed to the candidate. The + # delete-after-download behaviour here came from this service's one-shot + # PDF-signing origins and destroyed contracts on first download. return FileResponse( path=str(path), media_type="application/pdf", diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 38f1bbb..bfe6c3a 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -55,6 +55,50 @@ def test_missing_application_returns_404(): assert client.get("/applications/9999").status_code == 404 +def _upload_and_sign() -> str: + """Upload a blank PDF, sign it, and return the download URL.""" + doc = fitz.open() + doc.new_page() + pdf_buf = io.BytesIO() + doc.save(pdf_buf) + pdf_buf.seek(0) + + upload = client.post( + "/pdf/upload", + files={"file": ("sample.pdf", pdf_buf, "application/pdf")}, + ) + assert upload.status_code == 201 + task_id = upload.json()["task_id"] + + img = Image.new("RGBA", (50, 20), (255, 0, 0, 255)) + img_buf = io.BytesIO() + img.save(img_buf, format="PNG") + img_buf.seek(0) + + signed = client.post( + "/pdf/sign", + data={"task_id": task_id, "page": 0, "x": 10, "y": 10, "w": 50, "h": 20}, + files={"image": ("sig.png", img_buf, "image/png")}, + ) + assert signed.status_code == 200 + return signed.json()["download_url"] + + +def test_signed_contract_survives_download(): + """Regression test: /pdf/download used to schedule remove_task() as a + background task, wiping the whole task directory the first time the file was + fetched. The dashboard opens that link automatically right after signing, so + a signed internship agreement — which is previewed, re-downloaded and + emailed to the candidate — was destroyed on first download. Downloading + twice must keep working. + """ + download_url = _upload_and_sign() + + assert client.get(download_url).status_code == 200 + # The file must still be there for the preview / email / re-download paths. + assert client.get(download_url).status_code == 200 + + def test_sign_pdf_download_url_is_reachable(): """Regression test: /pdf/sign previously returned a download_url prefixed with settings.api_prefix ("/api/v1/pdf/download/..."), but routers are