157 lines
5.9 KiB
JavaScript
157 lines
5.9 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { fixture } from './helpers.js';
|
|
import { Limiter, ApiError } from '../src/security.js';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { once } from 'node:events';
|
|
|
|
test('a known reconnecting peer returns a transient code without creating a request or charging quotas', async () => {
|
|
const f = await fixture(true);
|
|
try {
|
|
f.cfg.offlineGraceMs = 1000;
|
|
f.cfg.pairRate = 1;
|
|
const a = await f.create('8.8.8.8'),
|
|
b = await f.create('8.8.8.8');
|
|
const disconnected = once(f.service.io.sockets.sockets.get(b.socket.id), 'disconnect');
|
|
b.socket.disconnect();
|
|
await disconnected;
|
|
const key = randomUUID();
|
|
for (let i = 0; i < 3; i++) {
|
|
const response = await f.request(a, b, 'test', 'url', 1, key);
|
|
assert.equal(response.statusCode, 409);
|
|
assert.equal(response.json().code, 'TARGET_RECONNECTING');
|
|
assert.equal((await f.call(a, '/transfer-requests')).json().requests.length, 0);
|
|
}
|
|
await f.call(b, '/device/me', 'PATCH', { discoverable: false });
|
|
assert.equal((await f.request(a, b, 'test', 'url', 1, key)).json().code, 'TARGET_UNAVAILABLE');
|
|
await f.call(b, '/device/me', 'PATCH', { discoverable: true });
|
|
await f.connect(b);
|
|
const response = await f.request(a, b, 'test', 'url', 1, key);
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(
|
|
(await f.request(a, b, 'test', 'url', 1, key)).json().requestId,
|
|
response.json().requestId,
|
|
);
|
|
assert.equal((await f.call(a, '/transfer-requests')).json().requests.length, 1);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|
|
|
|
test('rate windows report their deadline; denied retries do not charge other quotas', (t) => {
|
|
let now = 1000;
|
|
t.mock.method(Date, 'now', () => now);
|
|
const limiter = new Limiter();
|
|
const limits = [
|
|
{ key: 'request-guest:a', max: 2 },
|
|
{ key: 'request-pair:a:b', max: 1 },
|
|
];
|
|
limiter.hitMany(limits);
|
|
now += 5000;
|
|
for (let i = 0; i < 20; i++)
|
|
assert.throws(
|
|
() => limiter.hitMany(limits),
|
|
(err) => {
|
|
assert(err instanceof ApiError);
|
|
assert.equal(err.status, 429);
|
|
assert.equal(err.retryAt, 61000);
|
|
assert.equal(err.limitScope, 'pair');
|
|
return true;
|
|
},
|
|
);
|
|
limiter.hitMany([
|
|
{ key: 'request-guest:a', max: 2 },
|
|
{ key: 'request-pair:a:c', max: 1 },
|
|
]);
|
|
assert.throws(() => limiter.hit('request-guest:a', 2));
|
|
now = 61000;
|
|
assert.doesNotThrow(() => limiter.hitMany(limits));
|
|
});
|
|
|
|
test('transfer limit returns Retry-After and cooldown metadata without creating a request', async () => {
|
|
const f = await fixture();
|
|
try {
|
|
f.cfg.pairRate = 2;
|
|
const a = await f.create(),
|
|
b = await f.create();
|
|
await f.space(a, b);
|
|
for (let i = 0; i < 2; i++) {
|
|
const response = await f.request(a, b);
|
|
assert.equal(response.statusCode, 200);
|
|
await f.call(a, `/transfer-requests/${response.json().requestId}/cancel`, 'POST', {});
|
|
}
|
|
const limited = await f.request(a, b);
|
|
assert.equal(limited.statusCode, 429);
|
|
assert.equal(limited.json().code, 'RATE_LIMIT');
|
|
assert.equal(limited.json().limitScope, 'pair');
|
|
assert(Number(limited.headers['retry-after']) > 0);
|
|
assert(Number(limited.headers['retry-after']) <= 60);
|
|
assert.equal(Number(limited.headers['retry-after']), limited.json().retryAfterSeconds);
|
|
assert(limited.json().retryAt > Date.now());
|
|
const next = await f.request(a, b);
|
|
assert.equal(next.json().retryAt, limited.json().retryAt);
|
|
assert.equal(
|
|
(await f.call(b, '/device/snapshot')).json().requests.filter((r) => r.status === 'pending')
|
|
.length,
|
|
0,
|
|
);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|
|
|
|
test('rejection cooldown reports time and does not use a successful-transfer quota', async () => {
|
|
const f = await fixture();
|
|
try {
|
|
f.cfg.sessionRate = 2;
|
|
const a = await f.create(),
|
|
b = await f.create(),
|
|
c = await f.create();
|
|
await f.space(a, b);
|
|
await f.space(a, c);
|
|
const request = await f.request(a, b);
|
|
await f.call(b, `/transfer-requests/${request.json().requestId}/reject`, 'POST', {});
|
|
for (let i = 0; i < 4; i++) {
|
|
const denied = await f.request(a, b);
|
|
assert.equal(denied.statusCode, 429);
|
|
assert.equal(denied.json().code, 'REJECTION_COOLDOWN');
|
|
assert.equal(denied.json().limitScope, 'pair');
|
|
assert(denied.json().retryAfterSeconds <= 300 && denied.json().retryAfterSeconds > 290);
|
|
}
|
|
assert.equal((await f.request(a, c)).statusCode, 200);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|
|
|
|
test('block list is private; unblock only removes the current device block and never revives a request', async () => {
|
|
const f = await fixture();
|
|
try {
|
|
const a = await f.create(),
|
|
b = await f.create(),
|
|
c = await f.create();
|
|
await f.space(a, b);
|
|
const id = (await f.request(a, b)).json().requestId;
|
|
await f.call(b, `/transfer-requests/${id}/block`, 'POST', {});
|
|
assert.equal((await f.call(null, '/device/blocks')).statusCode, 401);
|
|
assert.deepEqual((await f.call(a, '/device/blocks')).json().blocks, []);
|
|
const blocks = (await f.call(b, '/device/blocks')).json().blocks;
|
|
assert.equal(blocks.length, 1);
|
|
assert.equal(blocks[0].deviceId, a.deviceId);
|
|
assert.deepEqual(Object.keys(blocks[0]).sort(), ['code', 'deviceId', 'name']);
|
|
await f.call(c, `/device/blocks/${a.deviceId}`, 'DELETE');
|
|
assert.equal((await f.call(b, '/device/blocks')).json().blocks.length, 1);
|
|
assert.equal((await f.request(a, b)).statusCode, 403);
|
|
assert.equal((await f.call(b, `/device/blocks/${a.deviceId}`, 'DELETE')).statusCode, 200);
|
|
assert.deepEqual((await f.call(b, '/device/blocks')).json().blocks, []);
|
|
assert.equal(
|
|
(await f.call(b, `/transfer-requests/${id}/accept`, 'POST', {}, crypto.randomUUID()))
|
|
.statusCode,
|
|
409,
|
|
);
|
|
assert.equal((await f.request(a, b)).statusCode, 200);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|