test: add real environment smoke coverage
This commit is contained in:
@@ -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: '您的验证码为123456,5分钟内有效。',
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user