// The server's traffic counters (server/http/traffic.js, GET /healthz?traffic=1; host bandwidth layer 0): the body // bytes every HTTP response wrote, by category, 304s apart, and the WebSocket's wire and payload bytes both ways. import { test } from 'node:test'; import assert from 'node:assert/strict'; import http from 'node:http'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { randomBytes } from 'node:crypto'; import { startServer } from '../server/index.js'; import { createTrafficCounter } from '../server/http/traffic.js'; import { TestClient } from './helpers/wsClient.js'; function get(port, p, headers = {}) { return new Promise((resolve, reject) => { const req = http.request({ host: '127.0.0.1', port, path: p, headers, agent: false }, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) })); }); req.on('error', reject); req.end(); }); } test('HTTP: body bytes by category as written (compressed), 304s counted, /healthz reports them only when asked', async () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sp-traffic-')); const w = (rel, content) => { fs.mkdirSync(path.dirname(path.join(tmp, rel)), { recursive: true }); fs.writeFileSync(path.join(tmp, rel), content); }; const js = `// app\n${'export const x = 1;\n'.repeat(400)}`; const png = randomBytes(3000); const mp3 = randomBytes(5000); w('public/index.html', '

hi

'); w('public/js/app.js', js); w('public/assets/img/a.png', png); w('public/assets/audio/bgm/a.mp3', mp3); w('data/config.json', JSON.stringify({ a: 1 })); w('shared/x.js', 'export const y = 2;'); const srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true, publicDir: path.join(tmp, 'public'), dataDir: path.join(tmp, 'data'), sharedDir: path.join(tmp, 'shared') }); try { const plain = await get(srv.port, '/health'); assert.equal(plain.status, 404); const gz = await get(srv.port, '/js/app.js', { 'accept-encoding': 'gzip' }); assert.ok(gz.body.length < js.length, 'compressed'); await get(srv.port, '/js/app.js', { 'if-none-match': (await get(srv.port, '/js/app.js')).headers.etag }); await get(srv.port, '/assets/img/a.png'); await get(srv.port, '/media/bgm/a'); await get(srv.port, '/data/config.json'); const before = await get(srv.port, '/healthz'); assert.equal(JSON.parse(before.body).traffic, undefined, 'plain /healthz stays as it was'); const t = JSON.parse((await get(srv.port, '/healthz?traffic=1')).body).traffic; assert.equal(t.http.code.requests, 3); assert.equal(t.http.code.bytes, gz.body.length + js.length, 'what was written: the gzip body, the plain body, nothing for the 304'); assert.equal(t.http.code.notModified, 1); assert.deepEqual(t.http.assets, { requests: 1, bytes: png.length, notModified: 0 }); assert.deepEqual(t.http.audio, { requests: 1, bytes: mp3.length, notModified: 0 }); assert.equal(t.http.data.requests, 1); assert.equal(t.http.other.requests, 2, 'the 404 page and the first /healthz'); assert.ok(t.http.other.bytes > 0); assert.equal(t.http.total.requests, 8); assert.equal(typeof t.since, 'string'); assert.equal(srv.traffic.snapshot().http.total.requests, 9, 'the traffic request itself once it has finished'); } finally { await srv.close(); fs.rmSync(tmp, { recursive: true, force: true }); } }); test('WebSocket: wire bytes both ways (after permessage-deflate) and the payloads, live and after the close', async () => { const srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true }); try { const c = await TestClient.connect(`ws://127.0.0.1:${srv.port}/ws`); await c.hello('流量计数'); const live = srv.traffic.snapshot().ws; assert.equal(live.connections, 1); assert.equal(live.open, 1); assert.ok(live.messagesOut >= 1 && live.payloadOut > 0, JSON.stringify(live)); assert.ok(live.messagesIn >= 1 && live.payloadIn > 0); assert.ok(live.wireOut > 0 && live.wireIn > 0); await c.close(); for (let i = 0; i < 50 && srv.traffic.snapshot().ws.open; i++) await new Promise((r) => setTimeout(r, 20)); const done = srv.traffic.snapshot().ws; assert.equal(done.open, 0); assert.ok(done.wireOut >= live.wireOut, 'the closed socket\'s bytes stay in the totals'); } finally { await srv.close(); } }); test('the counter on its own: a string body in another encoding, an aborted response, the payload of a fragmented message', () => { const tc = createTrafficCounter({ now: () => 1000 }); const listeners = {}; const res = { statusCode: 200, write() { return true; }, end() {}, once(ev, fn) { listeners[ev] = fn; } }; tc.meter(res, '/data/x.json'); res.write('aGk=', 'base64'); res.end('é'); listeners.close(); const s = tc.snapshot(); assert.deepEqual(s.http.data, { requests: 1, bytes: 2 + 2, notModified: 0 }); tc.wsSent('{"t":"pong"}'); tc.wsReceived([Buffer.alloc(3), Buffer.alloc(4)]); const ws = tc.snapshot().ws; assert.equal(ws.payloadOut, 12); assert.equal(ws.payloadIn, 7); assert.equal(tc.snapshot().seconds, 0); });