Files
epub2pdf/tests/test_edge_cases.py
T

101 lines
4.2 KiB
Python

"""Edge cases: bad uploads, size limits, unknown jobs, path traversal."""
import io
import zipfile
import app
from tests.helpers import make_epub_zip, make_zip_without_container, wait_for_job
def test_wrong_extension_returns_400(client, fixture_epub_bytes):
files = {"file": (io.BytesIO(fixture_epub_bytes), "book.pdf")}
resp = client.post("/api/convert", data=files, content_type="multipart/form-data")
assert resp.status_code == 400
assert ".epub" in resp.get_json()["error"]
def test_corrupt_zip_returns_202_then_job_error(client):
"""Non-zip upload: accepted (202) but the job must end in error state."""
files = {"file": (io.BytesIO(b"this is not a zip at all"), "book.epub")}
resp = client.post("/api/convert", data=files, content_type="multipart/form-data")
assert resp.status_code == 202
job_id = resp.get_json()["job_id"]
job = wait_for_job(client, job_id)
assert job["status"] == "error"
assert "Not a valid EPUB" in job["error"]
# The failed upload must not linger on disk.
assert not (app.UPLOAD_DIR / f"{job_id}.epub").exists()
def test_zip_without_container_xml_ends_in_job_error(client):
"""Valid zip, not an EPUB: worker hits the missing-container.xml error."""
files = {
"file": (io.BytesIO(make_zip_without_container()), "book.epub"),
}
resp = client.post("/api/convert", data=files, content_type="multipart/form-data")
assert resp.status_code == 202
job = wait_for_job(client, resp.get_json()["job_id"])
assert job["status"] == "error", job
assert "container.xml" in job["error"]
def test_epub_without_readable_content_ends_in_job_error(client):
"""EPUB whose spine has no html items -> 'No readable content' error."""
content = make_epub_zip(
items=[("img1", "img.png", "image/png", "")],
spine_ids=["img1"],
)
files = {"file": (io.BytesIO(content), "empty.epub")}
resp = client.post("/api/convert", data=files, content_type="multipart/form-data")
assert resp.status_code == 202
job = wait_for_job(client, resp.get_json()["job_id"])
assert job["status"] == "error", job
assert "No readable content" in job["error"]
def test_oversized_upload_returns_413_json(client, submit_epub, monkeypatch):
"""Flask MAX_CONTENT_LENGTH path: 413 with a JSON error body."""
monkeypatch.setitem(app.app.config, "MAX_CONTENT_LENGTH", 1024) # 1 KiB
resp = submit_epub(filename="book.epub") # fixture is ~3.3 KB
assert resp.status_code == 413
body = resp.get_json()
assert body is not None, "413 body must be JSON, not the HTML error page"
assert "too large" in body["error"].lower()
def test_app_side_size_check_returns_400(client, submit_epub, monkeypatch):
"""app.py's own MAX_UPLOAD_MB check (distinct from Flask's 413)."""
monkeypatch.setattr(app, "MAX_UPLOAD_MB", 0)
monkeypatch.setitem(app.app.config, "MAX_CONTENT_LENGTH", 10**10)
resp = submit_epub(filename="book.epub")
assert resp.status_code == 400
assert "too large" in resp.get_json()["error"].lower()
def test_unknown_job_returns_404(client):
resp = client.get("/api/jobs/doesnotexist12")
assert resp.status_code == 404
assert resp.get_json()["error"] == "unknown job"
def test_download_unknown_file_returns_404(client):
assert client.get("/download/never-converted.pdf").status_code == 404
def test_download_traversal_dotdot_returns_404(client):
""".. segments must be neutralized by Path(name).name, never resolve
outside OUTPUT_DIR."""
for name in ("../app.py", "../../etc/passwd", "..%2F..%2Fetc%2Fpasswd"):
resp = client.get(f"/download/{name}")
assert resp.status_code == 404, f"traversal not blocked for {name!r}"
def test_download_traversal_cannot_read_app_source(client, monkeypatch, tmp_path):
"""Prove the guard: even a file sitting next to OUTPUT_DIR is unreachable."""
secret = tmp_path / "secret.txt"
secret.write_text("top secret", encoding="utf-8")
# OUTPUT_DIR is DATA_DIR/output; the secret is one level up.
# Path('..').name == '' -> OUTPUT_DIR / '' -> OUTPUT_DIR itself (a dir)
# -> is_file() False -> 404 regardless.
resp = client.get("/download/..%2Fsecret.txt")
assert resp.status_code == 404
assert b"top secret" not in resp.data