test: add first-version coverage
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import { RiskReviewService } from './risk-review.service';
|
||||
|
||||
function createPrismaMock(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
riskRule: {
|
||||
findFirst: jest.fn().mockResolvedValue({ id: 'default-rule' }),
|
||||
create: jest.fn(),
|
||||
findMany: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
globalBlacklist: {
|
||||
findMany: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
enterpriseBlacklist: {
|
||||
findMany: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
smsApplication: {
|
||||
findUnique: jest.fn().mockResolvedValue(null),
|
||||
},
|
||||
smsTemplate: {
|
||||
findUnique: jest.fn().mockResolvedValue(null),
|
||||
},
|
||||
smsSendTask: {
|
||||
count: jest.fn().mockResolvedValue(0),
|
||||
create: jest.fn().mockImplementation(({ data }: { data: Record<string, unknown> }) =>
|
||||
Promise.resolve({ id: 'risk-task-1', ...data }),
|
||||
),
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'risk-task-1', riskHits: [] }),
|
||||
update: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
riskHitRecord: {
|
||||
createMany: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('RiskReviewService', () => {
|
||||
it('rejects tasks over the application max phone threshold', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
prisma.smsApplication.findUnique.mockResolvedValue({ id: 'app-1', maxPhonesPerTask: 2 });
|
||||
prisma.riskRule.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'rule-max',
|
||||
code: 'MAX_PHONES_PER_TASK',
|
||||
name: '单任务最大号码数',
|
||||
metric: 'phoneTotal',
|
||||
thresholdValue: 100000,
|
||||
action: 'block',
|
||||
priority: 10,
|
||||
},
|
||||
]);
|
||||
prisma.smsSendTask.findUnique.mockResolvedValue({
|
||||
id: 'risk-task-1',
|
||||
status: 'rejected',
|
||||
riskHits: [{ ruleCode: 'MAX_PHONES_PER_TASK' }],
|
||||
});
|
||||
|
||||
const service = new RiskReviewService(prisma as never);
|
||||
const result = await service.evaluateTask({
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
content: 'hello',
|
||||
phones: ['13800000001', '13800000002', '13800000003'],
|
||||
});
|
||||
|
||||
expect(result.canSubmit).toBe(false);
|
||||
expect(result.status).toBe('rejected');
|
||||
expect(prisma.riskHitRecord.createMany).toHaveBeenCalledWith({
|
||||
data: [expect.objectContaining({ ruleCode: 'MAX_PHONES_PER_TASK', actualValue: 3, action: 'block' })],
|
||||
});
|
||||
});
|
||||
|
||||
it('routes duplicate and blacklist ratio hits to manual review', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
prisma.globalBlacklist.findMany.mockResolvedValue([{ phoneNumber: '13800000001' }]);
|
||||
prisma.riskRule.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'rule-dup',
|
||||
code: 'DUPLICATE_PHONE_RATIO',
|
||||
name: '重复号码比例',
|
||||
metric: 'duplicateRatio',
|
||||
thresholdValue: 0.2,
|
||||
action: 'manual_review',
|
||||
priority: 20,
|
||||
},
|
||||
{
|
||||
id: 'rule-black',
|
||||
code: 'BLACKLIST_HIT_RATIO',
|
||||
name: '黑名单命中比例',
|
||||
metric: 'blacklistHitRatio',
|
||||
thresholdValue: 0.2,
|
||||
action: 'manual_review',
|
||||
priority: 40,
|
||||
},
|
||||
]);
|
||||
|
||||
const service = new RiskReviewService(prisma as never);
|
||||
const result = await service.evaluateTask({
|
||||
tenantId: 'tenant-1',
|
||||
content: 'hello',
|
||||
phones: ['13800000001', '13800000001', '13800000002'],
|
||||
});
|
||||
|
||||
expect(result.status).toBe('pending_review');
|
||||
expect(prisma.smsSendTask.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
duplicateRatio: 0.3333,
|
||||
blacklistHitRatio: 0.3333,
|
||||
status: 'pending_review',
|
||||
riskDecision: 'manual_review',
|
||||
}),
|
||||
});
|
||||
expect(prisma.riskHitRecord.createMany).toHaveBeenCalledWith({
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({ ruleCode: 'DUPLICATE_PHONE_RATIO' }),
|
||||
expect.objectContaining({ ruleCode: 'BLACKLIST_HIT_RATIO' }),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects illegal phone and template variable anomalies', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
prisma.smsTemplate.findUnique.mockResolvedValue({
|
||||
id: 'tpl-1',
|
||||
category: 'notice',
|
||||
variables: [{ name: 'code', required: true }],
|
||||
});
|
||||
prisma.riskRule.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'rule-illegal',
|
||||
code: 'ILLEGAL_PHONE_RATIO',
|
||||
name: '非法号码比例',
|
||||
metric: 'illegalRatio',
|
||||
thresholdValue: 0.1,
|
||||
action: 'block',
|
||||
priority: 30,
|
||||
},
|
||||
{
|
||||
id: 'rule-var',
|
||||
code: 'TEMPLATE_VARIABLE_ANOMALY',
|
||||
name: '模板变量异常',
|
||||
metric: 'variableIssueCount',
|
||||
thresholdValue: 0,
|
||||
action: 'block',
|
||||
priority: 70,
|
||||
},
|
||||
]);
|
||||
|
||||
const service = new RiskReviewService(prisma as never);
|
||||
const result = await service.evaluateTask({
|
||||
tenantId: 'tenant-1',
|
||||
templateId: 'tpl-1',
|
||||
content: '验证码 ${code}',
|
||||
phones: ['13800000001', 'not-a-phone'],
|
||||
variables: { extra: 'value' },
|
||||
});
|
||||
|
||||
expect(result.status).toBe('rejected');
|
||||
expect(prisma.smsSendTask.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
illegalRatio: 0.5,
|
||||
variableIssues: expect.arrayContaining([
|
||||
{ type: 'missing_required_variable', name: 'code' },
|
||||
{ type: 'unexpected_variable', name: 'extra' },
|
||||
]),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('marks non-working marketing bulk and frequent task creation for manual review', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
prisma.smsSendTask.count.mockResolvedValue(11);
|
||||
prisma.riskRule.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'rule-night',
|
||||
code: 'NON_WORKING_MARKETING_BULK',
|
||||
name: '非工作时间大批量营销发送',
|
||||
metric: 'nonWorkingMarketingPhones',
|
||||
thresholdValue: 2,
|
||||
action: 'manual_review',
|
||||
priority: 50,
|
||||
},
|
||||
{
|
||||
id: 'rule-frequency',
|
||||
code: 'TASK_CREATE_FREQUENCY',
|
||||
name: '短时间任务创建频控',
|
||||
metric: 'recentTaskCount',
|
||||
thresholdValue: 10,
|
||||
action: 'manual_review',
|
||||
priority: 60,
|
||||
},
|
||||
]);
|
||||
|
||||
const service = new RiskReviewService(prisma as never);
|
||||
const result = await service.evaluateTask({
|
||||
tenantId: 'tenant-1',
|
||||
category: 'marketing',
|
||||
content: 'promo',
|
||||
phones: ['13800000001', '13800000002', '13800000003'],
|
||||
requestedAt: '2026-07-01T22:00:00+08:00',
|
||||
});
|
||||
|
||||
expect(result.status).toBe('pending_review');
|
||||
expect(prisma.riskHitRecord.createMany).toHaveBeenCalledWith({
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({ ruleCode: 'NON_WORKING_MARKETING_BULK', actualValue: 3 }),
|
||||
expect.objectContaining({ ruleCode: 'TASK_CREATE_FREQUENCY', actualValue: 11 }),
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user