feat: 考勤模块重构与钉钉考勤同步
This commit is contained in:
@@ -2,7 +2,6 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { getDataSourceToken } from '@nestjs/typeorm';
|
||||
import { BadRequestException, ValidationPipe } from '@nestjs/common';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AttendanceService } from './attendance.service';
|
||||
import { AttendanceRecord } from '../entities/attendance-record.entity';
|
||||
import { AttendanceSession } from '../entities/attendance-session.entity';
|
||||
@@ -19,7 +18,6 @@ import { BatchCreateAttendanceDto } from './dto/attendance.dto';
|
||||
|
||||
describe('AttendanceService — batchCreate', () => {
|
||||
let service: AttendanceService;
|
||||
let attendanceRepo: jest.Mocked<Pick<Repository<AttendanceRecord>, 'create' | 'save'>>;
|
||||
|
||||
const savedRecords: AttendanceRecord[] = [];
|
||||
|
||||
@@ -28,14 +26,14 @@ describe('AttendanceService — batchCreate', () => {
|
||||
const mockRepo = {
|
||||
create: jest
|
||||
.fn()
|
||||
.mockImplementation((data: Partial<AttendanceRecord>) => ({ id: 1, ...data } as AttendanceRecord)),
|
||||
save: jest
|
||||
.fn()
|
||||
.mockImplementation((entities: AttendanceRecord[]) => {
|
||||
const result = entities.map((e, i) => ({ ...e, id: i + 1 }));
|
||||
savedRecords.push(...result);
|
||||
return Promise.resolve(result);
|
||||
}),
|
||||
.mockImplementation(
|
||||
(data: Partial<AttendanceRecord>) => ({ id: 1, ...data }) as AttendanceRecord,
|
||||
),
|
||||
save: jest.fn().mockImplementation((entities: AttendanceRecord[]) => {
|
||||
const result = entities.map((e, i) => ({ ...e, id: i + 1 }));
|
||||
savedRecords.push(...result);
|
||||
return Promise.resolve(result);
|
||||
}),
|
||||
};
|
||||
|
||||
const mockDingRepo = {};
|
||||
@@ -66,15 +64,32 @@ describe('AttendanceService — batchCreate', () => {
|
||||
}).compile();
|
||||
|
||||
service = module.get<AttendanceService>(AttendanceService);
|
||||
attendanceRepo = module.get(getRepositoryToken(AttendanceRecord));
|
||||
});
|
||||
|
||||
it('valid batch with morning_reading, evening_study, and night_check sessions → succeeds', async () => {
|
||||
const dto: BatchCreateAttendanceDto = {
|
||||
records: [
|
||||
{ studentId: 1, classId: 10, attendanceDate: '2026-07-05', session: 'morning_reading', status: 'present' },
|
||||
{ studentId: 2, classId: 10, attendanceDate: '2026-07-05', session: 'evening_study', status: 'present' },
|
||||
{ studentId: 3, classId: 10, attendanceDate: '2026-07-05', session: 'night_check', status: 'present' },
|
||||
{
|
||||
studentId: 1,
|
||||
classId: 10,
|
||||
attendanceDate: '2026-07-05',
|
||||
session: 'morning_reading',
|
||||
status: 'present',
|
||||
},
|
||||
{
|
||||
studentId: 2,
|
||||
classId: 10,
|
||||
attendanceDate: '2026-07-05',
|
||||
session: 'evening_study',
|
||||
status: 'present',
|
||||
},
|
||||
{
|
||||
studentId: 3,
|
||||
classId: 10,
|
||||
attendanceDate: '2026-07-05',
|
||||
session: 'night_check',
|
||||
status: 'present',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -92,7 +107,12 @@ describe('AttendanceService — batchCreate', () => {
|
||||
|
||||
const invalidPayload = {
|
||||
records: [
|
||||
{ studentId: 1, attendanceDate: '2026-07-05', session: 'invalid_session', status: 'present' },
|
||||
{
|
||||
studentId: 1,
|
||||
attendanceDate: '2026-07-05',
|
||||
session: 'invalid_session',
|
||||
status: 'present',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -151,11 +171,7 @@ describe('AttendanceService — teacher DingTalk class scope', () => {
|
||||
|
||||
it('returns only mapped active students for a class assigned to the teacher', async () => {
|
||||
classTeacherRepo.findOne.mockResolvedValue({ classId: 8, userId: 21 });
|
||||
classStudentRepo.find.mockResolvedValue([
|
||||
{ studentId: 2 },
|
||||
{ studentId: 1 },
|
||||
{ studentId: 2 },
|
||||
]);
|
||||
classStudentRepo.find.mockResolvedValue([{ studentId: 2 }, { studentId: 1 }, { studentId: 2 }]);
|
||||
mappingRepo.find.mockResolvedValue([
|
||||
{ studentId: 1, dingUserId: 'ding-1' },
|
||||
{ studentId: 2, dingUserId: 'ding-2' },
|
||||
@@ -206,7 +222,9 @@ describe('AttendanceService — DingTalk raw query', () => {
|
||||
};
|
||||
const dingRepo = { createQueryBuilder: jest.fn().mockReturnValue(qb) };
|
||||
const classStudentRepo = { find: jest.fn().mockResolvedValue([{ studentId: 3 }]) };
|
||||
const mappingRepo = { find: jest.fn().mockResolvedValue([{ studentId: 3, dingUserId: 'ding-3' }]) };
|
||||
const mappingRepo = {
|
||||
find: jest.fn().mockResolvedValue([{ studentId: 3, dingUserId: 'ding-3' }]),
|
||||
};
|
||||
const service = new AttendanceService(
|
||||
{} as never,
|
||||
dingRepo as never,
|
||||
@@ -237,7 +255,6 @@ describe('AttendanceService — DingTalk raw query', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('AttendanceService — attendance device display mappings', () => {
|
||||
function createHistoryQueryBuilder(records: AttendanceRecord[]) {
|
||||
return {
|
||||
@@ -337,8 +354,6 @@ describe('AttendanceService — attendance device display mappings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ── Session serialization tests ──
|
||||
function deferred<T>(): {
|
||||
promise: Promise<T>;
|
||||
@@ -400,7 +415,8 @@ describe('AttendanceService — session serialization', () => {
|
||||
}
|
||||
|
||||
function makeTxManager(sessionStatus: string) {
|
||||
const session = sessionStatus === 'completed' ? { ...sessionCompleted } : { ...sessionInProgress };
|
||||
const session =
|
||||
sessionStatus === 'completed' ? { ...sessionCompleted } : { ...sessionInProgress };
|
||||
|
||||
const sessionRepo = {
|
||||
findOne: jest.fn().mockResolvedValue(session),
|
||||
@@ -528,9 +544,7 @@ describe('AttendanceService — session serialization', () => {
|
||||
it('complete is idempotent: returns current state when session already completed', async () => {
|
||||
const manager = makeTxManager('completed');
|
||||
|
||||
const txMock = jest
|
||||
.fn()
|
||||
.mockImplementation((cb: (m: unknown) => unknown) => cb(manager));
|
||||
const txMock = jest.fn().mockImplementation((cb: (m: unknown) => unknown) => cb(manager));
|
||||
|
||||
const svc = makeService({ transaction: txMock });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user