jest.mock('@/lib/prisma', () => ({ prisma: { systemSettings: { findFirst: jest.fn(), }, user: { findUnique: jest.fn(), }, }, })) jest.mock('@/lib/dingtalk-login-token', () => ({ consumeDingtalkLoginToken: jest.fn(), })) jest.mock('@/lib/system-init', () => ({ isInitialAdminPasswordChangeRequired: jest.fn(), })) jest.mock('bcryptjs', () => ({ compare: jest.fn(), })) import { authOptions } from '../auth' import { consumeDingtalkLoginToken } from '../dingtalk-login-token' import { isInitialAdminPasswordChangeRequired } from '../system-init' import { prisma } from '../prisma' import bcrypt from 'bcryptjs' const mockConsumeDingtalkLoginToken = consumeDingtalkLoginToken as jest.MockedFunction const mockIsInitialAdminPasswordChangeRequired = isInitialAdminPasswordChangeRequired as jest.MockedFunction function getDingtalkProvider() { const provider = authOptions.providers.find((item: any) => item.id === 'credentials') as any if (!provider) { throw new Error('credentials provider missing') } return provider } describe('auth dingtalk-token provider', () => { beforeEach(() => { jest.clearAllMocks() ;(prisma.systemSettings.findFirst as jest.Mock).mockResolvedValue(null) mockIsInitialAdminPasswordChangeRequired.mockResolvedValue(false) }) it('使用一次性钉钉票据登录并返回会话用户', async () => { mockConsumeDingtalkLoginToken.mockResolvedValue({ id: 'user-1', email: 'user@example.com', username: '张三', role: 'USER', isSuperAdmin: false, avatar: null, isActive: true, }) const user = await getDingtalkProvider().options.authorize({ dingtalkToken: 'one-time-token' }) expect(mockConsumeDingtalkLoginToken).toHaveBeenCalledWith('one-time-token', 'login') expect(user).toEqual(expect.objectContaining({ id: 'user-1', email: 'user@example.com', name: '张三', role: 'USER', })) expect(user).toEqual(expect.objectContaining({ mustChangePassword: false, })) }) it('拒绝无效或已消费的钉钉票据', async () => { mockConsumeDingtalkLoginToken.mockResolvedValue(null) await expect(getDingtalkProvider().options.authorize({ dingtalkToken: 'bad-token' })) .rejects .toThrow('钉钉登录凭证已失效') }) it('拒绝已禁用用户使用钉钉票据登录', async () => { mockConsumeDingtalkLoginToken.mockResolvedValue({ id: 'user-disabled', email: 'disabled@example.com', username: '禁用用户', role: 'USER', isSuperAdmin: false, avatar: null, isActive: false, }) await expect(getDingtalkProvider().options.authorize({ dingtalkToken: 'disabled-token' })) .rejects .toThrow('账号未激活或已被禁用') }) it('拒绝已禁用用户使用账号密码登录,即使该用户是普通管理员', async () => { ;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ id: 'admin-disabled', email: 'disabled-admin@example.com', username: '禁用管理员', password: 'hashed-password', role: 'ADMIN', isSuperAdmin: false, avatar: null, isActive: false, }) ;(bcrypt.compare as jest.Mock).mockResolvedValue(true) await expect( getDingtalkProvider().options.authorize({ email: 'disabled-admin@example.com', password: 'plain-password', }) ) .rejects .toThrow('账号未激活或已被禁用') }) it('旧 JWT 会话遇到已禁用用户时会在下次校验失效', async () => { ;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ id: 'user-disabled', role: 'USER', isSuperAdmin: false, isActive: false, avatar: null, }) await expect( (authOptions.callbacks?.jwt as any)({ token: { id: 'user-disabled', avatarUpdatedAt: Date.now() }, }) ) .rejects .toThrow('账号不存在或已被禁用') expect(prisma.user.findUnique).toHaveBeenCalledWith({ where: { id: 'user-disabled' }, select: { avatar: true, role: true, isSuperAdmin: true, isActive: true, }, }) }) })