201 lines
5.4 KiB
TypeScript
201 lines
5.4 KiB
TypeScript
// Copyright 2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import { assert } from 'chai';
|
|
import * as sinon from 'sinon';
|
|
import { noop, omit } from 'lodash';
|
|
import { HTTPError, SendMessageProtoError } from '../../../textsecure/Errors';
|
|
import { SECOND } from '../../../util/durations';
|
|
|
|
import {
|
|
handleMultipleSendErrors,
|
|
maybeExpandErrors,
|
|
} from '../../../jobs/helpers/handleMultipleSendErrors';
|
|
|
|
describe('maybeExpandErrors', () => {
|
|
// This returns a readonly array, but Chai wants a mutable one.
|
|
const expand = (input: unknown) => maybeExpandErrors(input) as Array<unknown>;
|
|
|
|
it("wraps the provided value if it's not a SendMessageProtoError with errors", () => {
|
|
const input = { foo: 123 };
|
|
assert.sameMembers(expand(input), [input]);
|
|
});
|
|
|
|
it('wraps the provided value if a SendMessageProtoError with no errors', () => {
|
|
const input = new SendMessageProtoError({});
|
|
assert.sameMembers(expand(input), [input]);
|
|
});
|
|
|
|
it("uses a SendMessageProtoError's errors", () => {
|
|
const errors = [new Error('one'), new Error('two')];
|
|
const input = new SendMessageProtoError({ errors });
|
|
assert.strictEqual(expand(input), errors);
|
|
});
|
|
});
|
|
|
|
describe('handleMultipleSendErrors', () => {
|
|
const makeSlowDown = (code: 413 | 429, retryAfter: number): HTTPError =>
|
|
new HTTPError('Slow down', {
|
|
code,
|
|
headers: { 'retry-after': retryAfter.toString() },
|
|
response: {},
|
|
});
|
|
|
|
const defaultOptions = {
|
|
isFinalAttempt: false,
|
|
log: { info: noop },
|
|
markFailed: () => {
|
|
throw new Error('This should not be called');
|
|
},
|
|
timeRemaining: 1234,
|
|
};
|
|
|
|
let sandbox: sinon.SinonSandbox;
|
|
let clock: sinon.SinonFakeTimers;
|
|
|
|
beforeEach(() => {
|
|
sandbox = sinon.createSandbox();
|
|
clock = sandbox.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
sandbox.restore();
|
|
});
|
|
|
|
it('throws the first provided error', async () => {
|
|
await assert.isRejected(
|
|
handleMultipleSendErrors({
|
|
...defaultOptions,
|
|
errors: [new Error('first'), new Error('second')],
|
|
toThrow: new Error('to throw'),
|
|
}),
|
|
'to throw'
|
|
);
|
|
});
|
|
|
|
it("marks the send failed if it's the final attempt", async () => {
|
|
const markFailed = sinon.stub();
|
|
|
|
await assert.isRejected(
|
|
handleMultipleSendErrors({
|
|
...defaultOptions,
|
|
errors: [new Error('uh oh')],
|
|
markFailed,
|
|
isFinalAttempt: true,
|
|
toThrow: new Error('to throw'),
|
|
})
|
|
);
|
|
|
|
sinon.assert.calledOnceWithExactly(markFailed);
|
|
});
|
|
|
|
it("doesn't require `markFailed`", async () => {
|
|
await assert.isRejected(
|
|
handleMultipleSendErrors({
|
|
...omit(defaultOptions, 'markFailed'),
|
|
errors: [new Error('Test message')],
|
|
isFinalAttempt: true,
|
|
toThrow: new Error('to throw'),
|
|
}),
|
|
'to throw'
|
|
);
|
|
});
|
|
|
|
for (const code of [413, 429] as const) {
|
|
// eslint-disable-next-line no-loop-func
|
|
describe(`${code} handling`, () => {
|
|
it(`sleeps for the longest ${code} Retry-After time`, async () => {
|
|
let done = false;
|
|
|
|
void (async () => {
|
|
try {
|
|
await handleMultipleSendErrors({
|
|
...defaultOptions,
|
|
errors: [
|
|
new Error('Other'),
|
|
makeSlowDown(code, 10),
|
|
makeSlowDown(code, 999),
|
|
makeSlowDown(code, 20),
|
|
],
|
|
timeRemaining: 99999999,
|
|
toThrow: new Error('to throw'),
|
|
});
|
|
} catch (err) {
|
|
// No-op
|
|
} finally {
|
|
done = true;
|
|
}
|
|
})();
|
|
|
|
await clock.tickAsync(900 * SECOND);
|
|
assert.isFalse(done, "Didn't sleep for long enough");
|
|
await clock.tickAsync(100 * SECOND);
|
|
assert.isTrue(done, 'Slept for too long');
|
|
});
|
|
|
|
it("doesn't sleep longer than the remaining time", async () => {
|
|
let done = false;
|
|
|
|
void (async () => {
|
|
try {
|
|
await handleMultipleSendErrors({
|
|
...defaultOptions,
|
|
errors: [makeSlowDown(code, 9999)],
|
|
timeRemaining: 99,
|
|
toThrow: new Error('to throw'),
|
|
});
|
|
} catch (err) {
|
|
// No-op
|
|
} finally {
|
|
done = true;
|
|
}
|
|
})();
|
|
|
|
await clock.tickAsync(100);
|
|
assert.isTrue(done);
|
|
});
|
|
|
|
it("doesn't sleep if it's the final attempt", async () => {
|
|
await assert.isRejected(
|
|
handleMultipleSendErrors({
|
|
...defaultOptions,
|
|
errors: [new Error('uh oh')],
|
|
isFinalAttempt: true,
|
|
toThrow: new Error('to throw'),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
describe('508 handling', () => {
|
|
it('resolves with no error if any 508 is received', async () => {
|
|
await assert.isFulfilled(
|
|
handleMultipleSendErrors({
|
|
...defaultOptions,
|
|
errors: [
|
|
new Error('uh oh'),
|
|
{ code: 508 },
|
|
makeSlowDown(413, 99999),
|
|
makeSlowDown(429, 99999),
|
|
],
|
|
markFailed: noop,
|
|
toThrow: new Error('to throw'),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('marks the send failed on a 508', async () => {
|
|
const markFailed = sinon.stub();
|
|
|
|
await handleMultipleSendErrors({
|
|
...defaultOptions,
|
|
errors: [{ code: 508 }],
|
|
markFailed,
|
|
toThrow: new Error('to throw'),
|
|
});
|
|
|
|
sinon.assert.calledOnceWithExactly(markFailed);
|
|
});
|
|
});
|
|
});
|