test: 补充知识处理、检索与 Agent 回归用例
覆盖切块、向量检索、工具调用和历史预算;模型相关用例使用桩。
This commit is contained in:
1 parent
2dd062787d
commit
d9fe727c3f
123 files changed
+29005
No files matched your search
@@ -0,0 +1,407 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock, AsyncMock
|
||||
from app.core.auth_utils import (
|
||||
refresh_access_token,
|
||||
authenticate_socket,
|
||||
authenticate_socket_conversation_token,
|
||||
widget_socket_identity,
|
||||
)
|
||||
from app.models.user import User
|
||||
from app.models.widget import Widget
|
||||
from uuid import uuid4
|
||||
import http.cookies
|
||||
from datetime import datetime, timezone
|
||||
from app.core.security import verify_token, verify_conversation_token, create_access_token, create_refresh_token, create_conversation_token
|
||||
from app.models.auth_session import AuthSession
|
||||
from app.services.auth_sessions import issue_tokens, revoke_session
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db():
|
||||
"""Create a mock database session"""
|
||||
db = MagicMock()
|
||||
return db
|
||||
|
||||
@pytest.fixture
|
||||
def mock_user():
|
||||
"""Create a mock user"""
|
||||
user_id = uuid4()
|
||||
org_id = uuid4()
|
||||
return User(
|
||||
id=user_id,
|
||||
email="test@example.com",
|
||||
hashed_password="hashed_password",
|
||||
is_active=True,
|
||||
organization_id=org_id,
|
||||
full_name="Test User"
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_widget():
|
||||
"""Create a mock widget"""
|
||||
widget_id = uuid4()
|
||||
org_id = uuid4()
|
||||
return Widget(
|
||||
id=str(widget_id),
|
||||
organization_id=org_id,
|
||||
name="Test Widget"
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def live_user(db, test_organization):
|
||||
"""A real, active user — session checks run against the database."""
|
||||
user = User(
|
||||
id=uuid4(),
|
||||
email="socket@example.com",
|
||||
hashed_password="hashed_password",
|
||||
is_active=True,
|
||||
organization_id=test_organization.id,
|
||||
full_name="Socket User",
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def socket_db(db):
|
||||
"""Point authenticate_socket's SessionLocal() at the test database."""
|
||||
with patch('app.core.auth_utils.SessionLocal') as mock_session_local:
|
||||
mock_session_local.return_value.__enter__.return_value = db
|
||||
yield db
|
||||
|
||||
|
||||
def _socket_environ(**cookies):
|
||||
# A same-origin handshake: only our own pages may sign in with the cookies
|
||||
jar = http.cookies.SimpleCookie()
|
||||
for name, value in cookies.items():
|
||||
jar[name] = value
|
||||
return {'HTTP_COOKIE': jar.output(header='', sep=';'), 'HTTP_SEC_FETCH_SITE': 'same-origin'}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_access_token_success(db, live_user):
|
||||
"""A live refresh token re-mints an access token for the same session"""
|
||||
_, refresh_token = issue_tokens(db, live_user.id, live_user.organization_id)
|
||||
db.commit()
|
||||
|
||||
result = await refresh_access_token(refresh_token, db)
|
||||
|
||||
payload = verify_token(result)
|
||||
assert payload["type"] == "access"
|
||||
assert payload["sub"] == str(live_user.id)
|
||||
assert payload["sid"] == verify_token(refresh_token)["sid"]
|
||||
assert db.query(AuthSession).count() == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_access_token_invalid_token(db):
|
||||
"""Test access token refresh with invalid token"""
|
||||
assert await refresh_access_token("invalid_token", db) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_access_token_inactive_user(db, live_user):
|
||||
"""Test access token refresh with inactive user"""
|
||||
_, refresh_token = issue_tokens(db, live_user.id, live_user.organization_id)
|
||||
live_user.is_active = False
|
||||
db.commit()
|
||||
|
||||
assert await refresh_access_token(refresh_token, db) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_access_token_rejects_revoked_session(db, live_user):
|
||||
"""A refresh token from a signed-out session mints nothing"""
|
||||
_, refresh_token = issue_tokens(db, live_user.id, live_user.organization_id)
|
||||
revoke_session(db, verify_token(refresh_token)["sid"])
|
||||
db.commit()
|
||||
|
||||
assert await refresh_access_token(refresh_token, db) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_access_token_rejects_an_access_token(db, live_user):
|
||||
"""Only a refresh token can be refreshed"""
|
||||
access_token, _ = issue_tokens(db, live_user.id, live_user.organization_id)
|
||||
db.commit()
|
||||
|
||||
assert await refresh_access_token(access_token, db) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_access_token_migrates_a_legacy_token(db, live_user):
|
||||
"""A refresh token from before sessions still works, and gets a session"""
|
||||
legacy = create_refresh_token({"sub": str(live_user.id), "org": str(live_user.organization_id)})
|
||||
|
||||
result = await refresh_access_token(legacy, db)
|
||||
|
||||
sid = verify_token(result)["sid"]
|
||||
session = db.query(AuthSession).one()
|
||||
assert str(session.id) == sid
|
||||
assert session.user_id == live_user.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_socket_success(socket_db, live_user):
|
||||
"""Test successful socket authentication"""
|
||||
access_token, _ = issue_tokens(socket_db, live_user.id, live_user.organization_id)
|
||||
socket_db.commit()
|
||||
|
||||
result = await authenticate_socket("test_sid", _socket_environ(access_token=access_token))
|
||||
|
||||
assert result == (access_token, str(live_user.id), str(live_user.organization_id))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_socket_with_refresh(socket_db, live_user):
|
||||
"""Test socket authentication with token refresh"""
|
||||
sid = "test_sid"
|
||||
_, refresh_token = issue_tokens(socket_db, live_user.id, live_user.organization_id)
|
||||
socket_db.commit()
|
||||
|
||||
result_token, result_user_id, result_org_id = await authenticate_socket(
|
||||
sid, _socket_environ(refresh_token=refresh_token))
|
||||
|
||||
assert verify_token(result_token)["type"] == "access"
|
||||
assert result_user_id == str(live_user.id)
|
||||
assert result_org_id == str(live_user.organization_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_socket_rejects_a_revoked_session(socket_db, live_user):
|
||||
"""A token copied before sign-out cannot open a socket"""
|
||||
access_token, _ = issue_tokens(socket_db, live_user.id, live_user.organization_id)
|
||||
revoke_session(socket_db, verify_token(access_token)["sid"])
|
||||
socket_db.commit()
|
||||
|
||||
result = await authenticate_socket("test_sid", _socket_environ(access_token=access_token))
|
||||
|
||||
assert result == (None, None, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_socket_rejects_a_refresh_token(socket_db, live_user):
|
||||
"""A refresh token in the access_token cookie is the wrong type"""
|
||||
_, refresh_token = issue_tokens(socket_db, live_user.id, live_user.organization_id)
|
||||
socket_db.commit()
|
||||
|
||||
result = await authenticate_socket("test_sid", _socket_environ(access_token=refresh_token))
|
||||
|
||||
assert result == (None, None, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_socket_rejects_an_inactive_user(socket_db, live_user):
|
||||
"""A deactivated user's still-unexpired token cannot open a socket"""
|
||||
access_token, _ = issue_tokens(socket_db, live_user.id, live_user.organization_id)
|
||||
live_user.is_active = False
|
||||
socket_db.commit()
|
||||
|
||||
result = await authenticate_socket("test_sid", _socket_environ(access_token=access_token))
|
||||
|
||||
assert result == (None, None, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_socket_rejects_a_deleted_user(socket_db):
|
||||
"""A legacy token for a user who no longer exists cannot open a socket"""
|
||||
access_token = create_access_token({"sub": str(uuid4()), "org": str(uuid4())})
|
||||
|
||||
result = await authenticate_socket("test_sid", _socket_environ(access_token=access_token))
|
||||
|
||||
assert result == (None, None, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_socket_conversation_token_success(mock_db, mock_widget):
|
||||
"""Test successful widget socket authentication"""
|
||||
# Setup
|
||||
sid = "test_sid"
|
||||
conversation_token = "valid_conversation_token"
|
||||
customer_id = str(uuid4())
|
||||
|
||||
# Create auth data
|
||||
auth = {'conversation_token': conversation_token}
|
||||
|
||||
mock_db.query.return_value.filter.return_value.first.return_value = mock_widget
|
||||
|
||||
# No merged-into pointer for this customer: it resolves to itself.
|
||||
with patch('app.core.auth_utils.verify_conversation_token') as mock_verify, \
|
||||
patch('app.core.auth_utils.SessionLocal') as mock_session_local, \
|
||||
patch('app.repositories.customer.CustomerRepository.resolve_merged',
|
||||
return_value=MagicMock(id=customer_id)):
|
||||
mock_verify.return_value = {
|
||||
"widget_id": str(mock_widget.id),
|
||||
"sub": customer_id,
|
||||
"type": "conversation"
|
||||
}
|
||||
mock_session_local.return_value.__enter__.return_value = mock_db
|
||||
|
||||
# Execute
|
||||
result_widget_id, result_org_id, result_customer_id, result_token = await authenticate_socket_conversation_token(sid, auth)
|
||||
|
||||
# Assert
|
||||
assert result_widget_id == str(mock_widget.id)
|
||||
assert result_org_id == mock_widget.organization_id
|
||||
assert result_customer_id == customer_id
|
||||
assert result_token == conversation_token
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_socket_conversation_token_invalid(mock_db):
|
||||
"""Test widget socket authentication with invalid token"""
|
||||
# Setup
|
||||
sid = "test_sid"
|
||||
conversation_token = "invalid_token"
|
||||
|
||||
# Create auth data
|
||||
auth = {'conversation_token': conversation_token}
|
||||
|
||||
with patch('app.core.auth_utils.verify_conversation_token') as mock_verify:
|
||||
mock_verify.return_value = None
|
||||
|
||||
# Execute
|
||||
result = await authenticate_socket_conversation_token(sid, auth)
|
||||
|
||||
# Assert
|
||||
assert result == (None, None, None, None)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_socket_conversation_token_missing_widget(mock_db):
|
||||
"""Test widget socket authentication with missing widget"""
|
||||
# Setup
|
||||
sid = "test_sid"
|
||||
conversation_token = "valid_conversation_token"
|
||||
customer_id = str(uuid4())
|
||||
widget_id = str(uuid4())
|
||||
|
||||
# Create auth data
|
||||
auth = {'conversation_token': conversation_token}
|
||||
|
||||
mock_db.query.return_value.filter.return_value.first.return_value = None
|
||||
|
||||
with patch('app.core.auth_utils.verify_conversation_token') as mock_verify, \
|
||||
patch('app.core.auth_utils.get_db') as mock_get_db:
|
||||
mock_verify.return_value = {
|
||||
"widget_id": widget_id,
|
||||
"sub": customer_id,
|
||||
"type": "conversation"
|
||||
}
|
||||
mock_get_db.return_value.__next__.return_value = mock_db
|
||||
|
||||
# Execute
|
||||
result = await authenticate_socket_conversation_token(sid, auth)
|
||||
|
||||
# Assert
|
||||
assert result == (None, None, None, None)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_socket_conversation_token_wrong_type(mock_db, mock_widget):
|
||||
"""Test widget socket authentication with wrong token type"""
|
||||
# Setup
|
||||
sid = "test_sid"
|
||||
conversation_token = "wrong_type_token"
|
||||
customer_id = str(uuid4())
|
||||
|
||||
# Create auth data
|
||||
auth = {'conversation_token': conversation_token}
|
||||
|
||||
mock_db.query.return_value.filter.return_value.first.return_value = mock_widget
|
||||
|
||||
with patch('app.core.auth_utils.verify_conversation_token') as mock_verify, \
|
||||
patch('app.core.auth_utils.get_db') as mock_get_db:
|
||||
mock_verify.return_value = {
|
||||
"widget_id": str(mock_widget.id),
|
||||
"sub": customer_id,
|
||||
"type": "access" # Wrong token type
|
||||
}
|
||||
mock_get_db.return_value.__next__.return_value = mock_db
|
||||
|
||||
# Execute
|
||||
result = await authenticate_socket_conversation_token(sid, auth)
|
||||
|
||||
# Assert
|
||||
assert result == (None, None, None, None)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_socket_no_cookies(mock_db):
|
||||
"""Test socket authentication with no cookies"""
|
||||
# Setup
|
||||
sid = "test_sid"
|
||||
environ = {}
|
||||
|
||||
# Execute
|
||||
result_token, result_user_id, result_org_id = await authenticate_socket(sid, environ)
|
||||
|
||||
# Assert
|
||||
assert result_token is None
|
||||
assert result_user_id is None
|
||||
assert result_org_id is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_socket_invalid_token_format(mock_db):
|
||||
"""Test socket authentication with invalid token format"""
|
||||
# Setup
|
||||
sid = "test_sid"
|
||||
access_token = "invalid_format_token"
|
||||
|
||||
# Create mock environ with cookies
|
||||
environ = _socket_environ(access_token=access_token)
|
||||
|
||||
with patch('app.core.auth_utils.live_payload') as mock_verify:
|
||||
mock_verify.side_effect = Exception("Invalid token format")
|
||||
|
||||
# Execute
|
||||
result_token, result_user_id, result_org_id = await authenticate_socket(sid, environ)
|
||||
|
||||
# Assert
|
||||
assert result_token is None
|
||||
assert result_user_id is None
|
||||
assert result_org_id is None
|
||||
|
||||
|
||||
def test_widget_socket_identity_reads_the_connected_session():
|
||||
"""A live connection's identity comes from what connect resolved, not from
|
||||
re-verifying the token on every event (#315)."""
|
||||
session = {
|
||||
"widget_id": "widget-1",
|
||||
"org_id": "org-1",
|
||||
"customer_id": "customer-1",
|
||||
"conversation_token": "long-since-expired",
|
||||
}
|
||||
|
||||
assert widget_socket_identity(session) == ("widget-1", "org-1", "customer-1")
|
||||
|
||||
|
||||
def test_widget_socket_identity_allows_an_anonymous_customer():
|
||||
"""A visitor who has not been identified still has a widget and an org."""
|
||||
session = {"widget_id": "widget-1", "org_id": "org-1"}
|
||||
|
||||
assert widget_socket_identity(session) == ("widget-1", "org-1", None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("session", [
|
||||
None,
|
||||
{},
|
||||
{"widget_id": "widget-1"},
|
||||
{"org_id": "org-1"},
|
||||
])
|
||||
def test_widget_socket_identity_rejects_an_unauthenticated_socket(session):
|
||||
"""No session means connect never authenticated this sid."""
|
||||
assert widget_socket_identity(session) == (None, None, None)
|
||||
Reference in new issue
Block a user