Files
replypier/backend/tests/services/test_chat_notifications.py
T
WangShen007 ef9fee1585 feat(chat): Socket.IO 双端通道、消息持久化与幂等
访客与客服分别鉴权接入,消息带客户端 ID 去重,重发只产生一条业务记录。
2026-09-26 22:20:00 +08:00

254 lines
8.8 KiB
Python

"""
Copyright 2024-2026 ReplyPier
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Chat notification preference gating: who gets a push for which event, and
what happens for users who have never opened the settings page.
"""
from unittest.mock import AsyncMock, patch
from uuid import uuid4
import pytest
from app.models.notification import Notification, NotificationType
from app.models.notification_settings import UserNotificationSettings
from app.models.permission import Permission
from app.models.role import Role
from app.models.user import User
from app.repositories.chat import ChatRepository
from app.repositories.session_to_agent import SessionToAgentRepository
from app.services.chat_notifications import notify_chat_assigned, notify_new_chat
from app.services.notifications import ChatNotificationEvent, notify_chat_event
@pytest.fixture
def second_user(db, test_organization, test_role) -> User:
user = User(
id=uuid4(),
email="second@example.com",
full_name="Second User",
hashed_password="hashed_password",
is_active=True,
organization_id=test_organization.id,
role_id=test_role.id,
)
db.add(user)
db.commit()
return user
async def _notify(db, user_ids, event):
with patch("app.services.notifications.send_fcm_notification", new=AsyncMock()):
await notify_chat_event(
db=db,
user_ids=user_ids,
event=event,
title="New Chat Transfer",
message="A chat has been transferred to your group.",
metadata={"session_id": "abc-123"},
)
def _notifications_for(db, user_id):
return db.query(Notification).filter(Notification.user_id == user_id).all()
@pytest.mark.asyncio
async def test_users_without_settings_get_default_enabled_events(db, test_user):
"""No settings row means the defaults apply — transfers still notify."""
await _notify(db, [test_user.id], ChatNotificationEvent.CHAT_TRANSFER)
notifications = _notifications_for(db, test_user.id)
assert len(notifications) == 1
assert notifications[0].type == NotificationType.CHAT
assert notifications[0].notification_metadata["event"] == "notify_chat_transfer"
# session_id must survive — the push uses it to deep-link the conversation.
assert notifications[0].notification_metadata["session_id"] == "abc-123"
@pytest.mark.asyncio
async def test_users_without_settings_do_not_get_default_disabled_events(db, test_user):
"""New-chat is opt-in, so an untouched user gets nothing."""
await _notify(db, [test_user.id], ChatNotificationEvent.NEW_CHAT)
assert _notifications_for(db, test_user.id) == []
@pytest.mark.asyncio
async def test_muted_user_is_skipped_others_are_not(db, test_user, second_user):
db.add(UserNotificationSettings(user_id=test_user.id, notify_chat_transfer=False))
db.commit()
await _notify(db, [test_user.id, second_user.id], ChatNotificationEvent.CHAT_TRANSFER)
assert _notifications_for(db, test_user.id) == []
assert len(_notifications_for(db, second_user.id)) == 1
@pytest.mark.asyncio
async def test_opted_in_user_gets_new_chat(db, test_user):
db.add(UserNotificationSettings(user_id=test_user.id, notify_new_chat=True))
db.commit()
await _notify(db, [test_user.id], ChatNotificationEvent.NEW_CHAT)
assert len(_notifications_for(db, test_user.id)) == 1
@pytest.mark.asyncio
async def test_duplicate_recipients_notified_once(db, test_user):
await _notify(
db,
[test_user.id, test_user.id, str(test_user.id)],
ChatNotificationEvent.CHAT_TRANSFER,
)
assert len(_notifications_for(db, test_user.id)) == 1
@pytest.mark.asyncio
async def test_no_recipients_is_a_no_op(db):
await _notify(db, [], ChatNotificationEvent.CHAT_TRANSFER)
assert db.query(Notification).count() == 0
@pytest.fixture
def inbox_user(db, test_organization) -> User:
"""A user whose role can see unclaimed chats, opted in to new-chat pushes."""
permission = Permission(name="view_unassigned_chats")
db.add(permission)
db.commit()
role = Role(name="Inbox Agent", organization_id=test_organization.id)
role.permissions = [permission]
db.add(role)
db.commit()
user = User(
id=uuid4(),
email="inbox@example.com",
full_name="Inbox Agent",
hashed_password="hashed_password",
is_active=True,
organization_id=test_organization.id,
role_id=role.id,
)
db.add(user)
db.add(UserNotificationSettings(user_id=user.id, notify_new_chat=True))
db.commit()
return user
def _new_session(db, test_organization, test_agent, test_customer):
return SessionToAgentRepository(db).create_session(
session_id=uuid4(),
agent_id=test_agent.id,
customer_id=test_customer.id,
organization_id=test_organization.id,
)
@pytest.mark.asyncio
async def test_new_chat_notifies_only_users_who_can_open_it(
db, test_organization, test_agent, test_customer, test_user, inbox_user
):
"""test_user's role has no chat permissions, so it isn't a recipient."""
session = _new_session(db, test_organization, test_agent, test_customer)
with patch("app.services.notifications.send_fcm_notification", new=AsyncMock()):
await notify_new_chat(db, session)
assert len(_notifications_for(db, inbox_user.id)) == 1
assert _notifications_for(db, test_user.id) == []
notification = _notifications_for(db, inbox_user.id)[0]
assert notification.notification_metadata["session_id"] == str(session.session_id)
assert "web widget" in notification.message
@pytest.mark.asyncio
async def test_new_chat_respects_the_opt_in(
db, test_organization, test_agent, test_customer, inbox_user
):
db.query(UserNotificationSettings)\
.filter(UserNotificationSettings.user_id == inbox_user.id)\
.update({"notify_new_chat": False})
db.commit()
session = _new_session(db, test_organization, test_agent, test_customer)
with patch("app.services.notifications.send_fcm_notification", new=AsyncMock()):
await notify_new_chat(db, session)
assert _notifications_for(db, inbox_user.id) == []
@pytest.mark.asyncio
async def test_opening_the_widget_alone_is_not_a_new_chat(
db, test_organization, test_agent, test_customer
):
"""The widget opens its session on connect — no customer message, no chat."""
session = _new_session(db, test_organization, test_agent, test_customer)
chat_repo = ChatRepository(db)
assert chat_repo.has_customer_messages(session.session_id) is False
chat_repo.create_message({
"message": "Hello?",
"message_type": "user",
"session_id": str(session.session_id),
"organization_id": str(test_organization.id),
"agent_id": str(test_agent.id),
"customer_id": str(test_customer.id),
})
assert chat_repo.has_customer_messages(session.session_id) is True
# A bot-only session still counts as unstarted.
other = _new_session(db, test_organization, test_agent, test_customer)
chat_repo.create_message({
"message": "Hi there!",
"message_type": "bot",
"session_id": str(other.session_id),
"organization_id": str(test_organization.id),
"agent_id": str(test_agent.id),
"customer_id": str(test_customer.id),
})
assert chat_repo.has_customer_messages(other.session_id) is False
@pytest.mark.asyncio
async def test_assignment_notifies_the_target(
db, test_organization, test_agent, test_customer, test_user, second_user
):
session = _new_session(db, test_organization, test_agent, test_customer)
with patch("app.services.notifications.send_fcm_notification", new=AsyncMock()):
await notify_chat_assigned(db, session, second_user.id, assigned_by=test_user.id)
assert len(_notifications_for(db, second_user.id)) == 1
assert _notifications_for(db, test_user.id) == []
@pytest.mark.asyncio
async def test_self_assignment_notifies_nobody(
db, test_organization, test_agent, test_customer, test_user
):
"""Claiming a chat yourself shouldn't push a notification back at you."""
session = _new_session(db, test_organization, test_agent, test_customer)
with patch("app.services.notifications.send_fcm_notification", new=AsyncMock()):
await notify_chat_assigned(db, session, test_user.id, assigned_by=test_user.id)
assert _notifications_for(db, test_user.id) == []