Files
deerflow-code/offline-backend-20260512/backend/tests/test_username_login_domain.py
T
董宏凯andCursor db333a4c33 feat: 重构多轮RAG展示并完善报告协作、业务链审核与 agentfx 种子
抽出专用多轮RAG消息列表并升级会话模型;强化报告协作画布与检查器;业务链普通用户改为提交审核;扩展 agentfx 报告技能种子,并将含 admin 的用户名自动提权为管理员。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 17:34:16 +08:00

261 lines
9.0 KiB
Python

"""Tests for the username-based login domain handling.
Verifies that:
- The default mail domain is ``cm.com`` (with env-var override still honoured).
- ``_normalize_username_local_part`` distinguishes raw-email vs username input.
- The login flow falls back to a legacy-domain lookup so the same username
keeps mapping to the same account after the default domain changes.
"""
from __future__ import annotations
from datetime import UTC, datetime
from uuid import uuid4
import pytest
from app.gateway.auth.local_provider import LocalAuthProvider
from app.gateway.auth.models import User
from app.gateway.auth.repositories.base import UserRepository
from app.gateway.routers import auth as auth_module
from app.gateway.routers.auth import (
_get_or_create_user_by_username,
_lookup_user_by_username,
_normalize_username_local_part,
_username_to_email,
)
class FakeUserRepository(UserRepository):
"""In-memory repo just enough to exercise the local-part lookup."""
def __init__(self) -> None:
self.rows: dict[str, User] = {}
self.create_calls: list[User] = []
async def create_user(self, user: User) -> User:
for existing in self.rows.values():
if existing.email == user.email:
raise ValueError(f"Email already registered: {user.email}")
u = User(
id=user.id or uuid4(),
email=user.email,
password_hash=user.password_hash,
system_role=user.system_role,
created_at=user.created_at or datetime.now(UTC),
oauth_provider=user.oauth_provider,
oauth_id=user.oauth_id,
needs_setup=user.needs_setup,
token_version=user.token_version,
)
self.rows[str(u.id)] = u
self.create_calls.append(u)
return u
async def get_user_by_id(self, user_id: str) -> User | None:
return self.rows.get(user_id)
async def get_user_by_email(self, email: str) -> User | None:
for user in self.rows.values():
if user.email == email:
return user
return None
async def find_user_by_email_local_part(self, local_part: str) -> User | None:
prefix = f"{(local_part or '').strip().lower()}@"
candidates = [u for u in self.rows.values() if u.email.lower().startswith(prefix)]
candidates.sort(key=lambda u: u.created_at)
return candidates[0] if candidates else None
async def update_user(self, user: User) -> User:
self.rows[str(user.id)] = user
return user
async def count_users(self) -> int:
return len(self.rows)
async def count_admin_users(self) -> int:
return sum(1 for u in self.rows.values() if u.system_role == "admin")
async def list_users(self) -> list[User]:
return sorted(self.rows.values(), key=lambda user: user.created_at)
async def get_user_by_oauth(self, provider: str, oauth_id: str) -> User | None:
return None
def _seed(repo: FakeUserRepository, email: str, *, role: str = "user") -> User:
user = User(
id=uuid4(),
email=email,
password_hash=None,
system_role=role, # type: ignore[arg-type]
created_at=datetime.now(UTC),
needs_setup=False,
)
repo.rows[str(user.id)] = user
return user
def test_normalize_returns_none_for_explicit_email() -> None:
assert _normalize_username_local_part("alice@old.com") is None
def test_normalize_lowercases_and_keeps_safe_chars() -> None:
assert _normalize_username_local_part("Alice_Smith.1") == "alice_smith.1"
def test_normalize_strips_unsafe_chars() -> None:
assert _normalize_username_local_part("hi there!") == "hi-there"
def test_username_to_email_uses_cm_com_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("DEER_FLOW_USERNAME_LOGIN_DOMAIN", raising=False)
assert _username_to_email("alice") == "alice@cm.com"
def test_username_to_email_honours_env_override(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("DEER_FLOW_USERNAME_LOGIN_DOMAIN", "example.org")
assert _username_to_email("alice") == "alice@example.org"
def test_username_to_email_passes_explicit_email_through() -> None:
assert _username_to_email("bob@legacy.io") == "bob@legacy.io"
@pytest.mark.asyncio
async def test_local_part_lookup_finds_legacy_domain_user() -> None:
"""Same username under legacy domain should match the new lookup."""
repo = FakeUserRepository()
_seed(repo, "alice@local.deerflow")
legacy = await repo.find_user_by_email_local_part("alice")
assert legacy is not None
assert legacy.email == "alice@local.deerflow"
@pytest.mark.asyncio
async def test_local_part_lookup_returns_oldest_when_collision() -> None:
"""When the same local part exists under multiple domains, prefer the
oldest row so re-logins land on the original account."""
repo = FakeUserRepository()
older = _seed(repo, "alice@local.deerflow")
older.created_at = datetime(2024, 1, 1, tzinfo=UTC)
newer = _seed(repo, "alice@cm.com")
newer.created_at = datetime(2026, 1, 1, tzinfo=UTC)
match = await repo.find_user_by_email_local_part("alice")
assert match is not None
assert match.email == "alice@local.deerflow"
@pytest.mark.asyncio
async def test_local_part_lookup_misses_unknown_username() -> None:
repo = FakeUserRepository()
_seed(repo, "alice@cm.com")
assert await repo.find_user_by_email_local_part("bob") is None
@pytest.mark.asyncio
async def test_username_login_uses_oldest_account_when_canonical_duplicate_exists(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A domain migration must not make one username alternate user ids."""
repo = FakeUserRepository()
older = _seed(repo, "alice@local.deerflow")
older.created_at = datetime(2024, 1, 1, tzinfo=UTC)
newer = _seed(repo, "alice@cm.com")
newer.created_at = datetime(2026, 1, 1, tzinfo=UTC)
monkeypatch.setattr(auth_module, "get_local_provider", lambda: LocalAuthProvider(repo))
user, created = await _get_or_create_user_by_username("alice")
assert created is False
assert user.id == older.id
@pytest.mark.asyncio
async def test_password_lookup_uses_same_stable_username_account(
monkeypatch: pytest.MonkeyPatch,
) -> None:
repo = FakeUserRepository()
older = _seed(repo, "alice@local.deerflow")
older.created_at = datetime(2024, 1, 1, tzinfo=UTC)
newer = _seed(repo, "alice@cm.com")
newer.created_at = datetime(2026, 1, 1, tzinfo=UTC)
monkeypatch.setattr(auth_module, "get_local_provider", lambda: LocalAuthProvider(repo))
user = await _lookup_user_by_username("alice")
assert user is not None
assert user.id == older.id
# ── 用户名含 admin → 自动管理员(注册即 admin / 历史账号登录时提升)───────────
def test_admin_username_rule_matches_substring_case_insensitive() -> None:
from app.gateway.routers.auth import _is_admin_username
assert _is_admin_username("admin") is True
assert _is_admin_username("b1admin") is True
assert _is_admin_username("Admin_Planning") is True
assert _is_admin_username("administrator") is True
assert _is_admin_username("zhangsan") is False
assert _is_admin_username("") is False
@pytest.mark.asyncio
async def test_admin_substring_username_registers_as_admin(
monkeypatch: pytest.MonkeyPatch,
) -> None:
repo = FakeUserRepository()
monkeypatch.setattr(auth_module, "get_local_provider", lambda: LocalAuthProvider(repo))
user, created = await _get_or_create_user_by_username("b1admin")
assert created is True
assert user.system_role == "admin"
@pytest.mark.asyncio
async def test_plain_username_still_registers_as_user(
monkeypatch: pytest.MonkeyPatch,
) -> None:
repo = FakeUserRepository()
monkeypatch.setattr(auth_module, "get_local_provider", lambda: LocalAuthProvider(repo))
user, created = await _get_or_create_user_by_username("zhangsan")
assert created is True
assert user.system_role == "user"
@pytest.mark.asyncio
async def test_existing_admin_substring_account_promoted_on_login(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""历史已注册成普通用户的含 admin 账号,登录时自动提升为管理员。"""
repo = FakeUserRepository()
seeded = _seed(repo, "b1admin@cm.com", role="user")
monkeypatch.setattr(auth_module, "get_local_provider", lambda: LocalAuthProvider(repo))
user, created = await _get_or_create_user_by_username("b1admin")
assert created is False
assert user.id == seeded.id
assert user.system_role == "admin"
@pytest.mark.asyncio
async def test_existing_plain_account_not_promoted_on_login(
monkeypatch: pytest.MonkeyPatch,
) -> None:
repo = FakeUserRepository()
seeded = _seed(repo, "zhangsan@cm.com", role="user")
monkeypatch.setattr(auth_module, "get_local_provider", lambda: LocalAuthProvider(repo))
user, created = await _get_or_create_user_by_username("zhangsan")
assert created is False
assert user.id == seeded.id
assert user.system_role == "user"