后端:新建 apps/adminpanel(跨团队后台容器,/api/admin/);GET/POST invitations(列跨团队码+筛选分页 / 平台发 create_team 开团队码,写审计)+ revoke(写审计),全程 IsPlatformAdmin。 前端:adminApi 分层;Admin 邀请码页(类型筛选段控 + 表格 + 发码弹窗 + 复制邀请链接 + 撤销)接入 AdminApp toast; Invitation 类型补 kind/team_name;admin-page.css 补段控/表格/弹窗(仅 token)。 测试:adminpanel 9 + accounts 22 = 31 单测过;无头 e2e _admin-p2.mjs 6 断言过 + 0 console error + 端到端凭 UI 发的码注册开团队 201;tsc+build 绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
4.6 KiB
Python
88 lines
4.6 KiB
Python
from rest_framework.test import APIClient
|
|
from django.test import TestCase
|
|
|
|
from apps.accounts.models import AdminAuditLog, Invitation, Team, TeamMember, User
|
|
|
|
|
|
class AdminInvitationApiTests(TestCase):
|
|
"""Phase 2:平台超管邀请码发放管理(发开团队码 / 列跨团队码 / 撤销)+ 权限 + 审计。"""
|
|
|
|
def setUp(self):
|
|
self.admin = User.objects.create_user(username="padmin", password="x", is_platform_admin=True)
|
|
self.normal = User.objects.create_user(username="normal", password="x")
|
|
self.team = Team.objects.create(name="Normal Team", owner=self.normal)
|
|
TeamMember.objects.create(team=self.team, user=self.normal, role=TeamMember.Role.OWNER)
|
|
self.admin_client = APIClient()
|
|
self.admin_client.force_authenticate(self.admin)
|
|
self.normal_client = APIClient()
|
|
self.normal_client.force_authenticate(self.normal)
|
|
|
|
def test_list_requires_platform_admin(self):
|
|
self.assertEqual(self.normal_client.get("/api/admin/invitations/").status_code, 403)
|
|
self.assertEqual(self.admin_client.get("/api/admin/invitations/").status_code, 200)
|
|
|
|
def test_create_create_team_code_and_audit(self):
|
|
r = self.admin_client.post("/api/admin/invitations/", {"email": "x@y.z"}, format="json")
|
|
self.assertEqual(r.status_code, 201)
|
|
self.assertEqual(r.data["kind"], Invitation.Kind.CREATE_TEAM)
|
|
self.assertTrue(r.data["code"].startswith("TEAM-"))
|
|
self.assertIsNone(r.data["team"])
|
|
self.assertEqual(r.data["register_url"], f"/register?invite={r.data['code']}")
|
|
self.assertTrue(AdminAuditLog.objects.filter(action="invite.issue_create_team").exists())
|
|
|
|
def test_create_requires_platform_admin(self):
|
|
self.assertEqual(self.normal_client.post("/api/admin/invitations/", {}, format="json").status_code, 403)
|
|
|
|
def test_list_is_cross_team(self):
|
|
self.admin_client.post("/api/admin/invitations/", {}, format="json")
|
|
Invitation.objects.create(kind=Invitation.Kind.JOIN_TEAM, team=self.team, role=TeamMember.Role.MEMBER)
|
|
r = self.admin_client.get("/api/admin/invitations/")
|
|
self.assertEqual(r.status_code, 200)
|
|
self.assertGreaterEqual(r.data["count"], 2)
|
|
kinds = {i["kind"] for i in r.data["results"]}
|
|
self.assertIn(Invitation.Kind.CREATE_TEAM, kinds)
|
|
self.assertIn(Invitation.Kind.JOIN_TEAM, kinds)
|
|
|
|
def test_filter_by_kind(self):
|
|
self.admin_client.post("/api/admin/invitations/", {}, format="json")
|
|
Invitation.objects.create(kind=Invitation.Kind.JOIN_TEAM, team=self.team, role=TeamMember.Role.MEMBER)
|
|
r = self.admin_client.get("/api/admin/invitations/?kind=create_team")
|
|
self.assertTrue(all(i["kind"] == Invitation.Kind.CREATE_TEAM for i in r.data["results"]))
|
|
|
|
def test_revoke_and_audit(self):
|
|
created = self.admin_client.post("/api/admin/invitations/", {}, format="json")
|
|
inv_id = created.data["id"]
|
|
r = self.admin_client.post(f"/api/admin/invitations/{inv_id}/revoke/")
|
|
self.assertEqual(r.status_code, 200)
|
|
self.assertEqual(r.data["status"], Invitation.Status.REVOKED)
|
|
self.assertTrue(AdminAuditLog.objects.filter(action="invite.revoke").exists())
|
|
|
|
def test_revoke_requires_platform_admin(self):
|
|
created = self.admin_client.post("/api/admin/invitations/", {}, format="json")
|
|
inv_id = created.data["id"]
|
|
self.assertEqual(self.normal_client.post(f"/api/admin/invitations/{inv_id}/revoke/").status_code, 403)
|
|
|
|
def test_issued_create_team_code_registers_new_team(self):
|
|
created = self.admin_client.post("/api/admin/invitations/", {}, format="json")
|
|
code = created.data["code"]
|
|
reg = APIClient().post(
|
|
"/api/auth/register/",
|
|
{"username": "new-via-admin-code", "password": "strong-pass-1", "team_name": "Via Admin", "invite_code": code},
|
|
format="json",
|
|
)
|
|
self.assertEqual(reg.status_code, 201)
|
|
self.assertTrue(Team.objects.filter(name="Via Admin").exists())
|
|
# 码标 used
|
|
self.assertEqual(Invitation.objects.get(code=code).status, Invitation.Status.USED)
|
|
|
|
def test_revoked_code_cannot_register(self):
|
|
created = self.admin_client.post("/api/admin/invitations/", {}, format="json")
|
|
code = created.data["code"]
|
|
self.admin_client.post(f"/api/admin/invitations/{created.data['id']}/revoke/")
|
|
reg = APIClient().post(
|
|
"/api/auth/register/",
|
|
{"username": "blocked-user", "password": "strong-pass-1", "team_name": "Blocked", "invite_code": code},
|
|
format="json",
|
|
)
|
|
self.assertEqual(reg.status_code, 400)
|