2026-08-20 13:34:01 +08:00

231 lines
6.9 KiB
TypeScript

import { getServerSession } from 'next-auth'
import { prisma } from '@/lib/prisma'
jest.mock('next-auth', () => ({
getServerSession: jest.fn(),
}))
jest.mock('@/lib/auth', () => ({
authOptions: {},
}))
jest.mock('@/lib/prisma', () => ({
prisma: {
organizationUnit: {
findFirst: jest.fn(),
findMany: jest.fn(),
},
performanceSurveyEmployeeProfile: {
findMany: jest.fn(),
},
user: {
findMany: jest.fn(),
},
},
}))
jest.mock('@/lib/department-permission', () => ({
isAdmin: jest.fn((role: string) => ['ADMIN', 'SYSTEM_ADMIN', 'DEPT_ADMIN'].includes(role)),
isSystemAdmin: jest.fn((role: string) => ['ADMIN', 'SYSTEM_ADMIN'].includes(role)),
isDepartmentAdmin: jest.fn((role: string) => role === 'DEPT_ADMIN'),
}))
const { TextDecoder, TextEncoder } = require('util')
const { clearImmediate, setImmediate } = require('timers')
Object.assign(globalThis, { TextDecoder, TextEncoder, clearImmediate, setImmediate })
const edgePrimitives = require('next/dist/compiled/@edge-runtime/primitives')
Object.assign(globalThis, {
Request: edgePrimitives.Request,
Response: edgePrimitives.Response,
Headers: edgePrimitives.Headers,
})
const { GET } = require('@/app/api/organization/route') as typeof import('@/app/api/organization/route')
const companyPath = '江苏简尚品牌管理有限公司'
const brandPath = `${companyPath}/品牌中心`
const supportPath = `${companyPath}/业务支持`
const units = [
{
id: 'company',
name: '江苏简尚品牌管理有限公司',
path: companyPath,
level: 0,
parentId: null,
sourceDeptId: 1,
},
{
id: 'brand',
name: '品牌中心',
path: brandPath,
level: 1,
parentId: 'company',
sourceDeptId: 2,
},
{
id: 'support',
name: '业务支持',
path: supportPath,
level: 1,
parentId: 'company',
sourceDeptId: 3,
},
]
function createProfile(overrides: Record<string, unknown> = {}) {
return {
id: 'profile-brand',
displayName: '花名册员工',
realName: '花名册实名',
jobNumber: 'HR001',
departmentPath: brandPath,
departmentName: '品牌中心',
positionTitle: '设计师',
positionLevel: 'P2',
levelType: 'P',
managerName: '负责人',
isLeadership: false,
employmentState: 'ACTIVE',
sourcePayload: null,
departmentMemberships: [],
userBindings: [],
...overrides,
}
}
function request(query = '') {
return { url: `https://sc.jiansun.vip/api/organization${query}` } as any
}
describe('/api/organization HR 花名册口径', () => {
beforeEach(() => {
jest.clearAllMocks()
;(getServerSession as jest.Mock).mockResolvedValue({
user: { id: 'admin-1', role: 'SYSTEM_ADMIN' },
})
;(prisma.organizationUnit.findMany as jest.Mock).mockResolvedValue(units)
;(prisma.organizationUnit.findFirst as jest.Mock).mockResolvedValue(null)
;(prisma.user.findMany as jest.Mock).mockResolvedValue([])
})
test('未注册素材库账号的 HR 员工仍进入组织树和人员明细', async () => {
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValue([
createProfile(),
createProfile({
id: 'profile-bound',
displayName: '已绑定员工',
realName: 'HR 权威实名',
jobNumber: 'HR002',
userBindings: [
{
userId: 'user-2',
user: { username: 'material-user', isActive: true },
},
],
}),
])
const response = await GET(request())
const body = await response.json()
expect(response.status).toBe(200)
expect(body.summary).toEqual(expect.objectContaining({
totalEmployees: 2,
activeEmployees: 2,
visibleCount: 2,
scope: 'all',
}))
expect(body.employees).toEqual(expect.arrayContaining([
expect.objectContaining({
employeeProfileId: 'profile-brand',
userId: null,
username: null,
realName: '花名册实名',
accountIsActive: null,
}),
expect.objectContaining({
employeeProfileId: 'profile-bound',
userId: 'user-2',
username: 'material-user',
realName: 'HR 权威实名',
accountIsActive: true,
}),
]))
expect(body.tree[0]).toEqual(expect.objectContaining({ count: 2 }))
})
test('离职人员默认隐藏,明确选择后才显示', async () => {
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValue([
createProfile(),
createProfile({
id: 'profile-resigned',
displayName: '离职员工',
jobNumber: 'HR003',
employmentState: 'RESIGNED',
}),
])
const defaultResponse = await GET(request())
const defaultBody = await defaultResponse.json()
expect(defaultBody.employees).toHaveLength(1)
expect(defaultBody.summary).toEqual(expect.objectContaining({
totalEmployees: 2,
activeEmployees: 1,
inactiveEmployees: 1,
visibleCount: 1,
}))
const allResponse = await GET(request('?includeInactive=true'))
const allBody = await allResponse.json()
expect(allBody.employees).toHaveLength(2)
expect(allBody.summary.visibleCount).toBe(2)
})
test('部门管理员只获得本部门 HR 人员与本部门统计', async () => {
;(getServerSession as jest.Mock).mockResolvedValue({
user: { id: 'dept-1', role: 'DEPT_ADMIN', managedDepartment: '品牌中心' },
})
;(prisma.organizationUnit.findFirst as jest.Mock).mockResolvedValue({ path: brandPath })
;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValue([
createProfile(),
createProfile({
id: 'profile-support',
displayName: '业务支持员工',
jobNumber: 'HR004',
departmentPath: supportPath,
departmentName: '业务支持',
}),
])
const response = await GET(request())
const body = await response.json()
expect(response.status).toBe(200)
expect(body.employees).toHaveLength(1)
expect(body.employees[0].employeeProfileId).toBe('profile-brand')
expect(body.summary).toEqual(expect.objectContaining({
totalEmployees: 1,
activeEmployees: 1,
visibleCount: 1,
scope: brandPath,
}))
expect(body.unbound).toEqual([])
expect(prisma.user.findMany).not.toHaveBeenCalled()
})
test('部门管理员缺少有效管理范围时拒绝访问而不是回退全公司', async () => {
;(getServerSession as jest.Mock).mockResolvedValue({
user: { id: 'dept-1', role: 'DEPT_ADMIN', managedDepartment: '不存在的部门' },
})
;(prisma.organizationUnit.findFirst as jest.Mock).mockResolvedValue(null)
const response = await GET(request())
const body = await response.json()
expect(response.status).toBe(403)
expect(body.error).toBe('未找到可访问部门范围')
expect(prisma.performanceSurveyEmployeeProfile.findMany).not.toHaveBeenCalled()
})
})