test: harden coverage and browser release gates

This commit is contained in:
KoalaDev
2026-08-21 14:10:35 +02:00
parent 368daa6e2e
commit 230e7f5932
29 changed files with 1176 additions and 806 deletions
+3 -1
View File
@@ -87,7 +87,9 @@ The server is covered by the root verification suite. From the repository root,
npm run verify
```
For focused server checks, see `scripts/test-server-ops.mjs`, `scripts/test-server-routes.mjs`, `scripts/test-server-ws.mjs`, and `scripts/test-rate-limiter.mjs`.
For focused server checks, run `npm run test:unit` for `server/ops.test.mjs`
and `server/rate-limiter.test.mjs`, or use `scripts/test-server-routes.mjs` and
`scripts/test-server-ws.mjs` for process-level integration coverage.
## Security
- **Rate Limiting**: IP-based connection limits and socket-based event limits.
+87
View File
@@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest';
import {
buildHealthPayload,
checkCooldown,
getCachedPayload,
isAdminMetricsAuthorized,
isAdminMetricsTokenStrong
} from './ops.js';
describe('server operational helpers', () => {
it('authorizes only an exact configured bearer token', () => {
expect(isAdminMetricsAuthorized(undefined, 'secret-token')).toBe(false);
expect(isAdminMetricsAuthorized('Bearer wrong-token', 'secret-token')).toBe(false);
expect(isAdminMetricsAuthorized('Bearer secret-token', 'secret-token')).toBe(true);
expect(isAdminMetricsAuthorized('Bearer secret-token', '')).toBe(false);
});
it('allows disabled metrics or strong admin tokens', () => {
expect(isAdminMetricsTokenStrong('')).toBe(true);
expect(isAdminMetricsTokenStrong('short-token')).toBe(false);
expect(isAdminMetricsTokenStrong('a'.repeat(32))).toBe(true);
});
it('tracks cooldowns and expires cached payloads deterministically', () => {
const cooldowns = new Map();
expect(checkCooldown(cooldowns, 'socket-1', 10_000, 100_000)).toBe(true);
expect(checkCooldown(cooldowns, 'socket-1', 10_000, 105_000)).toBe(false);
expect(checkCooldown(cooldowns, 'socket-1', 10_000, 110_000)).toBe(true);
const cache = new Map();
let buildCalls = 0;
const first = getCachedPayload(cache, 'health', 60_000, () => ({ value: ++buildCalls }), 1_000);
const cached = getCachedPayload(cache, 'health', 60_000, () => ({ value: ++buildCalls }), 30_000);
const expired = getCachedPayload(cache, 'health', 60_000, () => ({ value: ++buildCalls }), 61_001);
expect(cached).toBe(first);
expect(expired).toEqual({ value: 2 });
});
it('keeps public health minimal and exposes aggregate admin metrics', () => {
const rooms = new Map([
['room-a', { peers: new Set(['a', 'b']), activeLobby: null }],
['room-b', { peers: new Set(['c', 'd', 'e']), activeLobby: { expectedTitle: 'Episode 2' } }]
]);
const input = {
rooms,
connections: 5,
now: 1234,
uptime: 99,
memoryUsage: () => ({ rss: 10, heapUsed: 5, heapTotal: 8 }),
rateLimitSizes: {
connections: 1,
events: 2,
health: 3,
adminMetricsAuth: 4,
authFailures: 5,
roomList: 6,
leaveRoom: 7
}
};
expect(Object.keys(buildHealthPayload({ ...input, includeMetrics: false })).sort()).toEqual(
['connections', 'rooms', 'status', 'timestamp', 'uptime'].sort()
);
expect(buildHealthPayload({
...input,
includeMetrics: true,
rateLimitDenied: { leaveRoom: 8 }
})).toMatchObject({
peers: 5,
roomsWithLobby: 1,
avgPeersPerRoom: 2.5,
maxPeersInRoom: 3,
memory: { rss: 10, heapUsed: 5, heapTotal: 8 },
rateLimits: {
trackedClients: input.rateLimitSizes,
denied: {
connections: 0,
events: 0,
health: 0,
adminMetricsAuth: 0,
roomList: 0,
leaveRoom: 8
}
}
});
});
});
+65 -12
View File
@@ -1,28 +1,50 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
checkAdminMetricsAuthRate,
checkAuthRate,
checkConnectionRate,
checkEventRate,
checkHealthRate,
checkLeaveRoomRate,
checkChatMessageRate,
CONNECTION_RATE_LIMIT,
EVENT_RATE_LIMIT,
CHAT_MESSAGE_RATE_LIMIT,
CHAT_MESSAGE_RATE_WINDOW_MS,
chatMessageCounts,
failedAuthAttempts,
LEAVE_ROOM_RATE_LIMIT,
LEAVE_ROOM_RATE_WINDOW_MS,
rateLimitDenied,
leaveRoomCounts,
clearRateLimitMaps
clearRateLimitMaps,
recordAuthFailure,
startRateLimitCleanup,
stopRateLimitCleanup
} from './rate-limiter.js';
function resetRateLimits() {
stopRateLimitCleanup();
clearRateLimitMaps();
Object.assign(rateLimitDenied, {
connections: 0,
events: 0,
health: 0,
adminMetricsAuth: 0,
roomList: 0,
leaveRoom: 0,
chatMessages: 0
});
}
describe('LEAVE_ROOM Rate Limiter', () => {
const testSocketId = 'test-socket-123';
beforeEach(() => {
clearRateLimitMaps();
rateLimitDenied.leaveRoom = 0;
resetRateLimits();
});
afterEach(() => {
clearRateLimitMaps();
});
afterEach(resetRateLimits);
it('should allow LEAVE_ROOM within limit', () => {
// Test within the rate limit
@@ -98,7 +120,7 @@ describe('LEAVE_ROOM Rate Limiter', () => {
checkLeaveRoomRate(testSocketId);
expect(leaveRoomCounts.size).toBe(1);
clearRateLimitMaps();
resetRateLimits();
expect(leaveRoomCounts.size).toBe(0);
});
});
@@ -106,12 +128,9 @@ describe('LEAVE_ROOM Rate Limiter', () => {
describe('CHAT_MESSAGE Rate Limiter', () => {
const socketId = 'chat-socket';
beforeEach(() => {
clearRateLimitMaps();
rateLimitDenied.chatMessages = 0;
});
beforeEach(resetRateLimits);
afterEach(() => clearRateLimitMaps());
afterEach(resetRateLimits);
it('allows ten messages per ten-second window and blocks the next', () => {
for (let i = 0; i < CHAT_MESSAGE_RATE_LIMIT; i++) {
@@ -129,6 +148,40 @@ describe('CHAT_MESSAGE Rate Limiter', () => {
});
});
describe('remaining relay rate limits', () => {
beforeEach(resetRateLimits);
afterEach(resetRateLimits);
it.each([
['connection', checkConnectionRate, CONNECTION_RATE_LIMIT, 'ip-1', 'connections'],
['event', checkEventRate, EVENT_RATE_LIMIT, 'socket-1', 'events'],
['health', checkHealthRate, 10, 'ip-2', 'health'],
['admin metrics auth', checkAdminMetricsAuthRate, 5, 'ip-3', 'adminMetricsAuth']
])('enforces the %s window and increments its denial counter', (_label, check, limit, key, counter) => {
for (let attempt = 0; attempt < limit; attempt++) expect(check(key)).toBe(true);
expect(check(key)).toBe(false);
expect(rateLimitDenied[counter]).toBe(1);
expect(check(`${key}-other`)).toBe(true);
});
it('scopes failed authentication attempts to IP and room', () => {
for (let attempt = 0; attempt < 5; attempt++) recordAuthFailure('10.0.0.1', 'room-a');
expect(checkAuthRate('10.0.0.1', 'room-a')).toBe(false);
expect(checkAuthRate('10.0.0.1', 'room-b')).toBe(true);
expect(failedAuthAttempts.get('10.0.0.1:room-a')).toMatchObject({ count: 5 });
});
it('starts cleanup only once and can stop safely', () => {
const io = { sockets: { sockets: new Map() } };
expect(() => {
startRateLimitCleanup(io);
startRateLimitCleanup(io);
stopRateLimitCleanup();
stopRateLimitCleanup();
}).not.toThrow();
});
});
describe('Rate Limit Constants', () => {
it('should have correct rate limit values', () => {
expect(LEAVE_ROOM_RATE_LIMIT).toBe(10);