310 lines
12 KiB
Python
310 lines
12 KiB
Python
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
from django.urls import reverse
|
|
import json
|
|
import pytest
|
|
|
|
from review_agent.models import (
|
|
ApplicationFormFillBatch,
|
|
Conversation,
|
|
ExportedSummaryFile,
|
|
FileAttachment,
|
|
FileSummaryBatch,
|
|
Message,
|
|
WorkflowNodeRun,
|
|
)
|
|
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
|
|
def test_upload_attachments_requires_conversation_owner(client, settings, tmp_path, django_user_model):
|
|
settings.MEDIA_ROOT = tmp_path
|
|
owner = django_user_model.objects.create_user(username="owner", password="pass")
|
|
other = django_user_model.objects.create_user(username="other", password="pass")
|
|
conversation = Conversation.objects.create(user=owner, title="会话")
|
|
client.force_login(other)
|
|
|
|
response = client.post(
|
|
reverse("file_summary_attachment_upload", args=[conversation.pk]),
|
|
{"files": [SimpleUploadedFile("a.docx", b"a")]},
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_attachment_api_requires_login(client, django_user_model):
|
|
user = django_user_model.objects.create_user(username="owner", password="pass")
|
|
conversation = Conversation.objects.create(user=user, title="会话")
|
|
|
|
response = client.get(reverse("file_summary_attachment_list", args=[conversation.pk]))
|
|
|
|
assert response.status_code == 302
|
|
|
|
|
|
def test_upload_and_list_current_conversation_attachments(client, settings, tmp_path, django_user_model):
|
|
settings.MEDIA_ROOT = tmp_path
|
|
user = django_user_model.objects.create_user(username="owner", password="pass")
|
|
conversation = Conversation.objects.create(user=user, title="会话")
|
|
client.force_login(user)
|
|
|
|
upload_response = client.post(
|
|
reverse("file_summary_attachment_upload", args=[conversation.pk]),
|
|
{
|
|
"files": [
|
|
SimpleUploadedFile("a.docx", b"a", content_type="application/docx"),
|
|
SimpleUploadedFile("b.zip", b"b", content_type="application/zip"),
|
|
]
|
|
},
|
|
)
|
|
list_response = client.get(reverse("file_summary_attachment_list", args=[conversation.pk]))
|
|
|
|
assert upload_response.status_code == 200
|
|
assert upload_response.json()["attachments"][0]["original_name"] == "a.docx"
|
|
assert len(list_response.json()["attachments"]) == 2
|
|
|
|
|
|
def test_delete_attachment_is_logical_and_scoped(client, settings, tmp_path, django_user_model):
|
|
settings.MEDIA_ROOT = tmp_path
|
|
user = django_user_model.objects.create_user(username="owner", password="pass")
|
|
conversation = Conversation.objects.create(user=user, title="会话")
|
|
attachment = FileAttachment.objects.create(
|
|
conversation=conversation,
|
|
user=user,
|
|
original_name="a.docx",
|
|
storage_path="x/a.docx",
|
|
file_size=1,
|
|
)
|
|
client.force_login(user)
|
|
|
|
response = client.delete(reverse("file_summary_attachment_detail", args=[conversation.pk, attachment.pk]))
|
|
|
|
attachment.refresh_from_db()
|
|
assert response.status_code == 200
|
|
assert attachment.upload_status == FileAttachment.UploadStatus.DELETED
|
|
assert attachment.is_active is False
|
|
|
|
|
|
def test_export_download_requires_batch_owner(client, tmp_path, django_user_model):
|
|
owner = django_user_model.objects.create_user(username="owner", password="pass")
|
|
other = django_user_model.objects.create_user(username="other", password="pass")
|
|
conversation = Conversation.objects.create(user=owner, title="会话")
|
|
batch = FileSummaryBatch.objects.create(conversation=conversation, user=owner, batch_no="FS-DL")
|
|
report_path = tmp_path / "summary.md"
|
|
report_path.write_text("ok", encoding="utf-8")
|
|
exported = ExportedSummaryFile.objects.create(
|
|
batch=batch,
|
|
export_type=ExportedSummaryFile.ExportType.MARKDOWN,
|
|
file_name="summary.md",
|
|
storage_path=str(report_path),
|
|
)
|
|
|
|
client.force_login(other)
|
|
denied = client.get(reverse("file_summary_export_download", args=[exported.pk]))
|
|
assert denied.status_code == 404
|
|
|
|
client.force_login(owner)
|
|
allowed = client.get(reverse("file_summary_export_download", args=[exported.pk]))
|
|
assert allowed.status_code == 200
|
|
assert "attachment" in allowed["Content-Disposition"]
|
|
assert "summary.md" in allowed["Content-Disposition"]
|
|
assert allowed["Content-Type"].startswith("text/markdown")
|
|
|
|
|
|
def test_export_download_checks_application_form_fill_batch_owner(client, tmp_path, django_user_model):
|
|
owner = django_user_model.objects.create_user(username="owner", password="pass")
|
|
other = django_user_model.objects.create_user(username="other", password="pass")
|
|
owner_conversation = Conversation.objects.create(user=owner, title="自动填表")
|
|
other_conversation = Conversation.objects.create(user=other, title="其他对话")
|
|
owner_summary = FileSummaryBatch.objects.create(
|
|
conversation=owner_conversation,
|
|
user=owner,
|
|
batch_no="FS-AFF-OWNER",
|
|
status=FileSummaryBatch.Status.SUCCESS,
|
|
)
|
|
other_summary = FileSummaryBatch.objects.create(
|
|
conversation=other_conversation,
|
|
user=other,
|
|
batch_no="FS-AFF-OTHER",
|
|
status=FileSummaryBatch.Status.SUCCESS,
|
|
)
|
|
form_batch = ApplicationFormFillBatch.objects.create(
|
|
conversation=owner_conversation,
|
|
user=owner,
|
|
source_summary_batch=owner_summary,
|
|
batch_no="AFF-DL",
|
|
)
|
|
report_path = tmp_path / "filled.docx"
|
|
report_path.write_bytes(b"word-content")
|
|
exported = ExportedSummaryFile.objects.create(
|
|
batch=other_summary,
|
|
workflow_type="application_form_fill",
|
|
workflow_batch_id=form_batch.pk,
|
|
export_category="filled_template",
|
|
export_type=ExportedSummaryFile.ExportType.WORD,
|
|
file_name="filled.docx",
|
|
storage_path=str(report_path),
|
|
)
|
|
|
|
client.force_login(other)
|
|
denied = client.get(reverse("file_summary_export_download", args=[exported.pk]))
|
|
assert denied.status_code == 404
|
|
|
|
client.force_login(owner)
|
|
allowed = client.get(reverse("file_summary_export_download", args=[exported.pk]))
|
|
assert allowed.status_code == 200
|
|
assert allowed["Content-Type"].startswith(
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
)
|
|
assert b"".join(allowed.streaming_content) == b"word-content"
|
|
|
|
|
|
def test_conversation_messages_returns_incremental_messages(client, django_user_model):
|
|
owner = django_user_model.objects.create_user(username="owner", password="pass")
|
|
other = django_user_model.objects.create_user(username="other", password="pass")
|
|
conversation = Conversation.objects.create(user=owner, title="会话")
|
|
first = Message.objects.create(
|
|
conversation=conversation,
|
|
role=Message.Role.USER,
|
|
content="用户消息",
|
|
)
|
|
second = Message.objects.create(
|
|
conversation=conversation,
|
|
role=Message.Role.ASSISTANT,
|
|
content="报告消息",
|
|
)
|
|
|
|
client.force_login(other)
|
|
denied = client.get(reverse("review_agent_conversation_messages", args=[conversation.pk]))
|
|
assert denied.status_code == 404
|
|
|
|
client.force_login(owner)
|
|
response = client.get(
|
|
f"{reverse('review_agent_conversation_messages', args=[conversation.pk])}?after={first.pk}"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["latest_message_id"] == second.pk
|
|
assert payload["messages"] == [
|
|
{
|
|
"id": second.pk,
|
|
"role": Message.Role.ASSISTANT,
|
|
"content": "报告消息",
|
|
"created_at": second.created_at.isoformat(),
|
|
}
|
|
]
|
|
|
|
|
|
def test_batch_status_exposes_batch_and_node_errors(client, django_user_model):
|
|
user = django_user_model.objects.create_user(username="owner", password="pass")
|
|
conversation = Conversation.objects.create(user=user, title="会话")
|
|
batch = FileSummaryBatch.objects.create(
|
|
conversation=conversation,
|
|
user=user,
|
|
batch_no="FS-ERR",
|
|
status=FileSummaryBatch.Status.FAILED,
|
|
error_message="压缩包解压失败",
|
|
)
|
|
WorkflowNodeRun.objects.create(
|
|
batch=batch,
|
|
node_code="extract",
|
|
node_name="压缩包解压",
|
|
status=WorkflowNodeRun.Status.FAILED,
|
|
progress=10,
|
|
message="未解出任何可扫描文件",
|
|
)
|
|
client.force_login(user)
|
|
|
|
response = client.get(reverse("file_summary_batch_status", args=[batch.pk]))
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["batch"]["error_message"] == "压缩包解压失败"
|
|
assert payload["nodes"][0]["message"] == "未解出任何可扫描文件"
|
|
|
|
|
|
def test_conversation_list_api_returns_owned_conversations_with_attachment_counts(client, django_user_model):
|
|
owner = django_user_model.objects.create_user(username="owner", password="pass")
|
|
other = django_user_model.objects.create_user(username="other", password="pass")
|
|
owned = Conversation.objects.create(user=owner, title="有附件会话")
|
|
Conversation.objects.create(user=other, title="其他用户会话")
|
|
FileAttachment.objects.create(
|
|
conversation=owned,
|
|
user=owner,
|
|
original_name="a.docx",
|
|
storage_path="x/a.docx",
|
|
file_size=1,
|
|
)
|
|
FileAttachment.objects.create(
|
|
conversation=owned,
|
|
user=owner,
|
|
original_name="deleted.docx",
|
|
storage_path="x/deleted.docx",
|
|
file_size=1,
|
|
upload_status=FileAttachment.UploadStatus.DELETED,
|
|
is_active=False,
|
|
)
|
|
client.force_login(owner)
|
|
|
|
response = client.get(reverse("review_agent_conversation_list"))
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert [item["title"] for item in payload["conversations"]] == ["有附件会话"]
|
|
assert payload["conversations"][0]["attachment_count"] == 1
|
|
|
|
|
|
def test_patch_attachment_updates_name_and_active_state(client, django_user_model):
|
|
user = django_user_model.objects.create_user(username="owner", password="pass")
|
|
conversation = Conversation.objects.create(user=user, title="会话")
|
|
attachment = FileAttachment.objects.create(
|
|
conversation=conversation,
|
|
user=user,
|
|
original_name="old.docx",
|
|
storage_path="x/old.docx",
|
|
file_size=1,
|
|
is_active=True,
|
|
)
|
|
client.force_login(user)
|
|
|
|
response = client.patch(
|
|
reverse("file_summary_attachment_detail", args=[conversation.pk, attachment.pk]),
|
|
data=json.dumps({"original_name": "new.docx", "is_active": False}),
|
|
content_type="application/json",
|
|
)
|
|
|
|
attachment.refresh_from_db()
|
|
assert response.status_code == 200
|
|
assert attachment.original_name == "new.docx"
|
|
assert attachment.is_active is False
|
|
assert response.json()["attachment"]["original_name"] == "new.docx"
|
|
|
|
|
|
def test_attachment_download_requires_owner_and_returns_file(client, settings, tmp_path, django_user_model):
|
|
settings.MEDIA_ROOT = tmp_path
|
|
owner = django_user_model.objects.create_user(username="owner", password="pass")
|
|
other = django_user_model.objects.create_user(username="other", password="pass")
|
|
conversation = Conversation.objects.create(user=owner, title="会话")
|
|
attachment_path = tmp_path / "uploads" / "a.docx"
|
|
attachment_path.parent.mkdir(parents=True)
|
|
attachment_path.write_bytes(b"attachment-content")
|
|
attachment = FileAttachment.objects.create(
|
|
conversation=conversation,
|
|
user=owner,
|
|
original_name="a.docx",
|
|
storage_path=str(attachment_path),
|
|
file_size=attachment_path.stat().st_size,
|
|
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
)
|
|
|
|
client.force_login(other)
|
|
denied = client.get(reverse("file_summary_attachment_download", args=[conversation.pk, attachment.pk]))
|
|
assert denied.status_code == 404
|
|
|
|
client.force_login(owner)
|
|
allowed = client.get(reverse("file_summary_attachment_download", args=[conversation.pk, attachment.pk]))
|
|
assert allowed.status_code == 200
|
|
assert "attachment" in allowed["Content-Disposition"]
|
|
assert "a.docx" in allowed["Content-Disposition"]
|
|
assert b"".join(allowed.streaming_content) == b"attachment-content"
|