Bound telemetry player identities (Refs #74)
CI / checks (push) Successful in 4m48s

This commit is contained in:
2026-10-09 17:08:55 -04:00
parent bd16737dd8
commit f0afd6de67
3 changed files with 201 additions and 10 deletions
+182
View File
@@ -0,0 +1,182 @@
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { JSON_BODY_LIMIT } from '../../src/lib/server/request-body.js';
import { newFixture, type Fixture } from '../helpers/fixture.js';
import { gameEvent, postTelemetry as post, TEST_TELEMETRY_TOKEN } from '../helpers/telemetry.js';
const events = ['game_start', 'round_change', 'heartbeat', 'game_end'] as const;
const identityLimit = 255;
let fixture: Fixture;
beforeEach(async () => {
vi.resetModules();
vi.stubEnv('EC_TELEMETRY_TOKEN', TEST_TELEMETRY_TOKEN);
fixture = await newFixture();
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
await seedServerRegistry();
});
afterEach(async () => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
await fixture.cleanup();
});
async function storedEvidence() {
const { db } = await import('../../src/lib/server/db/index.js');
return {
telemetry: db().prepare('SELECT * FROM server_telemetry').all(),
players: db().prepare('SELECT * FROM leaderboard_players').all(),
matches: db().prepare('SELECT * FROM leaderboard_matches').all(),
participation: db().prepare('SELECT * FROM leaderboard_match_players').all(),
beta: db().prepare('SELECT * FROM beta_test_evidence').all(),
lifecycle: db()
.prepare(
'SELECT active_since, inactive_since, game_started_at, game_ended_at FROM servers'
)
.all()
};
}
function streamedRequest(chunks: Uint8Array[], headers: Record<string, string> = {}) {
let index = 0;
const pull = vi.fn((controller: ReadableStreamDefaultController<Uint8Array>) => {
if (index === chunks.length) controller.close();
else controller.enqueue(chunks[index++]);
});
const cancel = vi.fn();
const request = new Request('http://localhost/api/telemetry', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body: new ReadableStream({ pull, cancel }, { highWaterMark: 0 }),
duplex: 'half'
} as RequestInit);
return { request, pull, cancel };
}
it.each(events)('preserves exact-limit player identities for %s', async (event) => {
const payload = gameEvent(event, 17, 1);
// Lengths use UTF-16 code units, while the request cap counts UTF-8 bytes.
const name = ` ${'🍒'.repeat(126)}é `;
const guid = `000${'9'.repeat(identityLimit - 3)}`;
expect(name.length).toBe(identityLimit);
payload.players[0] = { ...payload.players[0], name, guid };
const response = await post(payload);
expect(response.status).toBe(200);
expect((await response.json()).telemetry.players).toEqual(payload.players);
const { listTelemetry } = await import('../../src/lib/server/fleet/telemetry.js');
expect(listTelemetry()[0].players).toEqual(payload.players);
const { db } = await import('../../src/lib/server/db/index.js');
expect(db().prepare('SELECT name, guid FROM leaderboard_players').all()).toEqual([
{ name, guid }
]);
});
it('preserves an opaque Unicode GUID at the length limit', async () => {
const payload = gameEvent('game_start', 0, 1);
payload.players[0].guid = ` ${'識'.repeat(identityLimit - 2)} `;
const response = await post(payload);
expect(response.status).toBe(200);
expect((await response.json()).telemetry.players).toEqual(payload.players);
});
it.each(events)('rejects oversized %s identities without changing game evidence', async (event) => {
expect((await post(gameEvent('heartbeat', 5))).status).toBe(200);
const before = await storedEvidence();
const { subscribe } = await import('../../src/lib/server/pipeline/bus.js');
const listener = vi.fn();
const unsubscribe = subscribe(listener);
try {
for (const field of ['name', 'guid'] as const) {
for (const length of [identityLimit + 1, 20_000]) {
const payload = gameEvent(event, 17, 1);
payload.players[0] = { ...payload.players[0], [field]: 'x'.repeat(length) };
const response = await post(payload);
expect(response.status).toBe(400);
const result = await response.json();
expect(result).toMatchObject({ ok: false });
expect(result.error).toContain(`players[0].${field}`);
expect(result.error).toContain(String(identityLimit));
}
}
expect(await storedEvidence()).toEqual(before);
expect(listener).not.toHaveBeenCalled();
} finally {
unsubscribe();
}
});
it('accepts a telemetry body at the exact 1 MiB UTF-8 byte limit', async () => {
const payload = gameEvent('heartbeat', 17, 1);
const json = JSON.stringify({ ...payload, diagnostic: 'é🍒' });
const bytes = Buffer.from(json + ' '.repeat(JSON_BODY_LIMIT - Buffer.byteLength(json)));
expect(bytes.byteLength).toBe(1024 * 1024);
const { request } = streamedRequest([bytes.subarray(0, 100), bytes.subarray(100)], {
Authorization: `Bearer ${TEST_TELEMETRY_TOKEN}`,
'Content-Length': String(bytes.byteLength)
});
const { POST } = await import('../../src/routes/api/telemetry/+server.js');
const response = await POST({ request });
expect(response.status).toBe(200);
expect((await response.json()).telemetry).toMatchObject(payload);
});
it.each(['declared', 'absent', 'understated'] as const)(
'rejects body overflow with %s Content-Length before storing or broadcasting it',
async (length) => {
expect((await post(gameEvent('heartbeat', 5))).status).toBe(200);
const before = await storedEvidence();
const { db } = await import('../../src/lib/server/db/index.js');
const diagnostics = db().prepare('SELECT * FROM telemetry_requests').all();
const payload = JSON.stringify(gameEvent('game_end', 99));
const bytes = Buffer.from(
payload + ' '.repeat(JSON_BODY_LIMIT - Buffer.byteLength(payload))
);
const headers: Record<string, string> = {
Authorization: `Bearer ${TEST_TELEMETRY_TOKEN}`
};
if (length !== 'absent')
headers['Content-Length'] = length === 'declared' ? String(JSON_BODY_LIMIT + 1) : '1';
const { request, pull, cancel } = streamedRequest(
[bytes, Uint8Array.of(32), Buffer.from('unread')],
headers
);
const { POST } = await import('../../src/routes/api/telemetry/+server.js');
const { subscribe } = await import('../../src/lib/server/pipeline/bus.js');
const listener = vi.fn();
const unsubscribe = subscribe(listener);
try {
await expect(POST({ request })).rejects.toMatchObject({ status: 413 });
expect(pull).toHaveBeenCalledTimes(length === 'declared' ? 0 : 2);
expect(cancel).toHaveBeenCalledOnce();
expect(await storedEvidence()).toEqual(before);
expect(db().prepare('SELECT * FROM telemetry_requests').all()).toEqual(diagnostics);
expect(listener).not.toHaveBeenCalled();
} finally {
unsubscribe();
}
}
);
it.each([401, 503])('preserves authentication rejection %s before body limits', async (status) => {
if (status === 503) vi.stubEnv('EC_TELEMETRY_TOKEN', undefined);
const before = await storedEvidence();
const { request, pull } = streamedRequest([Buffer.alloc(JSON_BODY_LIMIT + 1)], {
'Content-Length': String(JSON_BODY_LIMIT + 1)
});
const { POST } = await import('../../src/routes/api/telemetry/+server.js');
const { subscribe } = await import('../../src/lib/server/pipeline/bus.js');
const listener = vi.fn();
const unsubscribe = subscribe(listener);
try {
expect((await POST({ request })).status).toBe(status);
expect(pull).not.toHaveBeenCalled();
expect(request.bodyUsed).toBe(false);
expect(await storedEvidence()).toEqual(before);
const { db } = await import('../../src/lib/server/db/index.js');
expect(db().prepare('SELECT * FROM telemetry_requests').all()).toEqual([]);
expect(listener).not.toHaveBeenCalled();
} finally {
unsubscribe();
}
});