mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-24 01:28:33 +02:00
Wownerogue
A browser-based roguelike synchronized with crypto block timing. A run is a race against the next block: reach the exit before the block lands, or the house wins. The same codebase runs the solo dungeon, the social Tavern, and operator-selected PvP and race rulesets, with separate free and paid leaderboards. Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md for the design, and docs/DEPLOY.md to operate an instance.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Rate-limiter storage-key resolution.
|
||||
*
|
||||
* Keys are `${id}:${action}`, and the id itself contains colons: `u:<userId>` / `s:<socketId>` from
|
||||
* rateLimitContext.stableId, plus raw IPv6 addresses in ipStorage. The old key parser stripped only
|
||||
* the first `:`-delimited segment, so `u:123:chat:message` resolved to the action `123:chat:message`
|
||||
* and matched nothing. Both consequences were silent:
|
||||
*
|
||||
* - `cleanup()` skips entries whose limit it cannot resolve, so the ONLY registered memory
|
||||
* reclamation freed nothing and both Maps grew for the lifetime of the process;
|
||||
* - `_recordSingleAttempt` never reset an expired window, so counts accumulated forever.
|
||||
*/
|
||||
|
||||
const RateLimiter = require('../src/network/rateLimiter');
|
||||
|
||||
describe('rate limiter key resolution', () => {
|
||||
let limiter;
|
||||
beforeEach(() => { limiter = new RateLimiter(); });
|
||||
afterEach(() => { if (limiter.shutdown) limiter.shutdown(); });
|
||||
|
||||
test.each([
|
||||
['stable user id', 'u:123:chat:message', 'chat:message'],
|
||||
['socket-scoped id', 's:AbCdEf123456:game:queue', 'game:queue'],
|
||||
['bare IPv4', '203.0.113.9:chat:message', 'chat:message'],
|
||||
['IPv6 (colon-dense)', '2001:db8:85a3::8a2e:370:7334:chat:message', 'chat:message']
|
||||
])('resolves the configured limit for a %s key', (_label, key, action) => {
|
||||
expect(limiter._getLimitFromKey(key)).toBe(limiter.limits[action]);
|
||||
});
|
||||
|
||||
test('returns undefined for a key with no configured action', () => {
|
||||
expect(limiter._getLimitFromKey('u:123:not:a:real:action')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('cleanup() actually frees expired entries for realistic keys', () => {
|
||||
const key = 'u:456:chat:message';
|
||||
const window = limiter.limits['chat:message'].window;
|
||||
limiter.storage.set(key, { count: 9, firstAttempt: Date.now() - window - 1000 });
|
||||
limiter.ipStorage.set('2001:db8::1:chat:message', {
|
||||
count: 4, firstAttempt: Date.now() - window - 1000
|
||||
});
|
||||
|
||||
limiter.cleanup();
|
||||
|
||||
expect(limiter.storage.has(key)).toBe(false);
|
||||
expect(limiter.ipStorage.size).toBe(0);
|
||||
});
|
||||
|
||||
test('cleanup() leaves entries whose window is still open', () => {
|
||||
const key = 'u:789:chat:message';
|
||||
limiter.storage.set(key, { count: 1, firstAttempt: Date.now() });
|
||||
limiter.cleanup();
|
||||
expect(limiter.storage.has(key)).toBe(true);
|
||||
});
|
||||
|
||||
test('an expired window resets the count instead of accumulating forever', () => {
|
||||
const action = 'chat:message';
|
||||
const key = `u:321:${action}`;
|
||||
const window = limiter.limits[action].window;
|
||||
limiter.storage.set(key, { count: 999, firstAttempt: Date.now() - window - 1000 });
|
||||
|
||||
limiter._recordSingleAttempt(key, Date.now());
|
||||
|
||||
expect(limiter.storage.get(key).count).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user