dify/api/tests/unit_tests/services/test_feedback_service.py
Asuka Minato c17a0f4e18
test: move feedback service coverage to unit tests (#38927)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-15 05:42:12 +00:00

245 lines
9.6 KiB
Python

"""Unit tests for feedback exports using SQLite-backed ORM rows.
The export query joins five tables and applies its filters in SQL, so these tests
use a real SQLite session instead of stubbing ``Session.execute`` results.
"""
import json
from datetime import datetime
from decimal import Decimal
from typing import TypedDict
import pytest
from sqlalchemy.orm import Session
from models.account import Account
from models.enums import FeedbackFromSource, FeedbackRating
from models.model import App, AppMode, Conversation, ConversationFromSource, IconType, Message, MessageFeedback
from services.feedback_service import FeedbackService
APP_ID = "11111111-1111-1111-1111-111111111111"
TENANT_ID = "22222222-2222-2222-2222-222222222222"
CONVERSATION_ID = "33333333-3333-3333-3333-333333333333"
MESSAGE_ID = "44444444-4444-4444-4444-444444444444"
ACCOUNT_ID = "55555555-5555-5555-5555-555555555555"
END_USER_ID = "66666666-6666-6666-6666-666666666666"
class FeedbackSample(TypedDict):
app: App
conversation: Conversation
message: Message
user_feedback: MessageFeedback
admin_feedback: MessageFeedback
@pytest.fixture
def sample_data(sqlite_session: Session) -> FeedbackSample:
"""Persist a complete user/admin feedback graph for export queries."""
app = App(
tenant_id=TENANT_ID,
name="Test App",
description="",
mode=AppMode.CHAT,
icon_type=IconType.EMOJI,
icon="🤖",
icon_background="#FFFFFF",
enable_site=False,
enable_api=False,
max_active_requests=0,
)
app.id = APP_ID
conversation = Conversation(
app_id=APP_ID,
mode=AppMode.CHAT,
name="Test Conversation",
from_source=ConversationFromSource.API,
from_end_user_id=END_USER_ID,
)
conversation.id = CONVERSATION_ID
conversation._inputs = {}
message = Message(
app_id=APP_ID,
conversation_id=CONVERSATION_ID,
query="What is AI?",
message={"role": "user", "content": "What is AI?"},
answer="AI is artificial intelligence.",
message_unit_price=Decimal(0),
answer_unit_price=Decimal(0),
currency="USD",
from_source=ConversationFromSource.API,
from_end_user_id=END_USER_ID,
created_at=datetime(2024, 1, 1, 10, 0, 0),
)
message.id = MESSAGE_ID
message._inputs = {"query": "What is AI?"}
account = Account(name="Admin User", email="admin@example.com")
account.id = ACCOUNT_ID
user_feedback = MessageFeedback(
app_id=APP_ID,
conversation_id=CONVERSATION_ID,
message_id=MESSAGE_ID,
rating=FeedbackRating.LIKE,
from_source=FeedbackFromSource.USER,
content="Great answer!",
from_end_user_id=END_USER_ID,
)
user_feedback.created_at = datetime(2024, 1, 1, 10, 5, 0)
admin_feedback = MessageFeedback(
app_id=APP_ID,
conversation_id=CONVERSATION_ID,
message_id=MESSAGE_ID,
rating=FeedbackRating.DISLIKE,
from_source=FeedbackFromSource.ADMIN,
content="Could be more detailed",
from_account_id=ACCOUNT_ID,
)
admin_feedback.created_at = datetime(2024, 1, 1, 10, 10, 0)
sqlite_session.add_all([app, conversation, message, account, user_feedback, admin_feedback])
sqlite_session.commit()
return {
"app": app,
"conversation": conversation,
"message": message,
"user_feedback": user_feedback,
"admin_feedback": admin_feedback,
}
@pytest.mark.parametrize(
"sqlite_session",
[(App, Conversation, Message, MessageFeedback, Account)],
indirect=True,
)
class TestFeedbackService:
"""Exercise feedback export formatting and its database-side filters."""
def test_export_feedbacks_csv_format(self, sqlite_session: Session, sample_data: FeedbackSample) -> None:
result = FeedbackService.export_feedbacks(
app_id=APP_ID,
session=sqlite_session,
from_source=FeedbackFromSource.USER,
format_type="csv",
)
assert "text/csv" in result.headers["Content-Type"]
assert "attachment" in result.headers["Content-Disposition"]
csv_content = result.get_data(as_text=True)
assert "feedback_id" in csv_content
assert "app_name" in csv_content
assert "conversation_id" in csv_content
assert sample_data["app"].name in csv_content
assert sample_data["message"].query in csv_content
assert sample_data["user_feedback"].content in csv_content
assert sample_data["admin_feedback"].content not in csv_content
def test_export_feedbacks_json_format(self, sqlite_session: Session, sample_data: FeedbackSample) -> None:
result = FeedbackService.export_feedbacks(
app_id=APP_ID,
session=sqlite_session,
from_source=FeedbackFromSource.ADMIN,
format_type="json",
)
assert "application/json" in result.headers["Content-Type"]
assert "attachment" in result.headers["Content-Disposition"]
json_content = json.loads(result.get_data(as_text=True))
assert json_content["export_info"]["app_id"] == APP_ID
assert json_content["export_info"]["total_records"] == 1
assert json_content["feedback_data"][0]["from_account_name"] == "Admin User"
def test_export_feedbacks_with_filters(self, sqlite_session: Session, sample_data: FeedbackSample) -> None:
result = FeedbackService.export_feedbacks(
app_id=APP_ID,
session=sqlite_session,
from_source=FeedbackFromSource.ADMIN,
rating=FeedbackRating.DISLIKE,
has_comment=True,
start_date="2024-01-01",
end_date="2024-12-31",
format_type="csv",
)
csv_content = result.get_data(as_text=True)
assert sample_data["admin_feedback"].content in csv_content
assert sample_data["user_feedback"].content not in csv_content
assert len([line for line in csv_content.strip().splitlines() if line.strip()]) == 2
def test_export_feedbacks_no_data(self, sqlite_session: Session, sample_data: FeedbackSample) -> None:
result = FeedbackService.export_feedbacks(
app_id="77777777-7777-7777-7777-777777777777",
session=sqlite_session,
format_type="csv",
)
csv_content = result.get_data(as_text=True)
assert "text/csv" in result.headers["Content-Type"]
assert "feedback_id" in csv_content
assert "app_name" in csv_content
assert "conversation_id" in csv_content
assert len([line for line in csv_content.strip().splitlines() if line.strip()]) == 1
def test_export_feedbacks_invalid_date_format(self, sqlite_session: Session, sample_data: FeedbackSample) -> None:
with pytest.raises(ValueError, match="Invalid start_date format"):
FeedbackService.export_feedbacks(
app_id=APP_ID,
session=sqlite_session,
start_date="invalid-date-format",
)
with pytest.raises(ValueError, match="Invalid end_date format"):
FeedbackService.export_feedbacks(
app_id=APP_ID,
session=sqlite_session,
end_date="invalid-date-format",
)
def test_export_feedbacks_invalid_format(self, sqlite_session: Session, sample_data: FeedbackSample) -> None:
with pytest.raises(ValueError, match="Unsupported format"):
FeedbackService.export_feedbacks(app_id=APP_ID, session=sqlite_session, format_type="xml")
def test_export_feedbacks_long_response_truncation(
self, sqlite_session: Session, sample_data: FeedbackSample
) -> None:
sample_data["message"].answer = "A" * 600
sqlite_session.commit()
result = FeedbackService.export_feedbacks(
app_id=APP_ID,
session=sqlite_session,
from_source=FeedbackFromSource.USER,
format_type="json",
)
exported_answer = json.loads(result.get_data(as_text=True))["feedback_data"][0]["ai_response"]
assert exported_answer == "A" * 500 + "..."
def test_export_feedbacks_unicode_content(self, sqlite_session: Session, sample_data: FeedbackSample) -> None:
sample_data["message"].query = "什么是人工智能?"
sample_data["message"].answer = "人工智能是模拟人类智能的技术。"
sample_data["user_feedback"].content = "回答不够详细,需要更多信息"
sqlite_session.commit()
result = FeedbackService.export_feedbacks(
app_id=APP_ID,
session=sqlite_session,
from_source=FeedbackFromSource.USER,
format_type="csv",
)
csv_content = result.get_data(as_text=True)
assert "什么是人工智能?" in csv_content
assert "回答不够详细,需要更多信息" in csv_content
assert "人工智能是模拟人类智能的技术" in csv_content
def test_export_feedbacks_emoji_ratings(self, sqlite_session: Session, sample_data: FeedbackSample) -> None:
result = FeedbackService.export_feedbacks(app_id=APP_ID, session=sqlite_session, format_type="json")
feedback_data = json.loads(result.get_data(as_text=True))["feedback_data"]
assert len(feedback_data) == 2
like_feedback = next(item for item in feedback_data if item["feedback_rating_raw"] == "like")
dislike_feedback = next(item for item in feedback_data if item["feedback_rating_raw"] == "dislike")
assert like_feedback["feedback_rating"] == "👍"
assert dislike_feedback["feedback_rating"] == "👎"