fix: harden real backend admin workflows and ui
This commit is contained in:
@@ -19,14 +19,59 @@ function createPrismaMock() {
|
||||
secretHash: 'secret-hash',
|
||||
tenant: { id: 'tenant-1', name: '租户A', code: 'TENANT-A' },
|
||||
}),
|
||||
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'app-1', tenantId: 'tenant-1', ...data })),
|
||||
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'app-new', ...data })),
|
||||
},
|
||||
smsApplicationIpAllowlist: {
|
||||
deleteMany: jest.fn().mockResolvedValue({ count: 1 }),
|
||||
},
|
||||
smsChannelGroup: {
|
||||
findMany: jest.fn().mockResolvedValue([
|
||||
{ id: 'group-mobile', carrier: 'mobile' },
|
||||
{ id: 'group-unicom', carrier: 'unicom' },
|
||||
]),
|
||||
},
|
||||
channelRouteRule: {
|
||||
deleteMany: jest.fn().mockResolvedValue({ count: 2 }),
|
||||
createMany: jest.fn().mockResolvedValue({ count: 2 }),
|
||||
findMany: jest.fn().mockResolvedValue([
|
||||
{ id: 'rule-1', applicationId: 'app-1', groupId: 'group-mobile', carrier: 'mobile', priority: 10, status: 'active' },
|
||||
{ id: 'rule-2', applicationId: 'app-1', groupId: 'group-unicom', carrier: 'unicom', priority: 20, status: 'active' },
|
||||
]),
|
||||
},
|
||||
smsSignature: {
|
||||
findMany: jest.fn().mockResolvedValue([{
|
||||
id: 'sig-1',
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
name: '签名A',
|
||||
purpose: '行业通知',
|
||||
auditStatus: 'pending',
|
||||
drainageInfo: { carrierStatus: { mobile: 'approved', unicom: 'pending', telecom: 'filing' }, links: [] },
|
||||
tenant: { id: 'tenant-1', name: '租户A', code: 'TENANT-A' },
|
||||
application: { id: 'app-1', name: '应用A' },
|
||||
materials: [],
|
||||
}]),
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', auditStatus: 'pending' }),
|
||||
update: jest.fn(),
|
||||
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'sig-1', tenantId: 'tenant-1', ...data })),
|
||||
},
|
||||
smsTemplate: {
|
||||
findMany: jest.fn().mockResolvedValue([{
|
||||
id: 'tpl-1',
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
signatureId: 'sig-1',
|
||||
name: '模板A',
|
||||
content: '您好${name}',
|
||||
auditStatus: 'pending',
|
||||
tenant: { id: 'tenant-1', name: '租户A', code: 'TENANT-A' },
|
||||
application: { id: 'app-1', name: '应用A' },
|
||||
signature: { id: 'sig-1', name: '签名A' },
|
||||
variables: [{ name: 'name', required: true }],
|
||||
}]),
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'tpl-1', tenantId: 'tenant-1', auditStatus: 'pending' }),
|
||||
update: jest.fn(),
|
||||
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'tpl-1', tenantId: 'tenant-1', ...data })),
|
||||
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'tpl-new', ...data })),
|
||||
},
|
||||
auditRecord: {
|
||||
create: jest.fn(),
|
||||
@@ -56,6 +101,28 @@ function createPrismaMock() {
|
||||
operationLog: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
$transaction: jest.fn((callback) => callback({
|
||||
smsApplication: {
|
||||
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'app-1', tenantId: 'tenant-1', ...data })),
|
||||
},
|
||||
smsApplicationIpAllowlist: {
|
||||
deleteMany: jest.fn().mockResolvedValue({ count: 1 }),
|
||||
},
|
||||
channelRouteRule: {
|
||||
deleteMany: jest.fn().mockResolvedValue({ count: 2 }),
|
||||
createMany: jest.fn().mockResolvedValue({ count: 2 }),
|
||||
findMany: jest.fn().mockResolvedValue([
|
||||
{ id: 'rule-1', applicationId: 'app-1', groupId: 'group-mobile', carrier: 'mobile', priority: 10, status: 'active' },
|
||||
{ id: 'rule-2', applicationId: 'app-1', groupId: 'group-unicom', carrier: 'unicom', priority: 20, status: 'active' },
|
||||
]),
|
||||
},
|
||||
templateVariable: {
|
||||
deleteMany: jest.fn().mockResolvedValue({ count: 1 }),
|
||||
},
|
||||
smsTemplate: {
|
||||
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'tpl-1', tenantId: 'tenant-1', ...data })),
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,6 +167,88 @@ describe('SmsConfigService', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('updates enterprise application profile and allowlist through a transaction', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const tx = {
|
||||
smsApplication: {
|
||||
update: jest.fn().mockResolvedValue({ id: 'app-1', name: '新应用', ipAllowlist: [{ ipCidr: '10.0.0.1/32' }] }),
|
||||
},
|
||||
smsApplicationIpAllowlist: {
|
||||
deleteMany: jest.fn().mockResolvedValue({ count: 1 }),
|
||||
},
|
||||
channelRouteRule: prisma.channelRouteRule,
|
||||
};
|
||||
prisma.$transaction.mockImplementationOnce((callback: (client: typeof tx) => unknown) => callback(tx));
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.updateApplication('app-1', { name: '新应用', customerUnitPrice: 300, ipAllowlist: ['10.0.0.1/32'] }))
|
||||
.resolves.toEqual(expect.objectContaining({ id: 'app-1', name: '新应用' }));
|
||||
|
||||
expect(tx.smsApplicationIpAllowlist.deleteMany).toHaveBeenCalledWith({ where: { applicationId: 'app-1' } });
|
||||
expect(tx.smsApplication.update).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: { id: 'app-1' },
|
||||
data: expect.objectContaining({
|
||||
name: '新应用',
|
||||
customerUnitPrice: 300,
|
||||
ipAllowlist: { create: [{ ipCidr: '10.0.0.1/32' }] },
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it('replaces application carrier channel-group routes with carrier validation', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const tx = {
|
||||
channelRouteRule: {
|
||||
deleteMany: jest.fn().mockResolvedValue({ count: 1 }),
|
||||
createMany: jest.fn().mockResolvedValue({ count: 2 }),
|
||||
findMany: jest.fn().mockResolvedValue([
|
||||
{ id: 'rule-1', carrier: 'mobile', groupId: 'group-mobile' },
|
||||
{ id: 'rule-2', carrier: 'unicom', groupId: 'group-unicom' },
|
||||
]),
|
||||
},
|
||||
};
|
||||
prisma.$transaction.mockImplementationOnce((callback: (client: typeof tx) => unknown) => callback(tx));
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.replaceApplicationRouteRules('app-1', {
|
||||
routes: [
|
||||
{ carrier: 'mobile', groupId: 'group-mobile' },
|
||||
{ carrier: 'unicom', groupId: 'group-unicom' },
|
||||
],
|
||||
})).resolves.toEqual([
|
||||
expect.objectContaining({ carrier: 'mobile' }),
|
||||
expect.objectContaining({ carrier: 'unicom' }),
|
||||
]);
|
||||
|
||||
expect(tx.channelRouteRule.deleteMany).toHaveBeenCalledWith({
|
||||
where: { applicationId: 'app-1', channelId: null, province: null },
|
||||
});
|
||||
expect(tx.channelRouteRule.createMany).toHaveBeenCalledWith({
|
||||
data: [
|
||||
expect.objectContaining({ tenantId: 'tenant-1', applicationId: 'app-1', carrier: 'mobile', groupId: 'group-mobile', priority: 10 }),
|
||||
expect.objectContaining({ tenantId: 'tenant-1', applicationId: 'app-1', carrier: 'unicom', groupId: 'group-unicom', priority: 20 }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects application routes when channel-group carrier does not match', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.replaceApplicationRouteRules('app-1', {
|
||||
routes: [{ carrier: 'telecom', groupId: 'group-mobile' }],
|
||||
})).rejects.toThrow('channel group carrier must match route carrier');
|
||||
|
||||
expect(prisma.$transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hides CMPP params when the application belongs to another tenant', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.getApplicationCmppParams('app-1', 'tenant-2')).rejects.toThrow('Application not found');
|
||||
});
|
||||
|
||||
it('disconnects application CMPP connections and writes operation logs', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
@@ -118,4 +267,112 @@ describe('SmsConfigService', () => {
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('lists enterprise signatures with keyword filters and real relations', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.listSignatures({ keyword: '签名A' })).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'sig-1',
|
||||
tenant: expect.objectContaining({ name: '租户A' }),
|
||||
application: expect.objectContaining({ name: '应用A' }),
|
||||
}),
|
||||
]);
|
||||
expect(prisma.smsSignature.findMany).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
auditStatus: { not: 'deleted' },
|
||||
OR: expect.any(Array),
|
||||
}),
|
||||
include: { materials: true, tenant: true, application: true },
|
||||
}));
|
||||
});
|
||||
|
||||
it('updates enterprise signature drainage info through the admin API path', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.updateSignature('sig-1', {
|
||||
name: '签名B',
|
||||
auditStatus: 'approved',
|
||||
drainageInfo: {
|
||||
carrierStatus: { mobile: 'approved', unicom: 'approved', telecom: 'approved' },
|
||||
links: [{ id: 'drain-1', siteName: '官网', url: 'https://example.com' }],
|
||||
},
|
||||
})).resolves.toEqual(expect.objectContaining({
|
||||
id: 'sig-1',
|
||||
name: '签名B',
|
||||
auditStatus: 'approved',
|
||||
}));
|
||||
|
||||
expect(prisma.smsSignature.update).toHaveBeenCalledWith({
|
||||
where: { id: 'sig-1' },
|
||||
data: expect.objectContaining({
|
||||
name: '签名B',
|
||||
auditStatus: 'approved',
|
||||
drainageInfo: expect.objectContaining({
|
||||
carrierStatus: expect.objectContaining({ mobile: 'approved' }),
|
||||
}),
|
||||
}),
|
||||
include: { materials: true, tenant: true, application: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('lists enterprise templates with real relations and excludes deleted by default', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.listTemplates({ keyword: '模板A' })).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'tpl-1',
|
||||
tenant: expect.objectContaining({ name: '租户A' }),
|
||||
application: expect.objectContaining({ name: '应用A' }),
|
||||
signature: expect.objectContaining({ name: '签名A' }),
|
||||
}),
|
||||
]);
|
||||
expect(prisma.smsTemplate.findMany).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
auditStatus: { not: 'deleted' },
|
||||
OR: expect.any(Array),
|
||||
}),
|
||||
include: { variables: true, application: true, tenant: true, signature: true },
|
||||
}));
|
||||
});
|
||||
|
||||
it('updates enterprise templates and rebuilds template variables', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const tx = {
|
||||
templateVariable: {
|
||||
deleteMany: jest.fn().mockResolvedValue({ count: 1 }),
|
||||
},
|
||||
smsTemplate: {
|
||||
update: jest.fn().mockResolvedValue({ id: 'tpl-1', name: '模板B', variables: [{ name: 'code' }] }),
|
||||
},
|
||||
};
|
||||
prisma.$transaction.mockImplementationOnce((callback: (client: typeof tx) => unknown) => callback(tx));
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.updateTemplate('tpl-1', {
|
||||
applicationId: 'app-1',
|
||||
signatureId: 'sig-1',
|
||||
name: '模板B',
|
||||
content: '验证码${code}',
|
||||
variables: [{ name: 'code', example: '123456', required: true }],
|
||||
})).resolves.toEqual(expect.objectContaining({ id: 'tpl-1', name: '模板B' }));
|
||||
|
||||
expect(tx.templateVariable.deleteMany).toHaveBeenCalledWith({ where: { templateId: 'tpl-1' } });
|
||||
expect(tx.smsTemplate.update).toHaveBeenCalledWith({
|
||||
where: { id: 'tpl-1' },
|
||||
data: expect.objectContaining({
|
||||
applicationId: 'app-1',
|
||||
signatureId: 'sig-1',
|
||||
name: '模板B',
|
||||
content: '验证码${code}',
|
||||
variables: {
|
||||
create: [{ name: 'code', example: '123456', required: true }],
|
||||
},
|
||||
}),
|
||||
include: { variables: true, application: true, tenant: true, signature: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user