test: 补充权限、消息与会话服务回归用例
会话读取、接管、重分配与 Socket 订阅统一校验角色、组织与可见性。
This commit is contained in:
1 parent
5da5f910da
commit
2554eb7bc7
97 files changed
+37281
No files matched your search
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Copyright 2024-2026 ChatterMate
|
||||
|
||||
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.
|
||||
|
||||
POST /widgets/{widget_id}/end-chat — the endpoint the widget calls to close a
|
||||
conversation. It shipped untested, and a client bug calling it on every message
|
||||
closed live chats the moment a human agent joined.
|
||||
"""
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.api import widget as widget_router
|
||||
from app.core.security import create_conversation_token
|
||||
from app.database import get_db
|
||||
from app.models.session_to_agent import SessionStatus, SessionToAgent
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(db) -> TestClient:
|
||||
app = FastAPI()
|
||||
app.include_router(widget_router.router, prefix="/api/v1/widgets")
|
||||
|
||||
def override_get_db():
|
||||
yield db
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
BROWSER_KEY = "browser-a"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def widget_token(test_widget, test_customer) -> str:
|
||||
"""The token the widget issued to the browser that opened the session."""
|
||||
return create_conversation_token(
|
||||
widget_id=str(test_widget.id), customer_id=str(test_customer.id), ckey=BROWSER_KEY
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def open_session(db, test_widget, test_customer, test_agent) -> SessionToAgent:
|
||||
session = SessionToAgent(
|
||||
session_id=uuid4(),
|
||||
customer_id=test_customer.id,
|
||||
agent_id=test_agent.id,
|
||||
organization_id=test_customer.organization_id,
|
||||
status=SessionStatus.OPEN,
|
||||
conversation_key=BROWSER_KEY,
|
||||
)
|
||||
db.add(session)
|
||||
db.commit()
|
||||
db.refresh(session)
|
||||
return session
|
||||
|
||||
|
||||
def _url(widget_id, session_id, **params) -> str:
|
||||
query = "".join(f"&{k}={v}" for k, v in params.items())
|
||||
return f"/api/v1/widgets/{widget_id}/end-chat?session_id={session_id}{query}"
|
||||
|
||||
|
||||
def _auth(token: str) -> dict:
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
def test_closes_the_session(client, db, test_widget, open_session, widget_token):
|
||||
response = client.post(
|
||||
_url(test_widget.id, open_session.session_id, reason="CUSTOMER_REQUEST"),
|
||||
headers=_auth(widget_token),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["success"] is True
|
||||
|
||||
db.refresh(open_session)
|
||||
assert open_session.status == SessionStatus.CLOSED
|
||||
|
||||
|
||||
def test_requires_a_token(client, db, test_widget, open_session):
|
||||
"""Regression: the token block used to be optional, so a request with no
|
||||
Authorization header fell through and closed any session by id."""
|
||||
response = client.post(_url(test_widget.id, open_session.session_id))
|
||||
|
||||
assert response.status_code == 401
|
||||
db.refresh(open_session)
|
||||
assert open_session.status == SessionStatus.OPEN
|
||||
|
||||
|
||||
def test_rejects_a_garbage_token(client, db, test_widget, open_session):
|
||||
response = client.post(
|
||||
_url(test_widget.id, open_session.session_id),
|
||||
headers=_auth("not-a-jwt"),
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
db.refresh(open_session)
|
||||
assert open_session.status == SessionStatus.OPEN
|
||||
|
||||
|
||||
def test_rejects_a_token_issued_for_another_widget(client, db, test_widget, open_session, test_customer):
|
||||
other_widget_token = create_conversation_token(
|
||||
widget_id=str(uuid4()), customer_id=str(test_customer.id)
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
_url(test_widget.id, open_session.session_id),
|
||||
headers=_auth(other_widget_token),
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
db.refresh(open_session)
|
||||
assert open_session.status == SessionStatus.OPEN
|
||||
|
||||
|
||||
def test_cannot_close_another_customers_session(client, db, test_widget, open_session):
|
||||
"""The token proves who the caller is, not that the session is theirs."""
|
||||
stranger_token = create_conversation_token(
|
||||
widget_id=str(test_widget.id), customer_id=str(uuid4())
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
_url(test_widget.id, open_session.session_id),
|
||||
headers=_auth(stranger_token),
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
db.refresh(open_session)
|
||||
assert open_session.status == SessionStatus.OPEN
|
||||
|
||||
|
||||
def test_cannot_close_another_browsers_session_of_the_same_customer(
|
||||
client, db, test_widget, open_session, test_customer
|
||||
):
|
||||
"""Naming the customer is not enough: a token the widget issued to another
|
||||
browser (e.g. after someone typed this customer's email) is out of scope."""
|
||||
other_browser_token = create_conversation_token(
|
||||
widget_id=str(test_widget.id), customer_id=str(test_customer.id), ckey="browser-b"
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
_url(test_widget.id, open_session.session_id),
|
||||
headers=_auth(other_browser_token),
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
db.refresh(open_session)
|
||||
assert open_session.status == SessionStatus.OPEN
|
||||
|
||||
|
||||
def test_unknown_session_is_not_found(client, test_widget, widget_token):
|
||||
response = client.post(
|
||||
_url(test_widget.id, uuid4()),
|
||||
headers=_auth(widget_token),
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
Reference in new issue
Block a user