mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 20:15:30 +00:00
fix: minor issue fixes
This commit is contained in:
1 parent
99484e02c8
commit
75e07af78f
16 files changed
+950
-43
No files matched your search
@@ -297,8 +297,9 @@ class TestSharedAccess:
|
||||
# Download
|
||||
# ---------------------------------------------------------------------------
|
||||
class _FakeStorage:
|
||||
def __init__(self, data: bytes):
|
||||
def __init__(self, data: bytes = b""):
|
||||
self._data = data
|
||||
self.deleted: list = []
|
||||
|
||||
def get_file(self, path):
|
||||
return io.BytesIO(self._data)
|
||||
@@ -306,6 +307,10 @@ class _FakeStorage:
|
||||
def generate_presigned_url(self, path, expires_in=300):
|
||||
return f"https://signed.example/{path}?exp={expires_in}"
|
||||
|
||||
def delete_file(self, path):
|
||||
self.deleted.append(path)
|
||||
return True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDownloadArtifact:
|
||||
@@ -343,6 +348,27 @@ class TestDownloadArtifact:
|
||||
assert resp.headers["Content-Disposition"] == 'attachment; filename="deck.pptx"'
|
||||
assert resp.headers["Content-Type"] == "application/vnd.ms-powerpoint"
|
||||
|
||||
def test_local_download_is_streamed_not_buffered(
|
||||
self, _patch_db, flask_app, token_owner, monkeypatch
|
||||
):
|
||||
# The response body must be a stream (generator), not the whole object
|
||||
# buffered into memory via make_response(file_obj.read()).
|
||||
from application.api.user.artifacts.routes import DownloadArtifact
|
||||
|
||||
art = self._seed(_patch_db)
|
||||
storage = _FakeStorage(b"Z" * 200_000)
|
||||
monkeypatch.setattr(
|
||||
"application.api.user.artifacts.routes.StorageCreator.get_storage",
|
||||
lambda: storage,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"application.api.user.artifacts.routes.settings.URL_STRATEGY",
|
||||
"backend", raising=False,
|
||||
)
|
||||
resp = _call(flask_app, DownloadArtifact, art["id"], token=token_owner)
|
||||
assert resp.is_streamed
|
||||
assert resp.get_data() == b"Z" * 200_000
|
||||
|
||||
def test_s3_strategy_redirects_to_presigned(
|
||||
self, _patch_db, flask_app, token_owner, monkeypatch
|
||||
):
|
||||
@@ -506,6 +532,91 @@ class TestRestoreArtifact:
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDeleteArtifact:
|
||||
def _seed(self, conn, **over):
|
||||
conv = _make_conversation(conn)
|
||||
defaults = dict(
|
||||
conversation_id=str(conv["id"]),
|
||||
filename="f.bin",
|
||||
storage_path="inputs/owner/artifacts/x/v1/f.bin",
|
||||
)
|
||||
defaults.update(over)
|
||||
return _make_artifact(conn, **defaults)
|
||||
|
||||
def test_owner_deletes_and_reaps_bytes(
|
||||
self, _patch_db, flask_app, token_owner, monkeypatch
|
||||
):
|
||||
from application.api.user.artifacts.routes import GetArtifact
|
||||
|
||||
art = self._seed(_patch_db)
|
||||
storage = _FakeStorage()
|
||||
monkeypatch.setattr(
|
||||
"application.api.user.artifacts.routes.StorageCreator.get_storage",
|
||||
lambda: storage,
|
||||
)
|
||||
resp = _call(flask_app, GetArtifact, art["id"], token=token_owner, method="delete")
|
||||
assert resp.status_code == 200
|
||||
assert ArtifactsRepository(_patch_db).get_artifact(art["id"]) is None
|
||||
assert "inputs/owner/artifacts/x/v1/f.bin" in storage.deleted
|
||||
|
||||
def test_stranger_denied(self, _patch_db, flask_app):
|
||||
from application.api.user.artifacts.routes import GetArtifact
|
||||
|
||||
art = self._seed(_patch_db)
|
||||
resp = _call(
|
||||
flask_app, GetArtifact, art["id"], token={"sub": STRANGER}, method="delete"
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
assert ArtifactsRepository(_patch_db).get_artifact(art["id"]) is not None
|
||||
|
||||
def test_share_token_holder_denied(self, _patch_db, flask_app):
|
||||
from application.api.user.artifacts.routes import GetArtifact
|
||||
|
||||
conv = _make_conversation(_patch_db)
|
||||
art = _make_artifact(_patch_db, conversation_id=str(conv["id"]))
|
||||
share = SharedConversationsRepository(_patch_db).create(str(conv["id"]), OWNER)
|
||||
resp = _call(
|
||||
flask_app, GetArtifact, art["id"], token=None,
|
||||
query={"share_token": str(share["uuid"])}, method="delete",
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
assert ArtifactsRepository(_patch_db).get_artifact(art["id"]) is not None
|
||||
|
||||
def test_unknown_artifact_404(self, _patch_db, flask_app, token_owner):
|
||||
from application.api.user.artifacts.routes import GetArtifact
|
||||
|
||||
resp = _call(
|
||||
flask_app, GetArtifact, str(uuid.uuid4()), token=token_owner, method="delete"
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestConversationDeleteReapsArtifacts:
|
||||
def test_delete_conversation_removes_artifacts_frees_quota_and_reaps_bytes(
|
||||
self, _patch_db, flask_app, monkeypatch
|
||||
):
|
||||
conv = _make_conversation(_patch_db)
|
||||
art = _make_artifact(
|
||||
_patch_db, conversation_id=str(conv["id"]),
|
||||
storage_path="k/reap.bin", filename="r.bin", size=42,
|
||||
)
|
||||
storage = _FakeStorage()
|
||||
monkeypatch.setattr(
|
||||
"application.storage.storage_creator.StorageCreator.get_storage",
|
||||
lambda: storage,
|
||||
)
|
||||
|
||||
deleted = ConversationsRepository(_patch_db).delete(str(conv["id"]), OWNER)
|
||||
assert deleted is True
|
||||
|
||||
repo = ArtifactsRepository(_patch_db)
|
||||
assert repo.get_artifact(art["id"]) is None # cascade-removed with the parent
|
||||
assert repo.count_for_user(OWNER) == 0 # quota freed
|
||||
assert "k/reap.bin" in storage.deleted # bytes reaped
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Malformed artifact_id path segment -> 404 (no CAST(:id AS uuid) txn poison)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -327,6 +327,53 @@ class TestMcpOauthTask:
|
||||
assert result == {"url": "http://auth"}
|
||||
|
||||
|
||||
class TestParseDocumentTask:
|
||||
"""parse_document runs on the parsing queue under a bounded time limit."""
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("application.api.user.tasks.parse_document_worker")
|
||||
def test_calls_parse_document_worker(self, mock_worker):
|
||||
from application.api.user.tasks import parse_document
|
||||
|
||||
mock_worker.return_value = {"status": "ok", "content": "hi"}
|
||||
|
||||
result = parse_document(
|
||||
"art-1", {"conversation_id": "c1"}, "user1", {"output": "markdown"},
|
||||
)
|
||||
|
||||
mock_worker.assert_called_once_with(
|
||||
ANY, "art-1", {"conversation_id": "c1"}, "user1",
|
||||
{"output": "markdown"},
|
||||
)
|
||||
assert result == {"status": "ok", "content": "hi"}
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("application.api.user.tasks.parse_document_worker")
|
||||
def test_soft_time_limit_returns_clean_error(self, mock_worker):
|
||||
from celery.exceptions import SoftTimeLimitExceeded
|
||||
|
||||
from application.api.user.tasks import parse_document
|
||||
|
||||
mock_worker.side_effect = SoftTimeLimitExceeded("parse")
|
||||
|
||||
# The task must swallow the soft-limit signal and return the worker's
|
||||
# clean error shape so the parsing-worker slot frees instead of crashing.
|
||||
result = parse_document("art-1", {"conversation_id": "c1"}, "user1")
|
||||
|
||||
assert result["status"] == "error"
|
||||
assert "timed out" in result["error"]
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_time_limits_derived_from_document_parse_timeout(self):
|
||||
from application.api.user.tasks import parse_document
|
||||
from application.core.settings import settings
|
||||
|
||||
assert parse_document.soft_time_limit == settings.DOCUMENT_PARSE_TIMEOUT
|
||||
assert parse_document.time_limit == settings.DOCUMENT_PARSE_TIMEOUT + 30
|
||||
# Hard limit must exceed the soft limit so the handler can unwind first.
|
||||
assert parse_document.time_limit > parse_document.soft_time_limit
|
||||
|
||||
|
||||
class TestDurableTaskRetryPolicy:
|
||||
"""The long-running tasks share a uniform retry policy."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user