forked from wangziqi/gongxue-base
256 lines
9.1 KiB
TypeScript
256 lines
9.1 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import { SyncService, ImportUserDto } from './sync.service';
|
|
import { SyncLog, SyncState, UserDingMapping } from '../entities';
|
|
import { User } from '../entities/user.entity';
|
|
import { Student } from '../entities/student.entity';
|
|
import { Role } from '../entities/role.entity';
|
|
import { DingTalkService } from '../integration/dingtalk.service';
|
|
import { WeComService } from '../integration/wecom.service';
|
|
import { AttendanceImportService } from '../attendance/attendance-import.service';
|
|
import { ScheduleSyncService } from './schedule-sync.service';
|
|
|
|
describe('SyncService — new methods', () => {
|
|
let service: SyncService;
|
|
|
|
let mappingRepo: jest.Mocked<
|
|
Pick<Repository<UserDingMapping>, 'findOne' | 'create' | 'save' | 'find'>
|
|
>;
|
|
let userRepo: jest.Mocked<Pick<Repository<User>, 'create' | 'save'>>;
|
|
let studentRepo: jest.Mocked<Pick<Repository<Student>, 'create' | 'save'>>;
|
|
let roleRepo: jest.Mocked<Pick<Repository<Role>, 'findOne'>>;
|
|
|
|
let dingTalkService: jest.Mocked<Pick<DingTalkService, 'fetchOrgTreeWithUsers' | 'syncAll' | 'fetchOrgTree'>>;
|
|
|
|
beforeEach(async () => {
|
|
mappingRepo = {
|
|
findOne: jest.fn(),
|
|
create: jest.fn(),
|
|
save: jest.fn(),
|
|
find: jest.fn().mockResolvedValue([]),
|
|
};
|
|
|
|
userRepo = {
|
|
create: jest.fn(),
|
|
save: jest.fn(),
|
|
};
|
|
|
|
studentRepo = {
|
|
create: jest.fn(),
|
|
save: jest.fn(),
|
|
};
|
|
|
|
roleRepo = {
|
|
findOne: jest.fn(),
|
|
};
|
|
|
|
dingTalkService = {
|
|
fetchOrgTreeWithUsers: jest.fn(),
|
|
syncAll: jest.fn(),
|
|
fetchOrgTree: jest.fn(),
|
|
};
|
|
|
|
const mockWeComService = { syncAll: jest.fn() };
|
|
const mockAttendanceImportService = { importFromDingTalk: jest.fn() };
|
|
const mockScheduleSyncService = { syncAll: jest.fn(), getStatus: jest.fn() };
|
|
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
SyncService,
|
|
{ provide: getRepositoryToken(SyncLog), useValue: { create: jest.fn(), save: jest.fn(), find: jest.fn(), findOne: jest.fn() } },
|
|
{ provide: getRepositoryToken(SyncState), useValue: { findOne: jest.fn(), upsert: jest.fn() } },
|
|
{ provide: getRepositoryToken(UserDingMapping), useValue: mappingRepo },
|
|
{ provide: getRepositoryToken(User), useValue: userRepo },
|
|
{ provide: getRepositoryToken(Student), useValue: studentRepo },
|
|
{ provide: getRepositoryToken(Role), useValue: roleRepo },
|
|
{ provide: DingTalkService, useValue: dingTalkService },
|
|
{ provide: WeComService, useValue: mockWeComService },
|
|
{ provide: AttendanceImportService, useValue: mockAttendanceImportService },
|
|
{ provide: ScheduleSyncService, useValue: mockScheduleSyncService },
|
|
],
|
|
}).compile();
|
|
|
|
service = module.get<SyncService>(SyncService);
|
|
});
|
|
|
|
// ── getDingTalkOrgTreeWithUsers ──
|
|
|
|
it('getDingTalkOrgTreeWithUsers delegates to dingTalkService.fetchOrgTreeWithUsers', async () => {
|
|
const mockTree = [{ id: 1, name: 'root', parentId: 0, children: [], users: [] }];
|
|
dingTalkService.fetchOrgTreeWithUsers.mockResolvedValue(mockTree);
|
|
|
|
const result = await service.getDingTalkOrgTreeWithUsers(1);
|
|
|
|
expect(dingTalkService.fetchOrgTreeWithUsers).toHaveBeenCalledWith(1);
|
|
expect(result).toBe(mockTree);
|
|
});
|
|
|
|
// ── importDingTalkUsers ──
|
|
|
|
it('imports teacher when roleId is a number (role found)', async () => {
|
|
const mockRole = { id: 5, name: 'Teacher' } as Role;
|
|
roleRepo.findOne.mockResolvedValue(mockRole);
|
|
|
|
const mockUser = { id: 10 } as User;
|
|
userRepo.create.mockReturnValue(mockUser);
|
|
userRepo.save.mockResolvedValue(mockUser);
|
|
|
|
mappingRepo.findOne.mockResolvedValue(null);
|
|
mappingRepo.create.mockReturnValue({} as UserDingMapping);
|
|
mappingRepo.save.mockResolvedValue({} as UserDingMapping);
|
|
|
|
const users: ImportUserDto[] = [
|
|
{ dingUserId: 'user1', name: 'Zhang San', mobile: '13800001111', roleId: 5 },
|
|
];
|
|
|
|
const result = await service.importDingTalkUsers(users);
|
|
|
|
expect(result.teacherCount).toBe(1);
|
|
expect(result.studentCount).toBe(0);
|
|
expect(result.skipped).toBe(0);
|
|
|
|
expect(roleRepo.findOne).toHaveBeenCalledWith({ where: { id: 5 } });
|
|
expect(userRepo.save).toHaveBeenCalledWith(
|
|
expect.objectContaining({ roles: [mockRole] }),
|
|
);
|
|
});
|
|
|
|
it('imports teacher when roleId is a number but role not found (logs warning, no roles assigned)', async () => {
|
|
roleRepo.findOne.mockResolvedValue(null);
|
|
|
|
const mockUser = { id: 11 } as User;
|
|
userRepo.create.mockReturnValue(mockUser);
|
|
userRepo.save.mockResolvedValue(mockUser);
|
|
|
|
mappingRepo.findOne.mockResolvedValue(null);
|
|
mappingRepo.create.mockReturnValue({} as UserDingMapping);
|
|
mappingRepo.save.mockResolvedValue({} as UserDingMapping);
|
|
|
|
const users: ImportUserDto[] = [
|
|
{ dingUserId: 'user2', name: 'Li Si', mobile: '13800002222', roleId: 999 },
|
|
];
|
|
|
|
const result = await service.importDingTalkUsers(users);
|
|
|
|
expect(result.teacherCount).toBe(1);
|
|
expect(result.studentCount).toBe(0);
|
|
expect(result.skipped).toBe(0);
|
|
|
|
// User should be saved once (without roles assignment the second time)
|
|
expect(userRepo.save).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('imports student when roleId is null', async () => {
|
|
const mockUser = { id: 20 } as User;
|
|
userRepo.create.mockReturnValue(mockUser);
|
|
userRepo.save.mockResolvedValue(mockUser);
|
|
|
|
const mockStudent = { id: 30 } as Student;
|
|
studentRepo.create.mockReturnValue(mockStudent);
|
|
studentRepo.save.mockResolvedValue(mockStudent);
|
|
|
|
mappingRepo.findOne.mockResolvedValue(null);
|
|
mappingRepo.create.mockReturnValue({} as UserDingMapping);
|
|
mappingRepo.save.mockResolvedValue({} as UserDingMapping);
|
|
|
|
const users: ImportUserDto[] = [
|
|
{ dingUserId: 'user3', name: 'Wang Wu', mobile: '', roleId: null },
|
|
];
|
|
|
|
const result = await service.importDingTalkUsers(users);
|
|
|
|
expect(result.studentCount).toBe(1);
|
|
expect(result.teacherCount).toBe(0);
|
|
expect(result.skipped).toBe(0);
|
|
|
|
expect(studentRepo.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
name: 'Wang Wu',
|
|
userId: 20,
|
|
status: 'active',
|
|
}),
|
|
);
|
|
expect(studentRepo.save).toHaveBeenCalled();
|
|
});
|
|
|
|
it('skips user when mapping already exists', async () => {
|
|
mappingRepo.findOne.mockResolvedValue({ id: 1 } as UserDingMapping);
|
|
|
|
const users: ImportUserDto[] = [
|
|
{ dingUserId: 'existing', name: 'Zhao Liu', mobile: '13800003333', roleId: null },
|
|
];
|
|
|
|
const result = await service.importDingTalkUsers(users);
|
|
|
|
expect(result.skipped).toBe(1);
|
|
expect(result.teacherCount).toBe(0);
|
|
expect(result.studentCount).toBe(0);
|
|
expect(userRepo.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('handles per-user errors gracefully — one failure does not block others', async () => {
|
|
// First user fails, second succeeds
|
|
mappingRepo.findOne.mockResolvedValue(null);
|
|
mappingRepo.create.mockReturnValue({} as UserDingMapping);
|
|
mappingRepo.save.mockResolvedValue({} as UserDingMapping);
|
|
|
|
userRepo.create
|
|
.mockReturnValueOnce(new Error('DB error') as unknown as User)
|
|
.mockReturnValueOnce({ id: 40 } as User);
|
|
|
|
userRepo.save
|
|
.mockRejectedValueOnce(new Error('DB error'))
|
|
.mockResolvedValueOnce({ id: 40 } as User);
|
|
|
|
studentRepo.create.mockReturnValue({} as Student);
|
|
studentRepo.save.mockResolvedValue({} as Student);
|
|
|
|
const users: ImportUserDto[] = [
|
|
{ dingUserId: 'fail', name: 'Fail User', mobile: '', roleId: null },
|
|
{ dingUserId: 'ok', name: 'Ok User', mobile: '', roleId: null },
|
|
];
|
|
|
|
const result = await service.importDingTalkUsers(users);
|
|
|
|
expect(result.studentCount).toBe(1);
|
|
expect(result.skipped).toBe(0);
|
|
|
|
// The first user's mapping should not be saved, but the second's should
|
|
expect(mappingRepo.save).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('counts mixed teacher/student/skipped correctly', async () => {
|
|
const mockRole = { id: 1, name: 'Teacher Role' } as Role;
|
|
roleRepo.findOne.mockResolvedValue(mockRole);
|
|
|
|
const mockUser = { id: 50 } as User;
|
|
userRepo.create.mockReturnValue(mockUser);
|
|
userRepo.save.mockResolvedValue(mockUser);
|
|
|
|
studentRepo.create.mockReturnValue({} as Student);
|
|
studentRepo.save.mockResolvedValue({} as Student);
|
|
|
|
mappingRepo.create.mockReturnValue({} as UserDingMapping);
|
|
mappingRepo.save.mockResolvedValue({} as UserDingMapping);
|
|
|
|
// First: skip (existing), second: teacher, third: student
|
|
mappingRepo.findOne
|
|
.mockResolvedValueOnce({ id: 99 } as UserDingMapping) // skip
|
|
.mockResolvedValueOnce(null) // teacher
|
|
.mockResolvedValueOnce(null); // student
|
|
|
|
const users: ImportUserDto[] = [
|
|
{ dingUserId: 'skip', name: 'Skip', mobile: '138', roleId: null },
|
|
{ dingUserId: 'teacher', name: 'Teacher', mobile: '139', roleId: 1 },
|
|
{ dingUserId: 'student', name: 'Student', mobile: '', roleId: null },
|
|
];
|
|
|
|
const result = await service.importDingTalkUsers(users);
|
|
|
|
expect(result.teacherCount).toBe(1);
|
|
expect(result.studentCount).toBe(1);
|
|
expect(result.skipped).toBe(1);
|
|
});
|
|
});
|