318 lines
10 KiB
JavaScript
318 lines
10 KiB
JavaScript
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',
|
||
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', 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 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: '您的验证码为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();
|
||
});
|