| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- #!/usr/bin/env node
- import { createHash, createHmac, randomBytes, randomUUID } from 'node:crypto';
- import { spawn } from 'node:child_process';
- import { dirname, resolve } from 'node:path';
- import { fileURLToPath } from 'node:url';
- const port = 56000 + Math.floor(Math.random() * 3000);
- const origin = `http://127.0.0.1:${port}`;
- const pathBase = '/xiaoshu-sync';
- const base = `${origin}${pathBase}`;
- const keyId = 'local-smoke';
- const secret = randomBytes(32).toString('hex');
- const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
- let output = '';
- const dotnet = process.env.DOTNET_PATH || '/usr/local/bin/dotnet';
- const child = spawn(dotnet, [
- 'run', '--project', 'legacy-sync-bridge/Xiaoshu.LegacySyncBridge.csproj',
- '-c', 'Release', '--no-build', '--no-restore', '--urls', origin,
- ], {
- cwd: repoRoot,
- env: {
- ...process.env,
- ASPNETCORE_ENVIRONMENT: 'Production',
- DOTNET_ROLL_FORWARD: 'Major',
- ConnectionStrings__LegacySqlServer: 'Server=127.0.0.1,1;Database=not-used;User Id=none;Password=none;TrustServerCertificate=True;Connect Timeout=1',
- XiaoshuSync__KeyId: keyId,
- XiaoshuSync__Secret: secret,
- XiaoshuSync__PathBase: pathBase,
- },
- stdio: ['ignore', 'pipe', 'pipe'],
- });
- child.stdout.on('data', (chunk) => { output = `${output}${chunk}`.slice(-12000); });
- child.stderr.on('data', (chunk) => { output = `${output}${chunk}`.slice(-12000); });
- child.on('error', (error) => { output = `${output}\n${error.message}`.slice(-12000); });
- function signatureHeaders(method, pathAndQuery, body = '', timestamp = Math.floor(Date.now() / 1000), nonce = randomUUID().replaceAll('-', '')) {
- const bodyHash = createHash('sha256').update(body).digest('hex');
- const canonical = `${method.toUpperCase()}\n${pathAndQuery}\n${timestamp}\n${nonce}\n${bodyHash}`;
- return {
- 'X-Xiaoshu-Key-Id': keyId,
- 'X-Xiaoshu-Timestamp': String(timestamp),
- 'X-Xiaoshu-Nonce': nonce,
- 'X-Xiaoshu-Signature': createHmac('sha256', secret).update(canonical).digest('hex'),
- };
- }
- async function waitForHealth() {
- const deadline = Date.now() + 30000;
- while (Date.now() < deadline) {
- if (child.exitCode !== null) throw new Error(`同步桥提前退出:${output}`);
- try {
- const response = await fetch(`${base}/v1/health`);
- if (response.ok && (await response.json()).status === 'ok') return;
- } catch {}
- await new Promise((resolve) => setTimeout(resolve, 250));
- }
- throw new Error(`同步桥健康检查超时:${output}`);
- }
- async function expectStatus(url, expected, init = {}) {
- const response = await fetch(url, init);
- if (response.status !== expected) {
- const body = await response.text();
- throw new Error(`${url} 期望 HTTP ${expected},实际 ${response.status}: ${body.slice(0, 500)}`);
- }
- return response;
- }
- try {
- await waitForHealth();
- await expectStatus(`${base}/v1/manifest`, 401);
- const invalidPath = `${pathBase}/v1/changes?dataset=unsupported`;
- const nonce = randomUUID().replaceAll('-', '');
- const headers = signatureHeaders('GET', invalidPath, '', Math.floor(Date.now() / 1000), nonce);
- await expectStatus(`${origin}${invalidPath}`, 400, { headers });
- await expectStatus(`${origin}${invalidPath}`, 409, { headers });
- const expiredHeaders = signatureHeaders('GET', invalidPath, '', Math.floor(Date.now() / 1000) - 1000);
- await expectStatus(`${origin}${invalidPath}`, 401, { headers: expiredHeaders });
- const oversized = JSON.stringify({ actor: 'smoke', reason: 'request-size-guard', idempotencyKey: randomUUID(), payload: { value: 'x'.repeat(1_048_576) } });
- await expectStatus(`${base}/v1/commands/member.update-profile`, 413, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: oversized,
- });
- console.log(JSON.stringify({ passed: true, publicHealth: true, manifestRequiresSignature: true, pathBaseSignature: true, replayRejected: true, expiredSignatureRejected: true, requestSizeGuard: true }, null, 2));
- } finally {
- if (child.exitCode === null) child.kill('SIGTERM');
- await new Promise((resolve) => {
- if (child.exitCode !== null) return resolve();
- child.once('exit', resolve);
- setTimeout(() => { if (child.exitCode === null) child.kill('SIGKILL'); resolve(); }, 3000).unref();
- });
- }
|