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', () => { type PrivateDingTalkService = { getDeptUsers(token: string, deptId: number): Promise; }; afterEach(() => { jest.restoreAllMocks(); global.fetch = undefined as unknown as typeof fetch; }); it('fails 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({ ok: true, 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 unknown as PrivateDingTalkService).getDeptUsers('token', 1)).rejects.toThrow('分页游标未前进'); expect(global.fetch).toHaveBeenCalledTimes(1); }); it('fails when the next cursor repeats the current cursor', async () => { const service = new DingTalkService({} as never, {} as never); global.fetch = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ errcode: 0, errmsg: 'ok', result: { list: [], has_more: true, next_cursor: 0 }, }), }) as jest.MockedFunction; await expect((service as unknown as PrivateDingTalkService).getDeptUsers('token', 1)).rejects.toThrow('分页游标未前进'); expect(global.fetch).toHaveBeenCalledTimes(1); }); it('fails on a DingTalk API error instead of returning a partial user list', async () => { const service = new DingTalkService({} as never, {} as never); global.fetch = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ errcode: 40035, errmsg: 'invalid department' }), }) as jest.MockedFunction; await expect((service as unknown as PrivateDingTalkService).getDeptUsers('token', 9)).rejects.toThrow('invalid department'); }); it('keeps a multi-department user visible in the selected subtree', async () => { const service = new DingTalkService({} as never, {} as never); const privateService = service as unknown as { isConfigured(): Promise; getAccessToken(): Promise; getDeptInfo(token: string, deptId: number): Promise<{ name: string; parent_id: number }>; buildDeptNode(token: string, deptId: number, name: string, parentId: number): Promise<{ id: number; name: string; parentId: number; children: []; }>; getDeptUsers(token: string, deptId: number): Promise>; }; jest.spyOn(privateService, 'isConfigured').mockResolvedValue(true); jest.spyOn(privateService, 'getAccessToken').mockResolvedValue('token'); jest.spyOn(privateService, 'getDeptInfo').mockResolvedValue({ name: '子部门', parent_id: 1 }); jest.spyOn(privateService, 'buildDeptNode').mockResolvedValue({ id: 2, name: '子部门', parentId: 1, children: [], }); jest.spyOn(privateService, 'getDeptUsers').mockResolvedValue([ { userid: 'u1', name: 'Alice', mobile: '', dept_id_list: [2, 99] }, ]); const tree = await service.fetchOrgTreeWithUsers(2); expect(tree[0].users).toEqual([ expect.objectContaining({ userid: 'u1', deptIds: [2, 99] }), ]); }); });