40 lines
2.8 KiB
TypeScript
40 lines
2.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import {test} from 'node:test';
|
|
import {database,closeDatabase} from '../lib/store.ts';
|
|
import {reserveRequest,limitHost,retryDelay,waitForHost,HostCooldown} from '../lib/throttle.ts';
|
|
import {checkLink} from '../lib/checker.ts';
|
|
import {useTestDatabase} from './test-database.ts';
|
|
useTestDatabase('throttle');
|
|
|
|
test('concurrent reservations space one host by two seconds; other hosts are independent',async()=>{
|
|
const slots=(await Promise.all([reserveRequest('one.org'),reserveRequest('one.org'),reserveRequest('one.org')])).sort((a,b)=>a-b);
|
|
for(let i=1;i<slots.length;i++)assert.equal(slots[i]-slots[i-1],2000);
|
|
assert.ok(await reserveRequest('two.org')<slots[1]);
|
|
});
|
|
test('Retry-After handles seconds, dates, invalid and excessive values',()=>{
|
|
const now=Date.parse('2026-10-29T00:00:00Z');
|
|
assert.equal(retryDelay('120',now),120000);assert.equal(retryDelay('Thu, 29 Oct 2026 00:02:00 GMT',now),120000);
|
|
for(const value of [undefined,'garbage','0','-1','Wed, 28 Oct 2026 00:00:00 GMT'])assert.equal(retryDelay(value,now),60000);
|
|
assert.equal(retryDelay('172800',now),172800000);
|
|
for(const value of ['99999999999999999999999999','9'.repeat(400)])assert.equal(new Date(now+retryDelay(value,now)).toISOString(),'+275760-09-13T00:00:00.000Z');
|
|
});
|
|
test('cooldown survives reconnect, cannot be shortened and prevents a network attempt',async()=>{
|
|
await limitHost('limited.org',429,'600');await limitHost('limited.org',503,'1');await closeDatabase();
|
|
await assert.rejects(waitForHost('limited.org'),HostCooldown);
|
|
const result=await checkLink('https://limited.org/book');assert.equal(result.status,'blocked');assert.match(result.reason,/Пауза до/);
|
|
const {rows}=await (await database()).query("SELECT value FROM records WHERE key='throttle:limited.org'");
|
|
assert.ok(JSON.parse(rows[0].value).blockedUntil>Date.now()+590000);
|
|
});
|
|
test('403 backs off for five minutes and successful responses do not block hosts',async()=>{
|
|
await limitHost('forbidden.org',403);await assert.rejects(reserveRequest('forbidden.org'),HostCooldown);
|
|
await limitHost('available.org',200);await reserveRequest('available.org');
|
|
});
|
|
test('expired cooldown permits requests and waiting is abortable',async()=>{
|
|
await limitHost('expired.org',429);await (await database()).query("UPDATE records SET value=$1 WHERE key='throttle:expired.org'",[JSON.stringify({nextAt:0,blockedUntil:Date.now()-1})]);
|
|
await reserveRequest('expired.org');
|
|
const controller=new AbortController();controller.abort();await assert.rejects(waitForHost('expired.org',controller.signal),{name:'AbortError'});
|
|
});
|
|
test('the shared gate actually waits before allowing a second request',async()=>{
|
|
await waitForHost('paced.org');const started=Date.now();await waitForHost('paced.org');assert.ok(Date.now()-started>=1900);
|
|
});
|