test: 补充权限、消息与会话服务回归用例

会话读取、接管、重分配与 Socket 订阅统一校验角色、组织与可见性。
This commit is contained in:
WangShen007 committed 2026-10-08 22:35:00 +08:00
1 parent 5da5f910da
commit 2554eb7bc7
97 files changed
+37281

No files matched your search

+185
View File
@@ -0,0 +1,185 @@
"""
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.
Keeping an identified visitor identified.
A visitor the embedding app vouched for used to become a brand-new anonymous
customer the moment their token lapsed and anything re-checked it - which, on a page
left open, is exactly when they start a new chat. The widget now rotates a live token
before it expires, and a lapsed one is refused rather than quietly replaced.
"""
import time
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.api import token as token_router
from app.api import widget as widget_router
from app.database import get_db
from app.models.customer import Customer
from app.services import conversation_token
@pytest.fixture
def client(db) -> TestClient:
app = FastAPI()
app.include_router(widget_router.router, prefix="/api/v1/widgets")
app.include_router(token_router.router, prefix="/api/v1")
def override_get_db():
yield db
app.dependency_overrides[get_db] = override_get_db
return TestClient(app)
@pytest.fixture
def identified_customer(db, test_organization) -> Customer:
"""The customer POST /generate-token creates for a signed-in visitor."""
customer = Customer(
organization_id=test_organization.id,
email="signed.in@example.com",
full_name="Signed In",
meta_data={"plan": "pro"},
is_authenticated=True,
)
db.add(customer)
db.commit()
db.refresh(customer)
return customer
@pytest.fixture
def identified_token(test_widget, identified_customer) -> str:
token, _, _ = conversation_token.mint(
{
"sub": str(identified_customer.id),
"customer_id": str(identified_customer.id),
"widget_id": str(test_widget.id),
"customer_email": identified_customer.email,
"customer_name": identified_customer.full_name,
"custom_data": {"plan": "pro"},
},
ttl_seconds=600,
)
return token
@pytest.fixture
def token_has_lapsed(monkeypatch):
"""Every token's JTI is gone from Redis - expired, or revoked."""
monkeypatch.setattr(conversation_token, "_is_token_in_redis", lambda jti: False)
def _auth(token: str) -> dict:
return {"Authorization": f"Bearer {token}"}
class TestRefreshToken:
def test_rotates_a_live_token_and_keeps_the_visitor(self, client, test_widget, identified_token):
response = client.post(
"/api/v1/refresh-token",
json={"widget_id": str(test_widget.id)},
headers=_auth(identified_token),
)
assert response.status_code == 200, response.text
rotated = response.json()["data"]["token"]
assert rotated != identified_token
claims = conversation_token.inspect(rotated, str(test_widget.id)).payload
assert claims["customer_email"] == "signed.in@example.com"
assert claims["custom_data"] == {"plan": "pro"}
assert response.json()["data"]["expires_in"] == 600
def test_refuses_a_lapsed_token_instead_of_issuing_a_new_identity(
self, client, test_widget, identified_token, token_has_lapsed
):
response = client.post(
"/api/v1/refresh-token",
json={"widget_id": str(test_widget.id)},
headers=_auth(identified_token),
)
assert response.status_code == 401
assert response.json()["detail"]["code"] == conversation_token.IDENTITY_EXPIRED_CODE
def test_refuses_a_token_issued_for_another_widget(self, client, identified_token):
response = client.post(
"/api/v1/refresh-token",
json={"widget_id": "some-other-widget"},
headers=_auth(identified_token),
)
assert response.status_code == 401
def test_requires_a_token(self, client, test_widget):
response = client.post("/api/v1/refresh-token", json={"widget_id": str(test_widget.id)})
assert response.status_code == 401
assert response.json()["detail"]["code"] == conversation_token.INVALID_TOKEN_CODE
class TestWidgetDataIdentity:
def test_keeps_the_identity_of_a_live_token(self, client, test_widget, identified_token):
response = client.get(f"/api/v1/widgets/{test_widget.id}", headers=_auth(identified_token))
assert response.status_code == 200, response.text
# No replacement token: the widget keeps the identity it arrived with.
assert not response.json().get("token")
def test_does_not_swap_a_lapsed_identity_for_an_anonymous_customer(
self, client, db, test_widget, identified_token, token_has_lapsed
):
customers_before = db.query(Customer).count()
response = client.get(f"/api/v1/widgets/{test_widget.id}", headers=_auth(identified_token))
assert response.status_code == 401
assert response.json()["detail"]["code"] == conversation_token.IDENTITY_EXPIRED_CODE
# The silent "@noemail.com" customer is the bug; there must be no new one.
assert db.query(Customer).count() == customers_before
def test_a_visitor_with_no_token_still_starts_anonymously(self, client, test_widget):
response = client.get(f"/api/v1/widgets/{test_widget.id}")
assert response.status_code == 200, response.text
assert response.json()["token"]
def test_visitors_arriving_in_the_same_millisecond_each_get_a_customer(
self, client, db, test_widget, monkeypatch
):
# The placeholder email used to be the clock in ms, so the second visitor
# hit the (email, org) unique key and their widget never loaded.
monkeypatch.setattr(time, "time", lambda: 1_791_833_067.582)
customers_before = db.query(Customer).count()
first = client.get(f"/api/v1/widgets/{test_widget.id}")
second = client.get(f"/api/v1/widgets/{test_widget.id}")
assert first.status_code == 200, first.text
assert second.status_code == 200, second.text
assert db.query(Customer).count() == customers_before + 2
def test_widget_frame_refuses_to_render_a_lapsed_identity(
self, client, test_widget, identified_token, token_has_lapsed
):
response = client.get(
f"/api/v1/widgets/{test_widget.id}/data", headers=_auth(identified_token)
)
assert response.status_code == 401
assert response.json()["detail"]["code"] == conversation_token.IDENTITY_EXPIRED_CODE