test: add real environment smoke coverage

This commit is contained in:
hectorzhao
2026-07-01 17:42:19 +08:00
parent bbbbe269a8
commit 7fa91675b8
3 changed files with 313 additions and 5 deletions
+295
View File
@@ -0,0 +1,295 @@
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 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,
displayName: 'Smoke Client Admin',
passwordHash: hashPassword(password),
status: 'active',
},
create: {
tenantId: tenant.id,
username,
displayName: 'Smoke Client Admin',
passwordHash: hashPassword(password),
status: 'active',
},
});
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',
gatewayHost: '127.0.0.1',
gatewayPort: 7890,
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: 7890,
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', status: 'active' },
create: { code: 'SMOKE_GROUP', name: 'Smoke Channel Group', 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, priority: 1, weight: 1, rateLimitPerSecond: 500 },
});
}
const routeRule = await prisma.channelRouteRule.findFirst({
where: { tenantId: tenant.id, groupId: group.id, status: 'active' },
});
if (!routeRule) {
await prisma.channelRouteRule.create({
data: { tenantId: tenant.id, groupId: group.id, priority: 1, status: 'active' },
});
}
const 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,
maxPhonesPerTask: 100000,
templateMismatchMode: 'reject',
status: 'active',
},
}));
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 }] },
},
}));
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 login = await request('/client/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
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();
});