fix: harden real backend admin workflows and ui

This commit is contained in:
hectorzhao
2026-07-03 19:29:56 +08:00
parent dd09d91c1e
commit 8cca361441
71 changed files with 5111 additions and 4439 deletions
+259 -2
View File
@@ -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 },
});
});
});