Files

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