由 OCR(open-codereview.ai,deepseek-v4-flash)审查驱动修复: - JWT 生产必填、密码 8-72 字节、防枚举;全局 ValidationPipe - classes/dashboard/schedules/students/attendance/archive/exams 越权与 IDOR 修复 - LIKE 通配符转义(14 处);上传 10MB 上限 + MIME 白名单 + 附件 XSS - 集成配置 appSecret AES 加密 + 回填脚本;审计 best-effort;IP 来源防伪造 Reviewed-by: OCR (open-codereview.ai)
194 lines
6.0 KiB
TypeScript
194 lines
6.0 KiB
TypeScript
import { IntegrationConfigService } from './integration-config.service';
|
|
import { decryptSecret, encryptSecret, isEncryptedSecret } from './secret-crypto';
|
|
|
|
describe('IntegrationConfigService.testConnection', () => {
|
|
const originalFetch = global.fetch;
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
it('uses the saved AppSecret when testing an existing configuration with a blank secret', async () => {
|
|
const configRepo = {
|
|
findOne: jest.fn().mockResolvedValue({ id: 1, type: 'THIRD' }),
|
|
};
|
|
const detailRepo = {
|
|
findOne: jest.fn().mockResolvedValue({
|
|
configId: 1,
|
|
type: 'DINGTALK_SYNC',
|
|
content: JSON.stringify({
|
|
config: {
|
|
corpId: 'ding-corp',
|
|
agentId: 'saved-key',
|
|
appSecret: 'saved-secret',
|
|
},
|
|
}),
|
|
}),
|
|
};
|
|
global.fetch = jest.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: jest.fn().mockResolvedValue({ accessToken: 'token' }),
|
|
}) as never;
|
|
|
|
const service = new IntegrationConfigService(configRepo as never, detailRepo as never);
|
|
|
|
await expect(
|
|
service.testConnection('DINGTALK', {
|
|
corpId: 'ding-corp',
|
|
agentId: 'saved-key',
|
|
appSecret: '',
|
|
}),
|
|
).resolves.toBe(true);
|
|
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
'https://api.dingtalk.com/v1.0/oauth2/accessToken',
|
|
expect.objectContaining({
|
|
body: JSON.stringify({ appKey: 'saved-key', appSecret: 'saved-secret' }),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('IntegrationConfigService security boundaries', () => {
|
|
const originalFetch = global.fetch;
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
it('masks AppSecret without mutating the parsed source object', async () => {
|
|
const content = JSON.stringify({
|
|
config: { corpId: 'corp', agentId: 'agent', appSecret: 'top-secret' },
|
|
});
|
|
const configRepo = {
|
|
findOne: jest.fn().mockResolvedValue({ id: 1, type: 'THIRD' }),
|
|
};
|
|
const detailRepo = {
|
|
find: jest.fn().mockResolvedValue([{ type: 'DINGTALK_SYNC', enable: true, content }]),
|
|
};
|
|
const service = new IntegrationConfigService(configRepo as never, detailRepo as never);
|
|
|
|
await expect(service.getThirdConfig()).resolves.toEqual([
|
|
{
|
|
type: 'DINGTALK',
|
|
verify: true,
|
|
config: { corpId: 'corp', agentId: 'agent' },
|
|
},
|
|
]);
|
|
expect(JSON.parse(content).config.appSecret).toBe('top-secret');
|
|
});
|
|
|
|
it('treats a non-2xx DingTalk token response as a failed connection even if it contains a token field', async () => {
|
|
const configRepo = { findOne: jest.fn() };
|
|
const detailRepo = { findOne: jest.fn() };
|
|
global.fetch = jest.fn().mockResolvedValue({
|
|
ok: false,
|
|
json: jest.fn().mockResolvedValue({ accessToken: 'must-not-be-used' }),
|
|
}) as never;
|
|
const service = new IntegrationConfigService(configRepo as never, detailRepo as never);
|
|
|
|
await expect(
|
|
service.testConnection('DINGTALK' as never, {
|
|
corpId: 'corp',
|
|
agentId: 'agent',
|
|
appSecret: 'secret',
|
|
}),
|
|
).resolves.toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('IntegrationConfigService appSecret encryption', () => {
|
|
const originalFetch = global.fetch;
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
it('encrypts AppSecret before persisting content', async () => {
|
|
const configRepo = {
|
|
findOne: jest.fn().mockResolvedValue({ id: 1, type: 'THIRD' }),
|
|
create: jest.fn(),
|
|
save: jest.fn(),
|
|
};
|
|
const detailRepo = {
|
|
findOne: jest.fn().mockResolvedValue(null),
|
|
create: jest.fn((data) => data),
|
|
save: jest.fn().mockResolvedValue(undefined),
|
|
};
|
|
global.fetch = jest.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: jest.fn().mockResolvedValue({ accessToken: 'token' }),
|
|
}) as never;
|
|
|
|
const service = new IntegrationConfigService(configRepo as never, detailRepo as never);
|
|
|
|
await service.saveConfig({
|
|
type: 'DINGTALK',
|
|
config: { corpId: 'corp', agentId: 'agent', appSecret: 'plain-secret' },
|
|
} as never);
|
|
|
|
expect(detailRepo.create).toHaveBeenCalled();
|
|
const saved = detailRepo.create.mock.calls[0][0];
|
|
const parsed = JSON.parse(saved.content) as {
|
|
config: { appSecret: string };
|
|
};
|
|
expect(parsed.config.appSecret).not.toBe('plain-secret');
|
|
expect(isEncryptedSecret(parsed.config.appSecret)).toBe(true);
|
|
expect(decryptSecret(parsed.config.appSecret)).toBe('plain-secret');
|
|
});
|
|
|
|
it('decrypts an encrypted stored AppSecret when reading raw config', async () => {
|
|
const configRepo = {
|
|
findOne: jest.fn().mockResolvedValue({ id: 1, type: 'THIRD' }),
|
|
};
|
|
const detailRepo = {
|
|
findOne: jest.fn().mockResolvedValue({
|
|
configId: 1,
|
|
type: 'DINGTALK_SYNC',
|
|
content: JSON.stringify({
|
|
config: {
|
|
corpId: 'corp',
|
|
agentId: 'agent',
|
|
appSecret: encryptSecret('saved-secret'),
|
|
},
|
|
}),
|
|
}),
|
|
};
|
|
const service = new IntegrationConfigService(configRepo as never, detailRepo as never);
|
|
|
|
await expect(service.getRawConfig('DINGTALK')).resolves.toEqual({
|
|
corpId: 'corp',
|
|
agentId: 'agent',
|
|
appSecret: 'saved-secret',
|
|
});
|
|
});
|
|
|
|
it('keeps masking AppSecret even when the stored value is encrypted', async () => {
|
|
const content = JSON.stringify({
|
|
config: {
|
|
corpId: 'corp',
|
|
agentId: 'agent',
|
|
appSecret: encryptSecret('top-secret'),
|
|
},
|
|
});
|
|
const configRepo = {
|
|
findOne: jest.fn().mockResolvedValue({ id: 1, type: 'THIRD' }),
|
|
};
|
|
const detailRepo = {
|
|
find: jest.fn().mockResolvedValue([{ type: 'DINGTALK_SYNC', enable: true, content }]),
|
|
};
|
|
const service = new IntegrationConfigService(configRepo as never, detailRepo as never);
|
|
|
|
await expect(service.getThirdConfig()).resolves.toEqual([
|
|
{
|
|
type: 'DINGTALK',
|
|
verify: true,
|
|
config: { corpId: 'corp', agentId: 'agent' },
|
|
},
|
|
]);
|
|
});
|
|
});
|