134 lines
4.6 KiB
Python
134 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
|
|
BACKEND_DIR = Path(__file__).resolve().parents[1]
|
|
os.environ["KEFU_DB_TYPE"] = "sqlite"
|
|
os.environ["KEFU_DATABASE_URL"] = "sqlite+aiosqlite:///:memory:"
|
|
if str(BACKEND_DIR) not in sys.path:
|
|
sys.path.insert(0, str(BACKEND_DIR))
|
|
|
|
from models.models import Base, Role, User # noqa: E402
|
|
from auth.passwords import hash_password # noqa: E402
|
|
from auth.permissions import ACCOUNTS_WRITE, ALL_PERMISSIONS, MENU_USERS # noqa: E402
|
|
from auth.role_service import ( # noqa: E402
|
|
create_role,
|
|
delete_role,
|
|
guard_last_admin_change,
|
|
seed_builtin_roles,
|
|
update_role,
|
|
)
|
|
from auth.roles import has_permission, is_admin, permissions_for_role # noqa: E402
|
|
from auth import router as auth_router # noqa: E402
|
|
|
|
|
|
class RolesRbacTests(unittest.IsolatedAsyncioTestCase):
|
|
async def asyncSetUp(self):
|
|
self.engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
async with self.engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
self.session_factory = sessionmaker(
|
|
self.engine, class_=AsyncSession, expire_on_commit=False
|
|
)
|
|
self.db = self.session_factory()
|
|
await seed_builtin_roles(self.db)
|
|
|
|
async def asyncTearDown(self):
|
|
await self.db.close()
|
|
await self.engine.dispose()
|
|
|
|
async def test_seed_creates_builtin_roles(self):
|
|
result = await self.db.execute(select(Role))
|
|
codes = {row.code for row in result.scalars().all()}
|
|
self.assertEqual(codes, {"admin", "operator", "viewer"})
|
|
self.assertTrue(is_admin("admin"))
|
|
self.assertFalse(is_admin("operator"))
|
|
self.assertIn(MENU_USERS, permissions_for_role("admin"))
|
|
self.assertNotIn(MENU_USERS, permissions_for_role("operator"))
|
|
self.assertFalse(has_permission("viewer", ACCOUNTS_WRITE))
|
|
|
|
async def test_custom_role_crud(self):
|
|
created = await create_role(
|
|
self.db,
|
|
code="ops_leader",
|
|
label="运营主管",
|
|
description="可管账号",
|
|
permissions=[MENU_USERS, ACCOUNTS_WRITE],
|
|
)
|
|
self.assertEqual(created.code, "ops_leader")
|
|
self.assertTrue(has_permission("ops_leader", ACCOUNTS_WRITE))
|
|
self.assertFalse(is_admin("ops_leader"))
|
|
|
|
updated = await update_role(
|
|
self.db,
|
|
"ops_leader",
|
|
label="主管",
|
|
permissions=[ACCOUNTS_WRITE],
|
|
)
|
|
self.assertEqual(updated.label, "主管")
|
|
self.assertFalse(has_permission("ops_leader", MENU_USERS))
|
|
|
|
await delete_role(self.db, "ops_leader")
|
|
self.assertFalse(has_permission("ops_leader", ACCOUNTS_WRITE))
|
|
|
|
async def test_cannot_delete_system_role(self):
|
|
with self.assertRaises(HTTPException) as caught:
|
|
await delete_role(self.db, "operator")
|
|
self.assertEqual(caught.exception.status_code, 400)
|
|
|
|
async def test_admin_permissions_always_full(self):
|
|
await update_role(
|
|
self.db,
|
|
"admin",
|
|
label="管理员",
|
|
permissions=[ACCOUNTS_WRITE],
|
|
)
|
|
self.assertEqual(permissions_for_role("admin"), list(ALL_PERMISSIONS))
|
|
|
|
async def test_me_payload_includes_permissions(self):
|
|
user = User(
|
|
username="u1",
|
|
password_hash=hash_password("password1"),
|
|
display_name="U1",
|
|
role="operator",
|
|
is_active=True,
|
|
email_verified=True,
|
|
)
|
|
self.db.add(user)
|
|
await self.db.commit()
|
|
await self.db.refresh(user)
|
|
payload = await auth_router._build_user_response(self.db, user)
|
|
self.assertEqual(payload.role_label, "运营")
|
|
self.assertFalse(payload.is_admin)
|
|
self.assertIn(ACCOUNTS_WRITE, payload.permissions)
|
|
self.assertNotIn(MENU_USERS, payload.permissions)
|
|
|
|
async def test_last_admin_cannot_be_demoted(self):
|
|
admin = User(
|
|
username="admin1",
|
|
password_hash=hash_password("password1"),
|
|
role="admin",
|
|
is_active=True,
|
|
email_verified=True,
|
|
)
|
|
self.db.add(admin)
|
|
await self.db.commit()
|
|
await self.db.refresh(admin)
|
|
|
|
with self.assertRaises(HTTPException) as caught:
|
|
await guard_last_admin_change(self.db, user=admin, new_role="operator")
|
|
self.assertEqual(caught.exception.status_code, 400)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|