Files
gongxue-base/apps/server/src/attendance/attendance.controller.spec.ts

156 lines
5.5 KiB
TypeScript

import { BadRequestException, ForbiddenException } from '@nestjs/common';
import { AttendanceController } from './attendance.controller';
import { AttendanceService } from './attendance.service';
import { AttendanceImportService } from './attendance-import.service';
import { OperationLogsService } from '../operation-logs/operation-logs.service';
describe('AttendanceController — DingTalk import scope', () => {
const attendanceService = {
getTeacherClassDingUserIds: jest.fn(),
getImportableClasses: jest.fn(),
};
const importService = {
importFromDingTalk: jest.fn(),
};
const logService = {
log: jest.fn(),
};
const authzService = {
can: jest.fn().mockReturnValue(false),
};
let controller: AttendanceController;
beforeEach(() => {
jest.clearAllMocks();
controller = new AttendanceController(
attendanceService as unknown as AttendanceService,
importService as unknown as AttendanceImportService,
logService as unknown as OperationLogsService,
authzService as never,
);
importService.importFromDingTalk.mockResolvedValue({
success: true,
imported: 0,
skipped: 0,
matched: 0,
errors: [],
duration: 1,
});
});
it('defaults teacher DingTalk import to today when no date range is provided', async () => {
jest.useFakeTimers().setSystemTime(new Date('2026-07-10T08:00:00.000Z'));
attendanceService.getTeacherClassDingUserIds.mockResolvedValue(['ding-today']);
await controller.importFromDingTalk({ classId: 8 }, {
user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false },
} as never);
expect(importService.importFromDingTalk).toHaveBeenCalledWith({
startDate: '2026-07-10',
endDate: '2026-07-10',
userIds: ['ding-today'],
autoMatch: true,
});
jest.useRealTimers();
});
it('uses only the selected class students mapped to DingTalk for a teacher import', async () => {
attendanceService.getTeacherClassDingUserIds.mockResolvedValue(['ding-1', 'ding-2']);
await controller.importFromDingTalk(
{ start: '2026-07-01', end: '2026-07-02', classId: 8, autoMatch: true },
{ user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false } } as never,
);
expect(attendanceService.getTeacherClassDingUserIds).toHaveBeenCalledWith(21, 8, false);
expect(importService.importFromDingTalk).toHaveBeenCalledWith({
startDate: '2026-07-01',
endDate: '2026-07-02',
userIds: ['ding-1', 'ding-2'],
autoMatch: true,
});
});
it('does not allow a teacher to supply arbitrary DingTalk user IDs', async () => {
await expect(
controller.importFromDingTalk(
{ start: '2026-07-01', end: '2026-07-02', users: 'someone-else', autoMatch: true },
{ user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false } } as never,
),
).rejects.toBeInstanceOf(ForbiddenException);
expect(importService.importFromDingTalk).not.toHaveBeenCalled();
});
it('requires teachers to select one of their classes', async () => {
await expect(
controller.importFromDingTalk({ start: '2026-07-01', end: '2026-07-02', autoMatch: true }, {
user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false },
} as never),
).rejects.toBeInstanceOf(BadRequestException);
});
it('lists only classes available to the current user for DingTalk import', async () => {
attendanceService.getImportableClasses.mockResolvedValue([{ classId: 8, className: '八班' }]);
await expect(
controller.getDingTalkImportClasses({
user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false },
} as never),
).resolves.toEqual([{ classId: 8, className: '八班' }]);
expect(attendanceService.getImportableClasses).toHaveBeenCalledWith(21, false);
});
it('always auto-matches class-scoped imports even if an old client sends autoMatch=false', async () => {
attendanceService.getTeacherClassDingUserIds.mockResolvedValue(['ding-1']);
await controller.importFromDingTalk(
{ start: '2026-07-01', end: '2026-07-02', classId: 8, autoMatch: false },
{ user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false } } as never,
);
expect(importService.importFromDingTalk).toHaveBeenCalledWith(
expect.objectContaining({ autoMatch: true }),
);
});
it('allows class managers to choose any importable class and supply explicit DingTalk users', async () => {
attendanceService.getImportableClasses.mockResolvedValue([{ classId: 1, className: '一班' }]);
authzService.can.mockReturnValue(true);
await expect(
controller.getDingTalkImportClasses({
user: {
id: 7,
username: 'manager',
permissions: ['class:edit', 'attendance:create'],
isSuperAdmin: false,
},
} as never),
).resolves.toEqual([{ classId: 1, className: '一班' }]);
expect(attendanceService.getImportableClasses).toHaveBeenCalledWith(7, true);
await controller.importFromDingTalk(
{ start: '2026-07-01', end: '2026-07-02', users: 'ding-1,ding-2', autoMatch: true },
{
user: {
id: 7,
username: 'manager',
permissions: ['class:edit', 'attendance:create'],
isSuperAdmin: false,
},
} as never,
);
expect(importService.importFromDingTalk).toHaveBeenLastCalledWith({
startDate: '2026-07-01',
endDate: '2026-07-02',
userIds: ['ding-1', 'ding-2'],
autoMatch: true,
});
});
});