119 lines
6.9 KiB
TypeScript
119 lines
6.9 KiB
TypeScript
/** @jest-environment node */
|
||
|
||
import { prisma } from '@/lib/prisma'
|
||
import { getEmployeeByUserId, getEmployeeOrganizationPathsByUserId } from '@/lib/employee-directory'
|
||
import { canUserAccessPrivateTaskDetail } from '@/lib/user-permission-inherit'
|
||
import { canAccessOperationsGMaterials, resolveMaterialUploadAccessScope, OPERATIONS_G_SOURCE_DEPT_ID } from '@/lib/material-organization-access'
|
||
import { canAccessMaterialPrivacy, buildMaterialPrivacyWhere } from '@/lib/task-detail-privacy'
|
||
import { buildMaterialWhereFromSearchParams } from '@/lib/material-query'
|
||
import { normalizeTaskDetailPrivacyFields } from '@/lib/task-detail-privacy-fields'
|
||
|
||
jest.mock('@/lib/prisma', () => ({ prisma: { organizationUnit: { findUnique: jest.fn() } } }))
|
||
jest.mock('@/lib/employee-directory', () => ({
|
||
getEmployeeByUserId: jest.fn(), getEmployeeOrganizationPathsByUserId: jest.fn(),
|
||
}))
|
||
jest.mock('@/lib/user-permission-inherit', () => ({ canUserAccessPrivateTaskDetail: jest.fn() }))
|
||
jest.mock('@/lib/department-permission', () => ({
|
||
isSuperAdmin: jest.fn(), isSystemAdmin: jest.fn(), getUserViewableMaterialTypes: jest.fn(),
|
||
}))
|
||
|
||
const root = '江苏简尚品牌管理有限公司/运营中心G'
|
||
const exclusive = { accessScope: 'operations_g', userId: 'uploader', taskDetailRequesterUserId: 'requester', taskDetailPrivate: false }
|
||
|
||
beforeEach(() => {
|
||
jest.clearAllMocks()
|
||
;(prisma.organizationUnit.findUnique as jest.Mock).mockResolvedValue({ path: root, isActive: true })
|
||
;(getEmployeeByUserId as jest.Mock).mockResolvedValue({ employmentState: 'ACTIVE' })
|
||
;(getEmployeeOrganizationPathsByUserId as jest.Mock).mockResolvedValue([`${root}/运营七部(HZ)/设计组`])
|
||
;(canUserAccessPrivateTaskDetail as jest.Mock).mockResolvedValue(false)
|
||
})
|
||
|
||
test.each([root, `${root}/运营七部(HZ)/设计组`, `${root}/运营一部(CZ)/京东一组`])(
|
||
'整个中心及其下属部门可以访问:%s', async (memberPath) => {
|
||
;(getEmployeeOrganizationPathsByUserId as jest.Mock).mockResolvedValue([memberPath])
|
||
expect(await canAccessOperationsGMaterials('member')).toBe(true)
|
||
expect(prisma.organizationUnit.findUnique).toHaveBeenCalledWith({
|
||
where: { sourceDeptId: OPERATIONS_G_SOURCE_DEPT_ID }, select: { path: true, isActive: true },
|
||
})
|
||
},
|
||
)
|
||
|
||
test.each([{ paths: [] }, { paths: ['江苏简尚品牌管理有限公司/运营中心A'] }, { paths: [`${root}二部/设计组`] }])(
|
||
'其他中心、无绑定及相似名称不能获得专属权限:%j', async ({ paths }) => {
|
||
;(getEmployeeOrganizationPathsByUserId as jest.Mock).mockResolvedValue(paths)
|
||
expect(await canAccessOperationsGMaterials('other')).toBe(false)
|
||
},
|
||
)
|
||
|
||
test('离职成员和失效组织均失去专属资格', async () => {
|
||
;(getEmployeeByUserId as jest.Mock).mockResolvedValue({ employmentState: 'RESIGNED' })
|
||
expect(await canAccessOperationsGMaterials('member')).toBe(false)
|
||
;(getEmployeeByUserId as jest.Mock).mockResolvedValue({ employmentState: 'ACTIVE' })
|
||
;(prisma.organizationUnit.findUnique as jest.Mock).mockResolvedValue({ path: root, isActive: false })
|
||
expect(await canAccessOperationsGMaterials('member')).toBe(false)
|
||
})
|
||
|
||
test('保留现有组织成员口径,已绑定成员的未知在职状态不等同于离职', async () => {
|
||
;(getEmployeeByUserId as jest.Mock).mockResolvedValue({ employmentState: 'UNKNOWN' })
|
||
expect(await canAccessOperationsGMaterials('member')).toBe(true)
|
||
})
|
||
|
||
test('稳定部门编号绑定当前组织路径,组织改名后仍使用当前边界', async () => {
|
||
const renamed = '江苏简尚品牌管理有限公司/运营中心G新名称'
|
||
;(prisma.organizationUnit.findUnique as jest.Mock).mockResolvedValue({ path: renamed, isActive: true })
|
||
;(getEmployeeOrganizationPathsByUserId as jest.Mock).mockResolvedValue([`${renamed}/设计组`])
|
||
expect(await canAccessOperationsGMaterials('member')).toBe(true)
|
||
})
|
||
|
||
test('专属素材在中心内共享,无需个人私密授权', async () => {
|
||
expect(await canAccessMaterialPrivacy(exclusive, 'any-g-member')).toBe(true)
|
||
expect(canUserAccessPrivateTaskDetail).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test.each(['requester', 'uploader', 'super-admin'])('中心外不能凭需求人、上传者或管理员身份访问:%s', async (userId) => {
|
||
;(getEmployeeOrganizationPathsByUserId as jest.Mock).mockResolvedValue(['江苏简尚品牌管理有限公司/品牌中心'])
|
||
;(canUserAccessPrivateTaskDetail as jest.Mock).mockResolvedValue(true)
|
||
expect(await canAccessMaterialPrivacy(exclusive, userId)).toBe(false)
|
||
expect(canUserAccessPrivateTaskDetail).not.toHaveBeenCalled()
|
||
})
|
||
|
||
test('匿名访问专属素材被拒绝,通用素材保留原有准入', async () => {
|
||
expect(await canAccessMaterialPrivacy(exclusive, null)).toBe(false)
|
||
expect(await canAccessMaterialPrivacy({ accessScope: 'general' }, null)).toBe(true)
|
||
})
|
||
|
||
test('上传校验拒绝非法范围和中心外专属上传', async () => {
|
||
await expect(resolveMaterialUploadAccessScope('any-department', 'member')).rejects.toMatchObject({ status: 400 })
|
||
;(getEmployeeOrganizationPathsByUserId as jest.Mock).mockResolvedValue([])
|
||
await expect(resolveMaterialUploadAccessScope('operations_g', 'other')).rejects.toMatchObject({ status: 403 })
|
||
expect(await resolveMaterialUploadAccessScope(undefined, 'other')).toBe('general')
|
||
})
|
||
|
||
test('专属素材不能叠加个人私密,不受新品或优化限制', () => {
|
||
expect(() => normalizeTaskDetailPrivacyFields({ accessScope: 'operations_g', taskDetailPrivate: true })).toThrow('不能设为个人私密')
|
||
for (const taskDetailDemandType of ['新品', '优化']) {
|
||
expect(normalizeTaskDetailPrivacyFields({ accessScope: 'operations_g', taskDetailDemandType }).taskDetailPrivate).toBe(false)
|
||
}
|
||
})
|
||
|
||
test('中心内可浏览各类型专属素材,通用素材继续遵守原类型权限', async () => {
|
||
const result = await buildMaterialWhereFromSearchParams(new URLSearchParams('materialType=视频&search=SPU'), { id: 'member' }, {
|
||
context: { canAccessDeleted: false, expandedViewableTypes: ['任务-详情'], isSuperAdmin: false,
|
||
canAccessOperationsGMaterials: true, searchOrConditions: [{ productModel: { contains: 'SPU' } }] },
|
||
})
|
||
expect(result.empty).toBe(false)
|
||
expect(result.where.AND[0].OR).toEqual(expect.arrayContaining([
|
||
{ accessScope: 'general', taskDetailPrivate: false, materialType: { in: ['任务-详情'] } },
|
||
{ accessScope: 'operations_g' },
|
||
]))
|
||
expect(result.where.OR).toEqual([{ productModel: { contains: 'SPU' } }])
|
||
})
|
||
|
||
test('列表、收藏、月份计数的共用过滤不向中心外泄露专属条目', async () => {
|
||
;(getEmployeeOrganizationPathsByUserId as jest.Mock).mockResolvedValue([])
|
||
const outside = await buildMaterialPrivacyWhere('other')
|
||
expect(outside.OR.every((branch: any) => branch.accessScope === 'general')).toBe(true)
|
||
;(getEmployeeOrganizationPathsByUserId as jest.Mock).mockResolvedValue([root])
|
||
expect((await buildMaterialPrivacyWhere('member')).OR).toContainEqual({ accessScope: 'operations_g' })
|
||
})
|