fix permissions and teacher attendance workflows

This commit is contained in:
2026-07-10 20:40:52 +08:00
parent 247879f276
commit 8ed1682b90
95 changed files with 4745 additions and 1237 deletions

View File

@@ -0,0 +1,128 @@
import { BadRequestException } from '@nestjs/common';
import { AttendanceImportService } from './attendance-import.service';
import { DingTalkService } from '../integration/dingtalk.service';
import { AttendanceService } from './attendance.service';
describe('AttendanceImportService', () => {
const dingRawRepo = {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
};
const studentRepo = { findOne: jest.fn() };
const studentDingMappingRepo = { findOne: jest.fn() };
const dingTalkService = {
fetchAttendanceResults: jest.fn(),
};
const attendanceService = {
autoMatchDingRecords: jest.fn(),
};
let service: AttendanceImportService;
beforeEach(() => {
jest.clearAllMocks();
service = new AttendanceImportService(
dingRawRepo as never,
studentRepo as never,
studentDingMappingRepo as never,
dingTalkService as unknown as DingTalkService,
attendanceService as unknown as AttendanceService,
);
});
it('splits DingTalk requests by at most 50 users and 7 calendar days without offset pagination', async () => {
const userIds = Array.from({ length: 51 }, (_, index) => `user-${index + 1}`);
dingTalkService.fetchAttendanceResults.mockResolvedValue([]);
await (service as any).fetchAllPages({
startDate: '2026-07-01',
endDate: '2026-07-10',
userIds,
});
expect(dingTalkService.fetchAttendanceResults).toHaveBeenCalledTimes(4);
expect(dingTalkService.fetchAttendanceResults.mock.calls.map(([params]) => params)).toEqual([
{
startDate: '2026-07-01',
endDate: '2026-07-07',
userIds: userIds.slice(0, 50),
},
{
startDate: '2026-07-01',
endDate: '2026-07-07',
userIds: userIds.slice(50),
},
{
startDate: '2026-07-08',
endDate: '2026-07-10',
userIds: userIds.slice(0, 50),
},
{
startDate: '2026-07-08',
endDate: '2026-07-10',
userIds: userIds.slice(50),
},
]);
});
it('rejects an attendance import without DingTalk user IDs', async () => {
await expect(
(service as any).fetchAllPages({
startDate: '2026-07-01',
endDate: '2026-07-01',
userIds: [],
}),
).rejects.toBeInstanceOf(BadRequestException);
expect(dingTalkService.fetchAttendanceResults).not.toHaveBeenCalled();
});
it('stores the DingTalk user name returned with the attendance record', async () => {
const entity = await (service as any).mapToEntity({
userId: 'ding-1',
userName: '张三',
workDate: '2026-07-01',
timeResult: 'Normal',
locationResult: '',
planCheckTime: '',
actualCheckTime: '2026-07-01T08:00:00.000Z',
checkId: 'check-1',
checkType: 'OnDuty',
});
expect(entity.userName).toBe('张三');
});
it('fills the student name from the DingTalk mapping when saving an imported record', async () => {
dingTalkService.fetchAttendanceResults.mockResolvedValue([
{
userId: 'ding-1',
userName: '',
workDate: '2026-07-01',
timeResult: 'Normal',
locationResult: '',
planCheckTime: '',
actualCheckTime: '2026-07-01T08:00:00.000Z',
checkId: 'check-1',
checkType: 'OnDuty',
},
]);
dingRawRepo.find.mockResolvedValue([]);
studentDingMappingRepo.findOne.mockResolvedValue({ studentId: 3 });
studentRepo.findOne.mockResolvedValue({ id: 3, name: '张三' });
dingRawRepo.save.mockImplementation(async (entities) => entities);
await service.importFromDingTalk({
startDate: '2026-07-01',
endDate: '2026-07-01',
userIds: ['ding-1'],
autoMatch: false,
});
expect(dingRawRepo.save).toHaveBeenCalledWith(
[expect.objectContaining({ userName: '张三' })],
{ chunk: 50 },
);
});
});