147 lines
4.2 KiB
TypeScript
147 lines
4.2 KiB
TypeScript
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<typeof consumeDingtalkLoginToken>
|
|
const mockIsInitialAdminPasswordChangeRequired = isInitialAdminPasswordChangeRequired as jest.MockedFunction<typeof isInitialAdminPasswordChangeRequired>
|
|
|
|
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,
|
|
},
|
|
})
|
|
})
|
|
})
|