文件
OmniCard/backend/test/modules/team/members.test.ts

500 行
18 KiB
TypeScript

import { BaseController, CoolCommException } from '@cool-midway/core';
import { BaseSysDepartmentEntity } from '../../../src/modules/base/entity/sys/department';
import { BaseSysUserEntity } from '../../../src/modules/base/entity/sys/user';
import { CardInfoEntity } from '../../../src/modules/card/entity/info';
import {
AdminTeamMembersController,
adminTeamMembersCurdOption,
} from '../../../src/modules/team/controller/admin/members';
import { TeamMembersService } from '../../../src/modules/team/service/members';
describe('Point 17: 后台组织架构、部门树与成员名片分配 (Team Members)', () => {
// ==========================================
// 1. 控制器路由与装饰器配置约定
// ==========================================
describe('1. 控制器装饰器与路由约定', () => {
it('控制器应继承 BaseController 并配置 CRUD', () => {
expect(AdminTeamMembersController.prototype).toBeInstanceOf(BaseController);
expect(adminTeamMembersCurdOption.entity).toBe(BaseSysUserEntity);
expect(adminTeamMembersCurdOption.service).toBe(TeamMembersService);
});
it('serviceApis 必须包含关键服务方法暴露', () => {
const methods = (adminTeamMembersCurdOption.serviceApis || []).map(
(api: { method: string }) => api.method
);
expect(methods).toContain('getDepartmentTree');
expect(methods).toContain('assignCard');
expect(methods).toContain('moveMembers');
expect(methods).toContain('getMemberPage');
expect(methods).toContain('addDepartment');
expect(methods).toContain('updateDepartment');
expect(methods).toContain('deleteDepartment');
});
});
// ==========================================
// 2. 部门树层级构建与 CRUD
// ==========================================
describe('2. 部门树层级构建与部门管理 (TeamMembersService)', () => {
let service: TeamMembersService;
let departments: BaseSysDepartmentEntity[];
let users: BaseSysUserEntity[];
let cards: CardInfoEntity[];
beforeEach(() => {
departments = [];
users = [];
cards = [];
// 预置部门数据:总部 (1) -> 研发中心 (2) -> 前端组 (3); 总部 (1) -> 国际业务拓展部 (4)
const d1 = new BaseSysDepartmentEntity();
d1.id = 1;
d1.name = '全球商务总部';
d1.parentId = null as unknown as number;
d1.orderNum = 1;
const d2 = new BaseSysDepartmentEntity();
d2.id = 2;
d2.name = '国际研发中心';
d2.parentId = 1;
d2.orderNum = 10;
const d3 = new BaseSysDepartmentEntity();
d3.id = 3;
d3.name = '前端终端架构组';
d3.parentId = 2;
d3.orderNum = 5;
const d4 = new BaseSysDepartmentEntity();
d4.id = 4;
d4.name = '国际业务拓展部';
d4.parentId = 1;
d4.orderNum = 20;
departments.push(d1, d2, d3, d4);
// 预置成员数据
const u1 = new BaseSysUserEntity();
u1.id = 101;
u1.name = '张总监';
u1.username = 'alex_zhang';
u1.departmentId = 1;
u1.status = 1;
const u2 = new BaseSysUserEntity();
u2.id = 102;
u2.name = '李架构';
u2.username = 'leo_li';
u2.departmentId = 3;
u2.status = 1;
const u3 = new BaseSysUserEntity();
u3.id = 103;
u3.name = '王商务';
u3.username = 'sarah_wang';
u3.departmentId = 4;
u3.status = 1;
users.push(u1, u2, u3);
// 预置名片数据
const c1 = new CardInfoEntity();
c1.id = 801;
c1.name = 'Alex Zhang';
c1.title = 'Managing Director';
c1.userId = 101;
c1.slug = 'alex-zhang';
c1.status = 'published';
const c2 = new CardInfoEntity();
c2.id = 802;
c2.name = 'Sarah Wang';
c2.title = 'Global BD Lead';
c2.userId = null as unknown as number;
c2.slug = 'sarah-wang';
c2.status = 'published';
cards.push(c1, c2);
// Mock Repositories
const mockDeptRepo = {
find: jest.fn(async () => [...departments]),
findOneBy: jest.fn(async (query: { id: number }) => {
return departments.find(d => d.id === Number(query.id)) || null;
}),
save: jest.fn(async (entity: BaseSysDepartmentEntity) => {
if (!entity.id) {
entity.id = departments.length ? Math.max(...departments.map(d => d.id)) + 1 : 1;
departments.push(entity);
return entity;
}
const idx = departments.findIndex(d => d.id === entity.id);
if (idx >= 0) {
Object.assign(departments[idx], entity);
return departments[idx];
}
departments.push(entity);
return entity;
}),
delete: jest.fn(async (id: number) => {
const idx = departments.findIndex(d => d.id === Number(id));
if (idx >= 0) {
departments.splice(idx, 1);
}
return { affected: 1 };
}),
};
const mockUserRepo = {
find: jest.fn(async () => [...users]),
findOneBy: jest.fn(async (query: { id: number }) => {
return users.find(u => u.id === Number(query.id)) || null;
}),
findAndCount: jest.fn(async () => [users, users.length]),
update: jest.fn(async (criteria: { id: unknown } | unknown, values: Partial<BaseSysUserEntity>) => {
let targetIds: number[] = [];
if (criteria && typeof criteria === 'object' && '_type' in criteria) {
// In(...) TypeORM operator
targetIds = (criteria as unknown as { _value: number[] })._value;
} else if (typeof criteria === 'number') {
targetIds = [criteria];
}
for (const u of users) {
if (targetIds.includes(u.id)) {
Object.assign(u, values);
}
}
return { affected: targetIds.length };
}),
};
const mockCardRepo = {
find: jest.fn(async () => [...cards]),
findOneBy: jest.fn(async (query: { id: number }) => {
return cards.find(c => c.id === Number(query.id)) || null;
}),
save: jest.fn(async (entity: CardInfoEntity) => {
const idx = cards.findIndex(c => c.id === entity.id);
if (idx >= 0) {
Object.assign(cards[idx], entity);
return cards[idx];
}
cards.push(entity);
return entity;
}),
};
service = new TeamMembersService();
service.baseSysDepartmentEntity = mockDeptRepo as unknown as TeamMembersService['baseSysDepartmentEntity'];
service.baseSysUserEntity = mockUserRepo as unknown as TeamMembersService['baseSysUserEntity'];
service.cardInfoEntity = mockCardRepo as unknown as TeamMembersService['cardInfoEntity'];
});
it('2.1 getDepartmentTree 正确构建多级部门树,按 orderNum 升序并统计人数', async () => {
const tree = await service.getDepartmentTree();
expect(Array.isArray(tree)).toBe(true);
expect(tree.length).toBe(1); // 根部门:全球商务总部 (1)
const root = tree[0];
expect(root.id).toBe(1);
expect(root.title).toBe('全球商务总部');
expect(root.children?.length).toBe(2);
// children 排序应为 国际研发中心 (orderNum: 10) 和 国际业务拓展部 (orderNum: 20)
expect(root.children?.[0].id).toBe(2);
expect(root.children?.[0].children?.length).toBe(1); // 前端组 (3)
expect(root.children?.[0].children?.[0].id).toBe(3);
expect(root.children?.[1].id).toBe(4);
// 人数统计验证
expect(root.memberCount).toBe(1); // u1 在总部
expect(root.children?.[0].children?.[0].memberCount).toBe(1); // u2 在前端组
expect(root.children?.[1].memberCount).toBe(1); // u3 在国际业务部
});
it('2.2 addDepartment 校验部门名称必填并成功入库', async () => {
await expect(service.addDepartment({ name: '' })).rejects.toThrow(CoolCommException);
const added = await service.addDepartment({
name: '中东迪拜办公室',
parentId: 1,
orderNum: 30,
});
expect(added.id).toBeDefined();
expect(added.name).toBe('中东迪拜办公室');
expect(added.parentId).toBe(1);
const found = departments.find(d => d.name === '中东迪拜办公室');
expect(found).toBeDefined();
});
it('2.3 updateDepartment 更新指定部门信息', async () => {
const updated = await service.updateDepartment(3, {
name: '全栈与跨端技术组',
orderNum: 1,
});
expect(updated.name).toBe('全栈与跨端技术组');
expect(updated.orderNum).toBe(1);
});
it('2.4 deleteDepartment 删除部门,部门不存在时抛出异常', async () => {
await expect(service.deleteDepartment(9999)).rejects.toThrow(CoolCommException);
const res = await service.deleteDepartment(4);
expect(res.success).toBe(true);
expect(departments.some(d => d.id === 4)).toBe(false);
});
});
// ==========================================
// 3. 成员列表查询与名片关联
// ==========================================
describe('3. 成员分页列表与名片关联 (getMemberPage)', () => {
let service: TeamMembersService;
let users: BaseSysUserEntity[];
let cards: CardInfoEntity[];
let departments: BaseSysDepartmentEntity[];
beforeEach(() => {
users = [
Object.assign(new BaseSysUserEntity(), {
id: 1,
name: 'Alex Zhang',
username: 'alex',
phone: '+86 13800000001',
departmentId: 10,
status: 1,
}),
Object.assign(new BaseSysUserEntity(), {
id: 2,
name: 'Sarah Wang',
username: 'sarah',
phone: '+86 13800000002',
departmentId: 20,
status: 1,
}),
];
departments = [
Object.assign(new BaseSysDepartmentEntity(), { id: 10, name: '商务部' }),
Object.assign(new BaseSysDepartmentEntity(), { id: 20, name: '技术部' }),
];
cards = [
Object.assign(new CardInfoEntity(), {
id: 501,
userId: 1,
name: 'Alex Zhang',
title: 'Managing Director',
slug: 'alex',
status: 'published',
}),
];
service = new TeamMembersService();
service.baseSysDepartmentEntity = {
find: jest.fn(async () => departments),
} as unknown as TeamMembersService['baseSysDepartmentEntity'];
service.baseSysUserEntity = {
find: jest.fn(async () => users),
} as unknown as TeamMembersService['baseSysUserEntity'];
service.cardInfoEntity = {
find: jest.fn(async () => cards),
} as unknown as TeamMembersService['cardInfoEntity'];
});
it('3.1 正确返回成员列表并聚合已分配的名片信息与部门名称', async () => {
const result = await service.getMemberPage({ page: 1, size: 20 });
expect(result.pagination.total).toBe(2);
expect(result.list.length).toBe(2);
const member1 = result.list.find(m => m.id === 1);
expect(member1?.name).toBe('Alex Zhang');
expect(member1?.departmentName).toBe('商务部');
expect(member1?.card).toBeDefined();
expect(member1?.card?.id).toBe(501);
expect(member1?.card?.title).toBe('Managing Director');
const member2 = result.list.find(m => m.id === 2);
expect(member2?.name).toBe('Sarah Wang');
expect(member2?.departmentName).toBe('技术部');
expect(member2?.card).toBeNull();
});
it('3.2 按部门 ID 筛选成员', async () => {
const result = await service.getMemberPage({ departmentId: 10 });
expect(result.list.length).toBe(1);
expect(result.list[0].id).toBe(1);
});
it('3.3 按关键字模糊检索成员', async () => {
const result = await service.getMemberPage({ keyword: 'sarah' });
expect(result.list.length).toBe(1);
expect(result.list[0].username).toBe('sarah');
});
});
// ==========================================
// 4. 名片分配与批量调部
// ==========================================
describe('4. 名片分配 (assignCard) 与 批量调整部门 (moveMembers)', () => {
let service: TeamMembersService;
let users: BaseSysUserEntity[];
let cards: CardInfoEntity[];
let departments: BaseSysDepartmentEntity[];
beforeEach(() => {
users = [
Object.assign(new BaseSysUserEntity(), {
id: 11,
name: 'User 11',
departmentId: 1,
}),
Object.assign(new BaseSysUserEntity(), {
id: 12,
name: 'User 12',
departmentId: 1,
}),
];
departments = [
Object.assign(new BaseSysDepartmentEntity(), { id: 1, name: '原有部门' }),
Object.assign(new BaseSysDepartmentEntity(), { id: 2, name: '目标部门' }),
];
cards = [
Object.assign(new CardInfoEntity(), {
id: 901,
name: 'VIP 名片',
userId: null as unknown as number,
}),
];
service = new TeamMembersService();
service.baseSysDepartmentEntity = {
findOneBy: jest.fn(async (q: { id: number }) =>
departments.find(d => d.id === Number(q.id)) || null
),
} as unknown as TeamMembersService['baseSysDepartmentEntity'];
service.baseSysUserEntity = {
findOneBy: jest.fn(async (q: { id: number }) =>
users.find(u => u.id === Number(q.id)) || null
),
find: jest.fn(async () => users),
update: jest.fn(async () => ({ affected: 2 })),
} as unknown as TeamMembersService['baseSysUserEntity'];
service.cardInfoEntity = {
findOneBy: jest.fn(async (q: { id: number }) =>
cards.find(c => c.id === Number(q.id)) || null
),
save: jest.fn(async (c: CardInfoEntity) => {
const idx = cards.findIndex(item => item.id === c.id);
if (idx >= 0) {
cards[idx] = c;
}
return c;
}),
} as unknown as TeamMembersService['cardInfoEntity'];
});
it('4.1 assignCard 将指定名片分配给指定成员', async () => {
const res = await service.assignCard(11, 901);
expect(res.success).toBe(true);
expect(res.memberId).toBe(11);
expect(res.cardId).toBe(901);
const targetCard = cards.find(c => c.id === 901);
expect(targetCard?.userId).toBe(11);
});
it('4.2 assignCard 兼容参数对象形式调用', async () => {
const res = await service.assignCard({ memberId: 12, cardId: 901 });
expect(res.success).toBe(true);
expect(res.memberId).toBe(12);
expect(res.cardId).toBe(901);
});
it('4.3 assignCard 成员不存在或名片不存在时抛出异常', async () => {
await expect(service.assignCard(999, 901)).rejects.toThrow('成员不存在');
await expect(service.assignCard(11, 9999)).rejects.toThrow('名片不存在');
});
it('4.4 moveMembers 批量更新成员所属部门', async () => {
const res = await service.moveMembers([11, 12], 2);
expect(res.success).toBe(true);
expect(res.departmentId).toBe(2);
expect(res.memberIds).toEqual([11, 12]);
});
it('4.5 moveMembers 校验成员列表非空且目标部门必须存在', async () => {
await expect(service.moveMembers([], 2)).rejects.toThrow('未指定成员列表');
await expect(service.moveMembers([11], 9999)).rejects.toThrow('目标部门不存在');
});
});
// ==========================================
// 5. 控制器方法暴露与响应结构
// ==========================================
describe('5. 控制器路由调用测试 (AdminTeamMembersController)', () => {
let controller: AdminTeamMembersController;
let mockService: TeamMembersService;
beforeEach(() => {
mockService = new TeamMembersService();
mockService.getDepartmentTree = jest.fn(async () => [
{ id: 1, name: '总部', children: [] },
]);
mockService.assignCard = jest.fn(async (mId, cId) => ({
success: true,
memberId: typeof mId === 'object' ? Number(mId.memberId) : Number(mId),
cardId: typeof mId === 'object' ? Number(mId.cardId) : Number(cId),
card: new CardInfoEntity(),
}));
mockService.moveMembers = jest.fn(async (ids, dId) => ({
success: true,
updatedCount: Array.isArray(ids) ? ids.length : (ids.memberIds?.length || 0),
memberIds: Array.isArray(ids) ? ids : (ids.memberIds || []),
departmentId: Array.isArray(ids) ? Number(dId) : Number(ids.departmentId),
}));
controller = new AdminTeamMembersController();
controller.teamMembersService = mockService;
controller.ok = (data: unknown) => ({
code: 1000,
message: 'success',
data,
});
});
it('postDepartmentTree 返回 code 1000', async () => {
const res = await controller.postDepartmentTree();
expect(res.code).toBe(1000);
expect(mockService.getDepartmentTree).toHaveBeenCalled();
});
it('postAssignCard 返回 code 1000', async () => {
const res = await controller.postAssignCard({ memberId: 10, cardId: 20 });
expect(res.code).toBe(1000);
expect(mockService.assignCard).toHaveBeenCalledWith(10, 20);
});
it('postMoveMembers 返回 code 1000', async () => {
const res = await controller.postMoveMembers({
memberIds: [10, 11],
departmentId: 5,
});
expect(res.code).toBe(1000);
expect(mockService.moveMembers).toHaveBeenCalledWith([10, 11], 5);
});
});
});