chore: import upstream snapshot with attribution
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""Wiring tests for safe ``.zip`` upload handling in the knowledge router.
|
||||
|
||||
The deep security guards live in ``tests/utils/test_archive_extractor.py``;
|
||||
these check that ``_save_uploaded_files`` routes ``.zip`` uploads through the
|
||||
safe extractor, registers only supported members, and never persists the
|
||||
archive itself.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("fastapi")
|
||||
from fastapi import HTTPException, UploadFile
|
||||
|
||||
from deeptutor.api.routers.knowledge import _save_uploaded_files
|
||||
|
||||
ALLOWED = {".txt", ".md", ".pdf", ".zip"}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _disable_pocketbase(monkeypatch):
|
||||
monkeypatch.setattr("deeptutor.services.pocketbase_client.is_pocketbase_enabled", lambda: False)
|
||||
|
||||
|
||||
def _zip_upload(filename: str, entries: list[tuple[str, bytes]]) -> UploadFile:
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w") as zf:
|
||||
for name, data in entries:
|
||||
zf.writestr(name, data)
|
||||
buf.seek(0)
|
||||
return UploadFile(filename=filename, file=buf)
|
||||
|
||||
|
||||
def test_zip_upload_extracts_only_supported_members(tmp_path: Path) -> None:
|
||||
upload = _zip_upload(
|
||||
"bundle.zip",
|
||||
[
|
||||
("notes.txt", b"hello"),
|
||||
("paper.md", b"# title"),
|
||||
("malware.exe", b"x"), # disallowed -> skipped
|
||||
("inner.zip", b"PK\x03\x04"), # nested archive -> skipped
|
||||
],
|
||||
)
|
||||
raw = tmp_path / "raw"
|
||||
raw.mkdir()
|
||||
|
||||
names, paths = _save_uploaded_files([upload], raw, allowed_extensions=ALLOWED)
|
||||
|
||||
assert sorted(names) == ["notes.txt", "paper.md"]
|
||||
assert (raw / "notes.txt").read_bytes() == b"hello"
|
||||
# The archive itself is never persisted or registered.
|
||||
assert not (raw / "bundle.zip").exists()
|
||||
assert all(not p.endswith(".zip") for p in paths)
|
||||
|
||||
|
||||
def test_zip_upload_with_zip_slip_member_stays_in_target(tmp_path: Path) -> None:
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w") as zf:
|
||||
zf.writestr(zipfile.ZipInfo("../../escape.txt"), b"x")
|
||||
zf.writestr("safe.txt", b"y")
|
||||
buf.seek(0)
|
||||
upload = UploadFile(filename="evil.zip", file=buf)
|
||||
raw = tmp_path / "raw"
|
||||
raw.mkdir()
|
||||
|
||||
names, _ = _save_uploaded_files([upload], raw, allowed_extensions=ALLOWED)
|
||||
|
||||
assert sorted(names) == ["escape.txt", "safe.txt"]
|
||||
assert (raw / "escape.txt").exists()
|
||||
assert not (tmp_path / "escape.txt").exists() # did not escape
|
||||
|
||||
|
||||
def test_invalid_zip_is_rejected(tmp_path: Path) -> None:
|
||||
upload = UploadFile(filename="broken.zip", file=io.BytesIO(b"not a zip"))
|
||||
raw = tmp_path / "raw"
|
||||
raw.mkdir()
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_save_uploaded_files([upload], raw, allowed_extensions=ALLOWED)
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
|
||||
def test_zip_with_no_supported_members_is_rejected(tmp_path: Path) -> None:
|
||||
upload = _zip_upload("only-junk.zip", [("a.exe", b"x"), ("b.sh", b"y")])
|
||||
raw = tmp_path / "raw"
|
||||
raw.mkdir()
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_save_uploaded_files([upload], raw, allowed_extensions=ALLOWED)
|
||||
assert exc_info.value.status_code == 400
|
||||
Reference in New Issue
Block a user