"""Интеграционные тесты справочника команд `/api/v1/admin/teams` и привязки `users.team_id`. Покрывает: CRUD команд, 409 на дубль имени, 403 не-админу на новые эндпоинты, назначение/снятие команды пользователю (в т.ч. явный `null`), 404 на несуществующую команду, обнуление `team_id` при удалении команды, разрешённое самоизменение `team_id` (в отличие от `role`/`is_blocked`). """ import uuid import httpx from sqlalchemy.ext.asyncio import AsyncSession from core.security import create_access_token, hash_password from models.team import Team from models.user import User async def _make_user(session: AsyncSession, *, role: str = "user") -> User: user = User( email=f"{uuid.uuid4()}@example.com", name_user="Team API Tester", password_hash=hash_password("password123"), email_verified=True, role=role, ) session.add(user) await session.flush() return user async def _make_team(session: AsyncSession, *, name: str | None = None) -> Team: team = Team(name=name or f"Team {uuid.uuid4()}") session.add(team) await session.flush() return team def _auth_headers(user: User) -> dict[str, str]: token = create_access_token(user.id, user.role) return {"Authorization": f"Bearer {token}"} # --- 403 не-админу ------------------------------------------------------------------- async def test_team_endpoints_forbidden_for_non_admin( client: httpx.AsyncClient, db_session: AsyncSession ) -> None: user = await _make_user(db_session) team = await _make_team(db_session) await db_session.commit() headers = _auth_headers(user) requests = [ ("GET", "/api/v1/admin/teams", None), ("POST", "/api/v1/admin/teams", {"name": "X"}), ("PATCH", f"/api/v1/admin/teams/{team.id}", {"name": "Y"}), ("DELETE", f"/api/v1/admin/teams/{team.id}", None), ] for method, path, body in requests: response = await client.request(method, path, json=body, headers=headers) assert response.status_code == 403, f"{method} {path} -> {response.status_code}" # --- CRUD команд --------------------------------------------------------------------- async def test_create_and_list_teams_sorted_by_name( client: httpx.AsyncClient, db_session: AsyncSession ) -> None: """Список отсортирован по названию; сравниваем взаимный порядок и наличие своих (уникальных по суффиксу) команд, а не полный список — в общей dev-БД могут быть и другие, легитимные команды разработчика.""" admin = await _make_user(db_session, role="admin") suffix = uuid.uuid4().hex[:8] name_alpha, name_beta, name_zeta = f"Alpha-{suffix}", f"Beta-{suffix}", f"Zeta-{suffix}" await _make_team(db_session, name=name_zeta) await _make_team(db_session, name=name_alpha) await db_session.commit() response = await client.post( "/api/v1/admin/teams", json={"name": name_beta}, headers=_auth_headers(admin) ) assert response.status_code == 201, response.text created = response.json() assert created["name"] == name_beta assert "id" in created and "created_at" in created list_response = await client.get("/api/v1/admin/teams", headers=_auth_headers(admin)) assert list_response.status_code == 200 body = list_response.json() names = [item["name"] for item in body["items"]] assert names == sorted(names) assert name_alpha in names and name_beta in names and name_zeta in names assert names.index(name_alpha) < names.index(name_beta) < names.index(name_zeta) assert body["total"] == len(body["items"]) async def test_create_team_strips_name(client: httpx.AsyncClient, db_session: AsyncSession) -> None: admin = await _make_user(db_session, role="admin") await db_session.commit() response = await client.post( "/api/v1/admin/teams", json={"name": " Padded "}, headers=_auth_headers(admin) ) assert response.status_code == 201, response.text assert response.json()["name"] == "Padded" async def test_create_team_duplicate_name_returns_409( client: httpx.AsyncClient, db_session: AsyncSession ) -> None: admin = await _make_user(db_session, role="admin") name = f"Duplicate {uuid.uuid4()}" await _make_team(db_session, name=name) await db_session.commit() response = await client.post( "/api/v1/admin/teams", json={"name": name}, headers=_auth_headers(admin) ) assert response.status_code == 409 async def test_update_team_renames(client: httpx.AsyncClient, db_session: AsyncSession) -> None: admin = await _make_user(db_session, role="admin") team = await _make_team(db_session, name=f"Old Name {uuid.uuid4()}") await db_session.commit() new_name = f"New Name {uuid.uuid4()}" response = await client.patch( f"/api/v1/admin/teams/{team.id}", json={"name": new_name}, headers=_auth_headers(admin) ) assert response.status_code == 200, response.text assert response.json()["name"] == new_name async def test_update_team_duplicate_name_returns_409( client: httpx.AsyncClient, db_session: AsyncSession ) -> None: admin = await _make_user(db_session, role="admin") name_a = f"Team A {uuid.uuid4()}" team_a = await _make_team(db_session, name=name_a) team_b = await _make_team(db_session, name=f"Team B {uuid.uuid4()}") await db_session.commit() response = await client.patch( f"/api/v1/admin/teams/{team_b.id}", json={"name": name_a}, headers=_auth_headers(admin), ) assert response.status_code == 409 assert team_a.name == name_a async def test_update_team_unknown_returns_404( client: httpx.AsyncClient, db_session: AsyncSession ) -> None: admin = await _make_user(db_session, role="admin") await db_session.commit() response = await client.patch( f"/api/v1/admin/teams/{uuid.uuid4()}", json={"name": "X"}, headers=_auth_headers(admin) ) assert response.status_code == 404 async def test_delete_team_unknown_returns_404( client: httpx.AsyncClient, db_session: AsyncSession ) -> None: admin = await _make_user(db_session, role="admin") await db_session.commit() response = await client.delete( f"/api/v1/admin/teams/{uuid.uuid4()}", headers=_auth_headers(admin) ) assert response.status_code == 404 async def test_delete_team_clears_user_team_id( client: httpx.AsyncClient, db_session: AsyncSession ) -> None: admin = await _make_user(db_session, role="admin") team = await _make_team(db_session) member = await _make_user(db_session) member.team_id = team.id await db_session.commit() response = await client.delete(f"/api/v1/admin/teams/{team.id}", headers=_auth_headers(admin)) assert response.status_code == 204 await db_session.refresh(member) assert member.team_id is None # --- Привязка пользователя к команде --------------------------------------------------- async def test_patch_user_assigns_team(client: httpx.AsyncClient, db_session: AsyncSession) -> None: admin = await _make_user(db_session, role="admin") target = await _make_user(db_session) team = await _make_team(db_session) await db_session.commit() response = await client.patch( f"/api/v1/admin/users/{target.id}", json={"team_id": str(team.id)}, headers=_auth_headers(admin), ) assert response.status_code == 200, response.text assert response.json()["team_id"] == str(team.id) async def test_patch_user_clears_team_with_explicit_null( client: httpx.AsyncClient, db_session: AsyncSession ) -> None: admin = await _make_user(db_session, role="admin") team = await _make_team(db_session) target = await _make_user(db_session) target.team_id = team.id await db_session.commit() response = await client.patch( f"/api/v1/admin/users/{target.id}", json={"team_id": None}, headers=_auth_headers(admin), ) assert response.status_code == 200, response.text assert response.json()["team_id"] is None async def test_patch_user_missing_team_field_keeps_current_team( client: httpx.AsyncClient, db_session: AsyncSession ) -> None: admin = await _make_user(db_session, role="admin") team = await _make_team(db_session) target = await _make_user(db_session) target.team_id = team.id await db_session.commit() response = await client.patch( f"/api/v1/admin/users/{target.id}", json={"role": "admin"}, headers=_auth_headers(admin), ) assert response.status_code == 200, response.text assert response.json()["team_id"] == str(team.id) async def test_patch_user_unknown_team_returns_404( client: httpx.AsyncClient, db_session: AsyncSession ) -> None: admin = await _make_user(db_session, role="admin") target = await _make_user(db_session) await db_session.commit() response = await client.patch( f"/api/v1/admin/users/{target.id}", json={"team_id": str(uuid.uuid4())}, headers=_auth_headers(admin), ) assert response.status_code == 404 async def test_admin_can_change_own_team_id( client: httpx.AsyncClient, db_session: AsyncSession ) -> None: admin = await _make_user(db_session, role="admin") team = await _make_team(db_session) await db_session.commit() response = await client.patch( f"/api/v1/admin/users/{admin.id}", json={"team_id": str(team.id)}, headers=_auth_headers(admin), ) assert response.status_code == 200, response.text assert response.json()["team_id"] == str(team.id) async def test_admin_still_forbidden_to_change_own_role_and_block( client: httpx.AsyncClient, db_session: AsyncSession ) -> None: admin = await _make_user(db_session, role="admin") await db_session.commit() response = await client.patch( f"/api/v1/admin/users/{admin.id}", json={"is_blocked": True}, headers=_auth_headers(admin), ) assert response.status_code == 409 response = await client.patch( f"/api/v1/admin/users/{admin.id}", json={"role": "user"}, headers=_auth_headers(admin), ) assert response.status_code == 409