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

+187
View File
@@ -0,0 +1,187 @@
"""
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.
"""
from datetime import datetime, timedelta, timezone
import pytest
# Personal Access Tokens are an enterprise-only feature; skip cleanly in community checkouts.
pytest.importorskip("app.enterprise.repositories.personal_access_token")
from app.enterprise.repositories.personal_access_token import ( # noqa: E402
PersonalAccessTokenRepository,
generate_pat,
TOKEN_PREFIX,
)
from app.enterprise.services.pat import resolve_pat_user # noqa: E402
def test_generate_pat_format():
token, prefix = generate_pat()
assert token.startswith(TOKEN_PREFIX)
assert prefix == token[: len(prefix)]
assert len(token) > len(prefix)
# two calls produce different secrets
other, _ = generate_pat()
assert other != token
def test_create_token_persists_hash_not_plaintext(db, test_user):
repo = PersonalAccessTokenRepository(db)
row, plain = repo.create_token(
user_id=test_user.id, organization_id=test_user.organization_id, name="cli"
)
assert plain.startswith(TOKEN_PREFIX)
assert row.token_hash != plain # stored hashed, not plaintext
assert row.token_prefix == plain[: len(row.token_prefix)]
assert row.is_active is True
assert row.user_id == test_user.id
def test_validate_token_roundtrip_and_last_used(db, test_user):
repo = PersonalAccessTokenRepository(db)
row, plain = repo.create_token(
user_id=test_user.id, organization_id=test_user.organization_id, name="cli"
)
assert row.last_used_at is None
validated = repo.validate_token(plain)
assert validated is not None
assert validated.id == row.id
assert validated.last_used_at is not None # touched on use
def test_validate_token_rejects_wrong_and_garbage(db, test_user):
repo = PersonalAccessTokenRepository(db)
repo.create_token(user_id=test_user.id, organization_id=test_user.organization_id, name="cli")
assert repo.validate_token("cmat_not-a-real-token") is None
assert repo.validate_token("jwt-style-token") is None
assert repo.validate_token("") is None
def test_validate_token_rejects_revoked(db, test_user):
repo = PersonalAccessTokenRepository(db)
row, plain = repo.create_token(
user_id=test_user.id, organization_id=test_user.organization_id, name="cli"
)
assert repo.revoke(row.id, test_user.id) is True
assert repo.validate_token(plain) is None
def test_validate_token_rejects_expired(db, test_user):
repo = PersonalAccessTokenRepository(db)
row, plain = repo.create_token(
user_id=test_user.id, organization_id=test_user.organization_id, name="cli"
)
# Force expiry into the past.
row.expires_at = datetime.now(timezone.utc) - timedelta(days=1)
db.commit()
assert repo.validate_token(plain) is None
def test_list_and_scope(db, test_user):
repo = PersonalAccessTokenRepository(db)
repo.create_token(user_id=test_user.id, organization_id=test_user.organization_id, name="a")
repo.create_token(user_id=test_user.id, organization_id=test_user.organization_id, name="b")
tokens = repo.list_for_user(test_user.id)
assert len(tokens) == 2
# get_by_id is scoped to the owning user
assert repo.get_by_id(tokens[0].id, test_user.id) is not None
def test_resolve_pat_user_returns_owner(db, test_user):
repo = PersonalAccessTokenRepository(db)
_, plain = repo.create_token(
user_id=test_user.id, organization_id=test_user.organization_id, name="cli"
)
user = resolve_pat_user(plain, db)
assert user is not None
assert user.id == test_user.id
def test_resolve_pat_user_rejects_inactive_user(db, test_user):
repo = PersonalAccessTokenRepository(db)
_, plain = repo.create_token(
user_id=test_user.id, organization_id=test_user.organization_id, name="cli"
)
test_user.is_active = False
db.commit()
assert resolve_pat_user(plain, db) is None
def test_resolve_pat_user_rejects_non_pat_token(db):
assert resolve_pat_user("not-a-pat", db) is None
assert resolve_pat_user("", db) is None
# --- Integration: the open-source get_current_user hook resolves cmat_ tokens ---
import asyncio # noqa: E402
from fastapi import HTTPException # noqa: E402
from starlette.requests import Request # noqa: E402
from app.core.auth import get_current_user # noqa: E402
def _request_with_bearer(token: str) -> Request:
headers = [(b"authorization", f"Bearer {token}".encode())]
scope = {
"type": "http", "method": "GET", "path": "/", "query_string": b"",
"headers": headers,
}
return Request(scope)
def test_get_current_user_accepts_valid_pat(db, test_user):
repo = PersonalAccessTokenRepository(db)
_, plain = repo.create_token(
user_id=test_user.id, organization_id=test_user.organization_id, name="cli"
)
user = asyncio.run(get_current_user(_request_with_bearer(plain), None, db))
assert user.id == test_user.id
def test_get_current_user_rejects_bogus_pat(db, test_user):
with pytest.raises(HTTPException) as exc:
asyncio.run(get_current_user(_request_with_bearer("cmat_bogus"), None, db))
assert exc.value.status_code == 401
# --- Security: a PAT cannot be used to mint further PATs ---
from app.enterprise.routes.personal_access_token import ( # noqa: E402
create_personal_access_token,
)
from app.enterprise.schemas.personal_access_token import PATCreate # noqa: E402
def test_create_pat_rejected_when_request_uses_a_pat(db, test_user):
req = _request_with_bearer("cmat_sometoken")
with pytest.raises(HTTPException) as exc:
# current_user/request are passed as kwargs, matching how FastAPI invokes the
# handler (the rate-limit decorator reads current_user from kwargs).
asyncio.run(create_personal_access_token(
PATCreate(name="child"), request=req, db=db, current_user=test_user))
assert exc.value.status_code == 403
def test_create_pat_allowed_with_interactive_session(db, test_user):
# A JWT bearer (not cmat_) represents an interactive login and is allowed.
req = _request_with_bearer("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.sig")
resp = asyncio.run(create_personal_access_token(
PATCreate(name="ok"), request=req, db=db, current_user=test_user))
assert resp.token.startswith("cmat_")