Files
gongxue-base/apps/server/src/integration/dingtalk.service.spec.ts

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] }),
]);
});
});