171 lines
7.1 KiB
Python
171 lines
7.1 KiB
Python
import unittest
|
|
from types import SimpleNamespace
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from app.core.security import create_access_token, get_password_hash
|
|
from app.services.auth_service import AuthService
|
|
|
|
|
|
class StubUserRepository:
|
|
def __init__(self):
|
|
self.user = SimpleNamespace(
|
|
id="user-admin",
|
|
tenant_id="tenant-demo-001",
|
|
username="admin@example.com",
|
|
display_name="管理员",
|
|
password_hash=get_password_hash("123456"),
|
|
role="admin",
|
|
status="active",
|
|
)
|
|
self.users_by_username = {self.user.username: self.user}
|
|
self.users_by_id = {self.user.id: self.user}
|
|
|
|
def get_by_username(self, username):
|
|
return self.users_by_username.get(username)
|
|
|
|
def get_by_id(self, user_id):
|
|
return self.users_by_id.get(user_id)
|
|
|
|
def create(self, user):
|
|
self.users_by_username[user.username] = user
|
|
self.users_by_id[user.id] = user
|
|
return user
|
|
|
|
def update(self, user):
|
|
self.users_by_username[user.username] = user
|
|
self.users_by_id[user.id] = user
|
|
if user.id == self.user.id:
|
|
self.user = user
|
|
return user
|
|
|
|
|
|
class StubAuditLogService:
|
|
def __init__(self):
|
|
self.events = []
|
|
|
|
def record_event(self, **kwargs):
|
|
self.events.append(kwargs)
|
|
return kwargs
|
|
|
|
|
|
class StubTenantService:
|
|
def __init__(self):
|
|
self.ensure_default_tenant_calls = 0
|
|
|
|
def ensure_default_tenant(self):
|
|
self.ensure_default_tenant_calls += 1
|
|
return {"id": "tenant-demo-001"}
|
|
|
|
|
|
class AuthServiceTestCase(unittest.TestCase):
|
|
def setUp(self):
|
|
self.service = AuthService(db=None)
|
|
self.service.repository = StubUserRepository()
|
|
self.service.audit_log_service = StubAuditLogService()
|
|
self.original_tenant_service = AuthService.register.__globals__["TenantService"]
|
|
self.stub_tenant_service = StubTenantService()
|
|
AuthService.register.__globals__["TenantService"] = lambda db: self.stub_tenant_service
|
|
|
|
def tearDown(self):
|
|
AuthService.register.__globals__["TenantService"] = self.original_tenant_service
|
|
|
|
def test_register_creates_member_user(self):
|
|
result = self.service.register("member@example.com", "123456", "测试成员")
|
|
|
|
self.assertEqual(result["username"], "member@example.com")
|
|
self.assertEqual(result["display_name"], "测试成员")
|
|
self.assertEqual(result["role"], "member")
|
|
self.assertIsNotNone(self.service.repository.get_by_username("member@example.com"))
|
|
self.assertEqual(self.stub_tenant_service.ensure_default_tenant_calls, 1)
|
|
self.assertEqual(self.service.audit_log_service.events[0]["action"], "register")
|
|
self.assertEqual(self.service.audit_log_service.events[0]["resource_type"], "user")
|
|
|
|
def test_register_rejects_blank_display_name(self):
|
|
with self.assertRaises(HTTPException) as context:
|
|
self.service.register("member@example.com", "123456", " ")
|
|
|
|
self.assertEqual(context.exception.status_code, 400)
|
|
self.assertEqual(context.exception.detail, "display name is required")
|
|
|
|
def test_register_rejects_short_password(self):
|
|
with self.assertRaises(HTTPException) as context:
|
|
self.service.register("member@example.com", "12345", "测试成员")
|
|
|
|
self.assertEqual(context.exception.status_code, 400)
|
|
self.assertEqual(context.exception.detail, "password must be at least 6 characters")
|
|
|
|
def test_register_then_login_works_with_same_credentials(self):
|
|
self.service.register("member@example.com", "123456", "测试成员")
|
|
|
|
result = self.service.login("member@example.com", "123456")
|
|
|
|
self.assertEqual(result["user"]["username"], "member@example.com")
|
|
self.assertEqual(len(self.service.audit_log_service.events), 2)
|
|
self.assertEqual(self.service.audit_log_service.events[0]["action"], "register")
|
|
self.assertEqual(self.service.audit_log_service.events[1]["action"], "login")
|
|
|
|
def test_login_records_audit_event(self):
|
|
result = self.service.login("admin@example.com", "123456")
|
|
|
|
self.assertEqual(result["user"]["id"], "user-admin")
|
|
self.assertEqual(len(self.service.audit_log_service.events), 1)
|
|
event = self.service.audit_log_service.events[0]
|
|
self.assertEqual(event["action"], "login")
|
|
self.assertEqual(event["user_id"], "user-admin")
|
|
self.assertEqual(event["resource_type"], "user")
|
|
self.assertEqual(event["resource_id"], "user-admin")
|
|
|
|
def test_login_rejects_invalid_credentials_with_chinese_message(self):
|
|
with self.assertRaises(HTTPException) as context:
|
|
self.service.login("admin@example.com", "wrong-password")
|
|
|
|
self.assertEqual(context.exception.status_code, 401)
|
|
self.assertEqual(context.exception.detail, "账号或密码错误")
|
|
|
|
def test_seed_default_admin_existing_user_does_not_return_default_password(self):
|
|
result = self.service.seed_default_admin()
|
|
|
|
self.assertEqual(result["username"], "admin@example.com")
|
|
self.assertFalse(result["created"])
|
|
self.assertIsNone(result["password"])
|
|
|
|
def test_update_current_user_updates_display_name_and_password(self):
|
|
previous_password_hash = self.service.repository.user.password_hash
|
|
authorization = f"Bearer {create_access_token('user-admin')}"
|
|
|
|
result = self.service.update_current_user(authorization, "法务负责人", "654321")
|
|
|
|
self.assertEqual(result["display_name"], "法务负责人")
|
|
self.assertEqual(self.service.repository.user.display_name, "法务负责人")
|
|
self.assertNotEqual(self.service.repository.user.password_hash, previous_password_hash)
|
|
profile_events = [event for event in self.service.audit_log_service.events if event["action"] == "update_profile"]
|
|
self.assertEqual(len(profile_events), 1)
|
|
self.assertEqual(profile_events[0]["resource_type"], "user")
|
|
self.assertEqual(profile_events[0]["detail_json"]["username"], "admin@example.com")
|
|
self.assertEqual(profile_events[0]["detail_json"]["display_name"], "法务负责人")
|
|
self.assertTrue(profile_events[0]["detail_json"]["password_updated"])
|
|
self.assertNotIn("654321", str(profile_events[0]["detail_json"]))
|
|
|
|
def test_update_current_user_rejects_blank_display_name(self):
|
|
authorization = f"Bearer {create_access_token('user-admin')}"
|
|
|
|
with self.assertRaises(HTTPException) as context:
|
|
self.service.update_current_user(authorization, " ", None)
|
|
|
|
self.assertEqual(context.exception.status_code, 400)
|
|
self.assertEqual(context.exception.detail, "display name is required")
|
|
|
|
def test_update_current_user_rejects_short_password(self):
|
|
authorization = f"Bearer {create_access_token('user-admin')}"
|
|
|
|
with self.assertRaises(HTTPException) as context:
|
|
self.service.update_current_user(authorization, "法务负责人", "12345")
|
|
|
|
self.assertEqual(context.exception.status_code, 400)
|
|
self.assertEqual(context.exception.detail, "password must be at least 6 characters")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|