import assert from 'node:assert/strict' import { spawn } from 'node:child_process' import fs from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import test from 'node:test' const token = 'gateway-test-token-12345678901234567890' async function waitForReady(baseUrl, child) { for (let attempt = 0; attempt < 80; attempt++) { if (child.exitCode !== null) throw new Error(`网关提前退出,代码 ${child.exitCode}`) try { const response = await fetch(`${baseUrl}/health/ready`) if (response.ok) return } catch { // 服务正在启动。 } await new Promise((resolve) => setTimeout(resolve, 50)) } throw new Error('等待 WhatsApp 网关启动超时') } test('网关执行健康检查、令牌鉴权和账号 CRUD', async () => { const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'lingxi-wa-gateway-')) const port = 32991 const baseUrl = `http://127.0.0.1:${port}` const child = spawn(process.execPath, ['src/index.js'], { cwd: path.resolve('.'), env: { ...process.env, PORT: String(port), DATA_DIR: dataDir, WHATSAPP_GATEWAY_TOKEN: token, LOG_LEVEL: 'silent', }, stdio: 'ignore', }) try { await waitForReady(baseUrl, child) const unauthorized = await fetch(`${baseUrl}/accounts`) assert.equal(unauthorized.status, 401) const headers = { authorization: `Bearer ${token}`, 'content-type': 'application/json', } const created = await fetch(`${baseUrl}/accounts`, { method: 'POST', headers, body: JSON.stringify({ phoneNumber: '15550008888', name: 'Gateway Test' }), }) assert.equal(created.status, 201) const account = (await created.json()).data assert.equal(account.phoneNumber, '15550008888') assert.equal(account.status, 'created') const duplicate = await fetch(`${baseUrl}/accounts`, { method: 'POST', headers, body: JSON.stringify({ phoneNumber: '15550008888' }), }) assert.equal(duplicate.status, 409) const fetched = await fetch(`${baseUrl}/accounts/${account.id}`, { headers }) assert.equal(fetched.status, 200) const webhookSecret = 'webhook-test-secret-that-must-be-encrypted' const webhook = await fetch(`${baseUrl}/accounts/${account.id}/webhook`, { method: 'PUT', headers, body: JSON.stringify({ url: 'http://127.0.0.1:39999/webhook', secret: webhookSecret, active: true, }), }) assert.equal(webhook.status, 200) assert.equal((await webhook.json()).data.webhookSecretConfigured, true) const persistedAccounts = await fs.readFile(path.join(dataDir, 'accounts.json'), 'utf8') assert.equal(persistedAccounts.includes(webhookSecret), false) const metrics = await fetch(`${baseUrl}/metrics`, { headers }) assert.equal(metrics.status, 200) assert.equal((await metrics.json()).data.accounts.total, 1) const invalidProxy = await fetch(`${baseUrl}/accounts/${account.id}/proxy`, { method: 'PUT', headers, body: JSON.stringify({ url: 'socks5://127.0.0.1:1080' }), }) assert.equal(invalidProxy.status, 400) const unsafeReauthorize = await fetch(`${baseUrl}/accounts/${account.id}/reauthorize`, { method: 'POST', headers, body: JSON.stringify({ confirm: 'wrong-account-id' }), }) assert.equal(unsafeReauthorize.status, 400) const backup = await fetch(`${baseUrl}/accounts/${account.id}/backup`, { headers }) assert.equal(backup.status, 200) const backupData = (await backup.json()).data assert.equal(backupData.accountId, account.id) assert.match(backupData.blob, /^v1\./) const removed = await fetch(`${baseUrl}/accounts/${account.id}`, { method: 'DELETE', headers, }) assert.equal(removed.status, 200) const missing = await fetch(`${baseUrl}/accounts/${account.id}`, { headers }) assert.equal(missing.status, 404) } finally { child.kill('SIGTERM') await new Promise((resolve) => child.once('exit', resolve)) await fs.rm(dataDir, { recursive: true, force: true }) } })