Files
lislgosms/tools/smoke/real-env-smoke.mjs
T

365 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createHash } from 'node:crypto';
import { PrismaPg } from '../../api/node_modules/@prisma/adapter-pg/dist/index.js';
import { PrismaClient } from '../../api/node_modules/@prisma/client/index.js';
const prisma = new PrismaClient({
adapter: new PrismaPg(
process.env.DATABASE_URL ?? 'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public',
),
});
const apiBaseUrl = process.env.API_BASE_URL ?? 'http://127.0.0.1:3101/api';
const tenantCode = 'smoke-tenant';
const username = 'smoke_client';
const email = 'smoke-client@example.com';
const phone = '13800138099';
const password = 'SmokePass123!';
function hashPassword(value) {
return createHash('sha256').update(value).digest('hex');
}
async function ensureSmokeData() {
const tenant = await prisma.tenant.upsert({
where: { code: tenantCode },
update: { name: 'Smoke Test Enterprise', status: 'active' },
create: { code: tenantCode, name: 'Smoke Test Enterprise', status: 'active' },
});
const user = await prisma.user.upsert({
where: { username },
update: {
tenantId: tenant.id,
email,
phone,
displayName: 'Smoke Client Admin',
passwordHash: hashPassword(password),
status: 'active',
failedLoginCount: 0,
lockedUntil: null,
deletedAt: null,
},
create: {
tenantId: tenant.id,
username,
email,
phone,
displayName: 'Smoke Client Admin',
passwordHash: hashPassword(password),
status: 'active',
},
});
const enterpriseAdminRole = await prisma.role.upsert({
where: { code: 'enterprise_admin' },
update: { name: '企业管理员', scope: 'tenant' },
create: { code: 'enterprise_admin', name: '企业管理员', scope: 'tenant' },
});
await prisma.userRole.upsert({
where: { userId_roleId: { userId: user.id, roleId: enterpriseAdminRole.id } },
update: {},
create: { userId: user.id, roleId: enterpriseAdminRole.id },
});
await prisma.tenantAccount.upsert({
where: { tenantId: tenant.id },
update: { balanceCents: { increment: 0 }, smsUnits: { increment: 0 }, creditCents: 5000, status: 'active' },
create: { tenantId: tenant.id, balanceCents: 100000, smsUnits: 10000, creditCents: 5000, status: 'active' },
});
const channel = await prisma.smsChannel.upsert({
where: { code: 'SMOKE_CMPP' },
update: {
name: 'Smoke CMPP Channel',
carrier: 'all',
sendRegion: '全国',
gatewayHost: '127.0.0.1',
gatewayPort: 17890,
account: 'smoke-account',
passwordCipher: 'smoke-password',
srcId: '10690000',
rateLimitPerSecond: 500,
unitPrice: 5,
status: 'active',
},
create: {
code: 'SMOKE_CMPP',
name: 'Smoke CMPP Channel',
carrier: 'all',
protocol: 'CMPP',
gatewayHost: '127.0.0.1',
gatewayPort: 17890,
account: 'smoke-account',
passwordCipher: 'smoke-password',
srcId: '10690000',
rateLimitPerSecond: 500,
unitPrice: 5,
status: 'active',
},
});
const group = await prisma.smsChannelGroup.upsert({
where: { code: 'SMOKE_GROUP' },
update: { name: 'Smoke Channel Group', carrier: 'mobile', status: 'active' },
create: { code: 'SMOKE_GROUP', name: 'Smoke Channel Group', carrier: 'mobile', status: 'active' },
});
const groupItem = await prisma.smsChannelGroupItem.findFirst({
where: { groupId: group.id, channelId: channel.id },
});
if (!groupItem) {
await prisma.smsChannelGroupItem.create({
data: { groupId: group.id, channelId: channel.id, carrier: 'mobile', priority: 1, weight: 1, rateLimitPerSecond: 500 },
});
} else {
await prisma.smsChannelGroupItem.update({
where: { id: groupItem.id },
data: { carrier: 'mobile', province: null, priority: 1, weight: 1, rateLimitPerSecond: 500 },
});
}
let application =
(await prisma.smsApplication.findFirst({ where: { tenantId: tenant.id, name: 'Smoke SMS App' } })) ??
(await prisma.smsApplication.create({
data: {
tenantId: tenant.id,
name: 'Smoke SMS App',
scene: 'smoke',
secretHash: hashPassword('smoke-secret'),
dailyLimit: 100000,
customerUnitPrice: 5,
maxPhonesPerTask: 100000,
templateMismatchMode: 'reject',
status: 'active',
},
}));
application = await prisma.smsApplication.update({
where: { id: application.id },
data: { customerUnitPrice: 5, status: 'active' },
});
const routeRule = await prisma.channelRouteRule.findFirst({
where: { tenantId: tenant.id, applicationId: application.id, groupId: group.id, carrier: 'mobile', status: 'active' },
});
if (!routeRule) {
await prisma.channelRouteRule.create({
data: { tenantId: tenant.id, applicationId: application.id, groupId: group.id, carrier: 'mobile', priority: 1, status: 'active' },
});
}
await prisma.cmppConnectionState.upsert({
where: { applicationId_channelId_connectionId: { applicationId: application.id, channelId: channel.id, connectionId: 'smoke-conn-1' } },
update: {
tenantId: tenant.id,
applicationId: application.id,
status: 'connected',
desiredConnections: 1,
currentConnections: 1,
lastHeartbeatAt: new Date(),
lastError: null,
},
create: {
tenantId: tenant.id,
applicationId: application.id,
channelId: channel.id,
connectionId: 'smoke-conn-1',
status: 'connected',
desiredConnections: 1,
currentConnections: 1,
lastConnectedAt: new Date(),
lastHeartbeatAt: new Date(),
},
});
const signature =
(await prisma.smsSignature.findFirst({ where: { tenantId: tenant.id, applicationId: application.id, name: '烟测签名' } })) ??
(await prisma.smsSignature.create({
data: {
tenantId: tenant.id,
applicationId: application.id,
name: '烟测签名',
purpose: 'smoke',
auditStatus: 'approved',
reportStatus: 'approved',
},
}));
const template =
(await prisma.smsTemplate.findFirst({ where: { tenantId: tenant.id, applicationId: application.id, name: 'Smoke Verify Code' } })) ??
(await prisma.smsTemplate.create({
data: {
tenantId: tenant.id,
applicationId: application.id,
signatureId: signature.id,
name: 'Smoke Verify Code',
content: '您的验证码为${code}5分钟内有效。',
category: 'verification',
auditStatus: 'approved',
billingUnits: 1,
variables: { create: [{ name: 'code', example: '123456', required: true }] },
},
}));
const reportTask = await prisma.channelSignatureReportTask.findFirst({
where: { tenantId: tenant.id, signatureId: signature.id, channelId: channel.id },
});
if (!reportTask) {
await prisma.channelSignatureReportTask.create({
data: { tenantId: tenant.id, signatureId: signature.id, channelId: channel.id, status: 'approved' },
});
} else if (reportTask.status !== 'approved') {
await prisma.channelSignatureReportTask.update({ where: { id: reportTask.id }, data: { status: 'approved', reason: null } });
}
return { tenant, user, channel, application, signature, template };
}
async function request(path, options = {}) {
const response = await fetch(`${apiBaseUrl}${path}`, {
...options,
headers: {
'content-type': 'application/json',
...(options.headers ?? {}),
},
});
const text = await response.text();
let body;
try {
body = text ? JSON.parse(text) : null;
} catch {
body = text;
}
if (!response.ok) {
throw new Error(`${options.method ?? 'GET'} ${path} failed: ${response.status} ${text}`);
}
return body;
}
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
async function run() {
const data = await ensureSmokeData();
const tenantHeader = { 'x-tenant-id': data.tenant.id };
const health = await request('/health');
assert(health.status === 'ok', 'health should return ok');
const captcha = await request('/client/auth/captcha');
const captchaText = String(captcha.challenge.split('=')[0].split('+').map((part) => Number(part.trim())).reduce((sum, value) => sum + value, 0));
const login = await request('/client/auth/login', {
method: 'POST',
body: JSON.stringify({ login: email, password, captchaId: captcha.captchaId, captchaText }),
});
assert(login.accessToken && login.user?.tenantId === data.tenant.id, 'login should return smoke tenant user');
const tenants = await request('/admin/tenants');
assert(tenants.some((tenant) => tenant.code === tenantCode), 'tenant list should contain smoke tenant');
const recharge = await request('/admin/billing/manual-recharges', {
method: 'POST',
body: JSON.stringify({
tenantId: data.tenant.id,
amountCents: 1234,
smsUnits: 10,
operatorId: data.user.id,
remark: 'real env smoke manual recharge',
}),
});
assert(recharge.id, 'manual recharge should create an order');
const transactions = await request('/client/billing/transactions', { headers: tenantHeader });
assert(transactions.some((item) => item.relatedId === recharge.id), 'transactions should include recharge delta');
const check = await request('/admin/billing/check', {
method: 'POST',
body: JSON.stringify({ tenantId: data.tenant.id, amountCents: 1, smsUnits: 1 }),
});
assert(check.canSend === true, 'billing check should allow the smoke send');
const task = await request('/client/send/batch-tasks', {
method: 'POST',
body: JSON.stringify({
tenantId: data.tenant.id,
applicationId: data.application.id,
templateId: data.template.id,
content: '您的验证码为1234565分钟内有效。',
category: 'verification',
phones: ['13800138000', '13900139000'],
variables: { code: '123456' },
createdById: data.user.id,
sourceIp: '127.0.0.1',
userAgent: 'real-env-smoke',
}),
});
assert(task.id && task.messages?.length === 2, 'send batch task should create two messages');
const taskList = await request('/client/operations/batch-tasks', { headers: tenantHeader });
assert(taskList.some((item) => item.id === task.id), 'client task list should include smoke task');
const upload = await request('/admin/files/presigned-upload', {
method: 'POST',
body: JSON.stringify({ objectKey: `smoke/${Date.now()}-upload.txt`, expiresInSeconds: 300 }),
});
assert(upload.uploadUrl?.startsWith('http'), 'presigned upload should return an upload URL');
const putResponse = await fetch(upload.uploadUrl, {
method: 'PUT',
body: 'cmpp real env smoke upload',
headers: { 'content-type': 'text/plain' },
});
assert(putResponse.ok, `MinIO presigned PUT should succeed, got ${putResponse.status}`);
const log = await request('/admin/operation-logs', {
method: 'POST',
body: JSON.stringify({
tenantId: data.tenant.id,
userId: data.user.id,
action: 'smoke.verify',
resource: 'real-env-smoke',
resourceId: task.id,
ipAddress: '127.0.0.1',
userAgent: 'real-env-smoke',
detail: { taskNo: task.taskNo, rechargeOrderId: recharge.id, objectKey: upload.objectKey },
}),
});
assert(log.id, 'operation log should be created');
const logs = await request('/admin/operations/audit-logs', { headers: tenantHeader });
assert(logs.some((item) => item.id === log.id), 'operations audit logs should include smoke log');
const trace = await request(`/admin/operations/trace?tenantId=${data.tenant.id}&taskId=${task.id}`);
assert(trace.messages?.length >= 2, 'trace should include smoke messages');
const reconciliation = await request(`/admin/operations/reconciliation?tenantId=${data.tenant.id}&taskId=${task.id}`);
assert(reconciliation.messages?._count?._all >= 2, 'reconciliation should return message aggregate');
console.log(
JSON.stringify(
{
ok: true,
apiBaseUrl,
tenantId: data.tenant.id,
userId: data.user.id,
rechargeOrderId: recharge.id,
batchTaskId: task.id,
messageCount: task.messages.length,
uploadedObjectKey: upload.objectKey,
operationLogId: log.id,
},
null,
2,
),
);
}
run()
.catch((error) => {
console.error(error);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});