forked from wangziqi/gongxue-base
269 lines
9.0 KiB
TypeScript
269 lines
9.0 KiB
TypeScript
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<typeof fetch>;
|
|
|
|
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<typeof fetch>;
|
|
|
|
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<typeof fetch>;
|
|
|
|
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<typeof fetch>;
|
|
|
|
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<unknown[]>;
|
|
};
|
|
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<typeof fetch>;
|
|
|
|
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<typeof fetch>;
|
|
|
|
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<typeof fetch>;
|
|
|
|
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<boolean>;
|
|
getAccessToken(): Promise<string>;
|
|
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<Array<{
|
|
userid: string;
|
|
name: string;
|
|
mobile: string;
|
|
dept_id_list: number[];
|
|
}>>;
|
|
};
|
|
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] }),
|
|
]);
|
|
});
|
|
});
|