import { UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; import { hashPassword } from '../users/users.service'; function createUsersMock(roleCode: string, overrides: Record = {}) { const user = { id: 'user-1', tenantId: roleCode === 'enterprise_admin' ? 'tenant-1' : null, username: 'user', email: 'user@example.com', phone: '13800000000', displayName: '用户', passwordHash: hashPassword('secret1'), status: 'active', deletedAt: null, lockedUntil: null, tenant: { id: 'tenant-1', name: '企业A' }, roles: [{ role: { code: roleCode } }], ...overrides, }; return { findByLogin: jest.fn().mockResolvedValue(user), recordLoginSuccess: jest.fn(), recordLoginFailure: jest.fn(), }; } function createSessionsMock() { const record = { userId: 'user-1', portal: 'admin', sessionVersion: 0, createdAt: 1, lastActivityAt: 1, lastAuthenticatedAt: 1, absoluteExpiresAt: Date.now() + 1000, }; return { create: jest.fn().mockResolvedValue({ token: 'opaque-session-token', record }), publicSession: jest.fn().mockReturnValue({ idleTimeoutSeconds: 3600, absoluteExpiresAt: new Date(record.absoluteExpiresAt).toISOString() }), }; } async function loginWithCaptcha(service: AuthService, portal: 'admin' | 'client', password = 'secret1') { const captcha = service.createCaptcha(); const answer = captcha.challenge.split('=')[0].split('+').map((part) => Number(part.trim())).reduce((sum, value) => sum + value, 0); return service.login({ login: 'user@example.com', password, captchaId: captcha.captchaId, captchaText: String(answer), }, portal); } describe('AuthService', () => { it('allows platform admins to login admin portal', async () => { const users = createUsersMock('platform_admin'); const sessions = createSessionsMock(); const service = new AuthService(users as never, sessions as never); await expect(loginWithCaptcha(service, 'admin')).resolves.toEqual(expect.objectContaining({ portal: 'admin', sessionToken: 'opaque-session-token' })); expect(users.recordLoginSuccess).toHaveBeenCalledWith('user-1'); expect(sessions.create).toHaveBeenCalledWith('user-1', 'admin', 0); }); it('rejects enterprise admins on admin portal', async () => { const users = createUsersMock('enterprise_admin'); const service = new AuthService(users as never, createSessionsMock() as never); await expect(loginWithCaptcha(service, 'admin')).rejects.toBeInstanceOf(UnauthorizedException); expect(users.recordLoginFailure).toHaveBeenCalledWith('user-1'); }); it('locks user after five failed password attempts', async () => { const users = createUsersMock('platform_admin'); const service = new AuthService(users as never, createSessionsMock() as never); for (let index = 0; index < 5; index += 1) { await expect(loginWithCaptcha(service, 'admin', 'bad-password')).rejects.toBeInstanceOf(UnauthorizedException); } expect(users.recordLoginFailure).toHaveBeenCalledTimes(5); }); });