feat(history): add multi-account and WhatsApp foundation

This commit is contained in:
wmq
2026-07-30 00:14:32 +08:00
parent 7f09132fad
commit 9804341463
43 changed files with 7484 additions and 290 deletions
+119
View File
@@ -0,0 +1,119 @@
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 })
}
})