500 行
18 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|