Clarify transfer rejections and improve storage readability
This commit is contained in:
@@ -339,6 +339,11 @@ test('거절, 만료, 취소 후 확정 금지와 거절 재요청 제한', asyn
|
||||
).statusCode,
|
||||
409,
|
||||
);
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const next = await f.request(a, b);
|
||||
assert.equal(next.statusCode, 200);
|
||||
await f.call(b, `/transfer-requests/${next.json().requestId}/reject`, 'POST', {});
|
||||
}
|
||||
assert.equal((await f.request(a, b)).statusCode, 429);
|
||||
const c = await f.create();
|
||||
await f.space(a, c);
|
||||
|
||||
@@ -4,6 +4,8 @@ import { fixture } from './helpers.js';
|
||||
import { Limiter, ApiError } from '../src/security.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { once } from 'node:events';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import * as schema from '../src/db/schema.js';
|
||||
|
||||
test('a known reconnecting peer returns a transient code without creating a request or charging quotas', async () => {
|
||||
const f = await fixture(true);
|
||||
@@ -100,25 +102,83 @@ test('transfer limit returns Retry-After and cooldown metadata without creating
|
||||
}
|
||||
});
|
||||
|
||||
test('rejection cooldown reports time and does not use a successful-transfer quota', async () => {
|
||||
test('only three consecutive recent rejections trigger a directional pair cooldown; denied retries do not charge quotas', async () => {
|
||||
const f = await fixture();
|
||||
try {
|
||||
f.cfg.sessionRate = 2;
|
||||
f.cfg.sessionRate = 4;
|
||||
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 < 3; i++) {
|
||||
const request = await f.request(a, b);
|
||||
assert.equal(request.statusCode, 200, `rejection ${i} must allow the next request`);
|
||||
const id = request.json().requestId;
|
||||
assert.equal(
|
||||
(await f.call(b, `/transfer-requests/${id}/reject`, 'POST', {})).statusCode,
|
||||
200,
|
||||
);
|
||||
assert.equal(
|
||||
(await f.call(b, `/transfer-requests/${id}/reject`, 'POST', {})).statusCode,
|
||||
409,
|
||||
);
|
||||
}
|
||||
let deadline;
|
||||
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);
|
||||
deadline ??= denied.json().retryAt;
|
||||
assert.equal(denied.json().retryAt, deadline, 'retry never extends the cooldown');
|
||||
}
|
||||
assert.equal((await f.request(a, c)).statusCode, 200);
|
||||
assert.equal((await f.request(b, a)).statusCode, 200, 'reverse direction is independent');
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('acceptance breaks a rejection streak; cancellation does not bypass it; cooldown expires without deleting history', async () => {
|
||||
const f = await fixture(false, undefined, { pairRate: 30, sessionRate: 50 });
|
||||
try {
|
||||
const a = await f.create(),
|
||||
b = await f.create();
|
||||
await f.space(a, b);
|
||||
const reject = async () => {
|
||||
const request = await f.request(a, b);
|
||||
assert.equal(request.statusCode, 200, request.body);
|
||||
const id = request.json().requestId;
|
||||
assert.equal(
|
||||
(await f.call(b, `/transfer-requests/${id}/reject`, 'POST', {})).statusCode,
|
||||
200,
|
||||
);
|
||||
return id;
|
||||
};
|
||||
await reject();
|
||||
await reject();
|
||||
await f.complete(a, b, '수락하면 누적 초기화');
|
||||
await reject();
|
||||
await reject();
|
||||
const cancelled = await f.request(a, b);
|
||||
assert.equal(cancelled.statusCode, 200);
|
||||
await f.call(a, `/transfer-requests/${cancelled.json().requestId}/cancel`, 'POST', {});
|
||||
const id = await reject();
|
||||
assert.equal((await f.request(a, b)).json().code, 'REJECTION_COOLDOWN');
|
||||
const cutoff = Date.now() - f.cfg.rejectCooldownMs;
|
||||
// At exactly the third rejection's deadline the same history no longer blocks.
|
||||
f.service.db
|
||||
.update(schema.requests)
|
||||
.set({ rejectedAt: cutoff })
|
||||
.where(eq(schema.requests.id, id))
|
||||
.run();
|
||||
assert.equal((await f.request(a, b)).statusCode, 200);
|
||||
assert.equal(
|
||||
f.service.db.select().from(schema.requests).where(eq(schema.requests.id, id)).get().status,
|
||||
'rejected',
|
||||
);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user