import { DingTalkService } from './dingtalk.service'; describe('DingTalkService — queryShifts', () => { const originalAppKey = process.env.DINGTALK_APP_KEY; const originalAppSecret = process.env.DINGTALK_APP_SECRET; let service: DingTalkService; beforeEach(() => { process.env.DINGTALK_APP_KEY = 'test-app-key'; process.env.DINGTALK_APP_SECRET = 'test-app-secret'; service = new DingTalkService({} as never, {} as never); Object.assign(service, { accessToken: 'test-token', accessTokenCredentialKey: 'test-app-key:test-app-secret', tokenExpiresAt: Date.now() + 3_600_000, }); jest.spyOn(service, 'rateLimit').mockResolvedValue(undefined); }); afterEach(() => { jest.restoreAllMocks(); global.fetch = undefined as unknown as typeof fetch; }); afterAll(() => { if (originalAppKey === undefined) delete process.env.DINGTALK_APP_KEY; else process.env.DINGTALK_APP_KEY = originalAppKey; if (originalAppSecret === undefined) delete process.env.DINGTALK_APP_SECRET; else process.env.DINGTALK_APP_SECRET = originalAppSecret; }); it('unwraps the paged result object returned by DingTalk', async () => { global.fetch = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ errcode: 0, errmsg: 'ok', result: { cursor: 678215070, has_more: false, result: [ { id: 677995086, name: 'A' }, { id: 678215070, name: 'B' }, ], }, }), }) as jest.MockedFunction; await expect(service.queryShifts('manager')).resolves.toEqual([ { id: 677995086, name: 'A' }, { id: 678215070, name: 'B' }, ]); }); it('requests subsequent pages using the cursor returned by DingTalk', async () => { global.fetch = jest .fn() .mockResolvedValueOnce({ json: jest.fn().mockResolvedValue({ errcode: 0, errmsg: 'ok', result: { cursor: 200, has_more: true, result: [{ id: 100, name: '早班' }], }, }), }) .mockResolvedValueOnce({ json: jest.fn().mockResolvedValue({ errcode: 0, errmsg: 'ok', result: { cursor: 300, has_more: false, result: [{ id: 200, name: '晚班' }], }, }), }) as jest.MockedFunction; await expect(service.queryShifts('manager')).resolves.toEqual([ { id: 100, name: '早班' }, { id: 200, name: '晚班' }, ]); expect(global.fetch).toHaveBeenCalledTimes(2); expect(JSON.parse((global.fetch as jest.Mock).mock.calls[0][1].body)).toEqual({ op_user_id: 'manager', cursor: 0, }); expect(JSON.parse((global.fetch as jest.Mock).mock.calls[1][1].body)).toEqual({ op_user_id: 'manager', cursor: 200, }); }); }); describe('DingTalkService — attendance machine only group', () => { const originalAppKey = process.env.DINGTALK_APP_KEY; const originalAppSecret = process.env.DINGTALK_APP_SECRET; let service: DingTalkService; beforeEach(() => { process.env.DINGTALK_APP_KEY = 'test-app-key'; process.env.DINGTALK_APP_SECRET = 'test-app-secret'; service = new DingTalkService({} as never, {} as never); Object.assign(service, { accessToken: 'test-token', accessTokenCredentialKey: 'test-app-key:test-app-secret', tokenExpiresAt: Date.now() + 3_600_000, }); jest.spyOn(service, 'rateLimit').mockResolvedValue(undefined); }); afterEach(() => { jest.restoreAllMocks(); global.fetch = undefined as unknown as typeof fetch; }); afterAll(() => { if (originalAppKey === undefined) delete process.env.DINGTALK_APP_KEY; else process.env.DINGTALK_APP_KEY = originalAppKey; if (originalAppSecret === undefined) delete process.env.DINGTALK_APP_SECRET; else process.env.DINGTALK_APP_SECRET = originalAppSecret; }); it('accepts the success envelope returned when updating an attendance group', async () => { global.fetch = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ success: true, result: { id: 123, name: '排课_冲刺班' }, request_id: 'request-1', }), }) as jest.MockedFunction; await expect( service.updateAttendanceGroup({ id: 123, name: '排课_冲刺班', type: 'TURN', owner: 'manager', members: [{ role: 'Attendance', type: 'StaffMember', user_id: 'student-1' }], shift_ids: [456], }), ).resolves.toBeUndefined(); }); it('disables mobile-oriented punching when creating a machine-only group', async () => { global.fetch = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ errcode: 0, errmsg: 'ok', result: { id: 123 }, }), }) as jest.MockedFunction; await service.createAttendanceGroup({ name: '排课_冲刺班', type: 'TURN', owner: 'manager', members: [{ role: 'Attendance', type: 'StaffMember', user_id: 'student-1' }], shift_ids: [456], attendance_machine_only: true, }); const body = JSON.parse((global.fetch as jest.Mock).mock.calls[0][1].body); expect(body.top_group).toEqual(expect.objectContaining({ enable_emp_select_class: false, disable_check_without_schedule: true, enable_outside_check: false, enable_position_ble: false, positions: [], wifis: [], })); }); }); describe('DingTalkService — department user pagination boundaries', () => { afterEach(() => { jest.restoreAllMocks(); global.fetch = undefined as unknown as typeof fetch; }); it('stops when DingTalk says there is another page but omits the next cursor', async () => { const service = new DingTalkService({} as never, {} as never); global.fetch = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ errcode: 0, errmsg: 'ok', result: { list: [{ userid: 'u1', name: 'Alice', mobile: '', dept_id_list: [1] }], has_more: true, }, }), }) as jest.MockedFunction; await expect((service as any).getDeptUsers('token', 1)).resolves.toHaveLength(1); expect(global.fetch).toHaveBeenCalledTimes(1); }); it('stops when the next cursor repeats the current cursor', async () => { const service = new DingTalkService({} as never, {} as never); global.fetch = jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({ errcode: 0, errmsg: 'ok', result: { list: [], has_more: true, next_cursor: 0 }, }), }) as jest.MockedFunction; await expect((service as any).getDeptUsers('token', 1)).resolves.toEqual([]); expect(global.fetch).toHaveBeenCalledTimes(1); }); });