/** @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' }) })