Files
hop.noname.company/apps/server/test/transfer-controls.test.js

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();
}
});