mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-13 10:03:31 +01:00
chore: monorepo setup (#7175)
This commit is contained in:
115
packages/rest/__tests__/CDN.test.ts
Normal file
115
packages/rest/__tests__/CDN.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { CDN } from '../src';
|
||||
|
||||
const base = 'https://discord.com';
|
||||
const id = '123456';
|
||||
const hash = 'abcdef';
|
||||
const animatedHash = 'a_bcdef';
|
||||
const defaultAvatar = 1234 % 5;
|
||||
|
||||
const cdn = new CDN(base);
|
||||
|
||||
test('appAsset default', () => {
|
||||
expect(cdn.appAsset(id, hash)).toBe(`${base}/app-assets/${id}/${hash}.png`);
|
||||
});
|
||||
|
||||
test('appIcon default', () => {
|
||||
expect(cdn.appIcon(id, hash)).toBe(`${base}/app-icons/${id}/${hash}.png`);
|
||||
});
|
||||
|
||||
test('avatar default', () => {
|
||||
expect(cdn.avatar(id, hash)).toBe(`${base}/avatars/${id}/${hash}.png`);
|
||||
});
|
||||
|
||||
test('avatar dynamic-animated', () => {
|
||||
expect(cdn.avatar(id, animatedHash, { dynamic: true })).toBe(`${base}/avatars/${id}/${animatedHash}.gif`);
|
||||
});
|
||||
|
||||
test('avatar dynamic-not-animated', () => {
|
||||
expect(cdn.avatar(id, hash, { dynamic: true })).toBe(`${base}/avatars/${id}/${hash}.png`);
|
||||
});
|
||||
|
||||
test('banner default', () => {
|
||||
expect(cdn.banner(id, hash)).toBe(`${base}/banners/${id}/${hash}.png`);
|
||||
});
|
||||
|
||||
test('channelIcon default', () => {
|
||||
expect(cdn.channelIcon(id, hash)).toBe(`${base}/channel-icons/${id}/${hash}.png`);
|
||||
});
|
||||
|
||||
test('defaultAvatar default', () => {
|
||||
expect(cdn.defaultAvatar(defaultAvatar)).toBe(`${base}/embed/avatars/${defaultAvatar}.png`);
|
||||
});
|
||||
|
||||
test('discoverySplash default', () => {
|
||||
expect(cdn.discoverySplash(id, hash)).toBe(`${base}/discovery-splashes/${id}/${hash}.png`);
|
||||
});
|
||||
|
||||
test('emoji default', () => {
|
||||
expect(cdn.emoji(id)).toBe(`${base}/emojis/${id}.png`);
|
||||
});
|
||||
|
||||
test('emoji gif', () => {
|
||||
expect(cdn.emoji(id, 'gif')).toBe(`${base}/emojis/${id}.gif`);
|
||||
});
|
||||
|
||||
test('guildMemberAvatar default', () => {
|
||||
expect(cdn.guildMemberAvatar(id, id, hash)).toBe(`${base}/guilds/${id}/users/${id}/avatars/${hash}.png`);
|
||||
});
|
||||
|
||||
test('guildMemberAvatar dynamic-animated', () => {
|
||||
expect(cdn.guildMemberAvatar(id, id, animatedHash, { dynamic: true })).toBe(
|
||||
`${base}/guilds/${id}/users/${id}/avatars/${animatedHash}.gif`,
|
||||
);
|
||||
});
|
||||
|
||||
test('guildMemberAvatar dynamic-not-animated', () => {
|
||||
expect(cdn.guildMemberAvatar(id, id, hash, { dynamic: true })).toBe(
|
||||
`${base}/guilds/${id}/users/${id}/avatars/${hash}.png`,
|
||||
);
|
||||
});
|
||||
|
||||
test('icon default', () => {
|
||||
expect(cdn.icon(id, hash)).toBe(`${base}/icons/${id}/${hash}.png`);
|
||||
});
|
||||
|
||||
test('icon dynamic-animated', () => {
|
||||
expect(cdn.icon(id, animatedHash, { dynamic: true })).toBe(`${base}/icons/${id}/${animatedHash}.gif`);
|
||||
});
|
||||
|
||||
test('icon dynamic-not-animated', () => {
|
||||
expect(cdn.icon(id, hash, { dynamic: true })).toBe(`${base}/icons/${id}/${hash}.png`);
|
||||
});
|
||||
|
||||
test('role icon default', () => {
|
||||
expect(cdn.roleIcon(id, hash)).toBe(`${base}/role-icons/${id}/${hash}.png`);
|
||||
});
|
||||
|
||||
test('splash default', () => {
|
||||
expect(cdn.splash(id, hash)).toBe(`${base}/splashes/${id}/${hash}.png`);
|
||||
});
|
||||
|
||||
test('sticker default', () => {
|
||||
expect(cdn.sticker(id)).toBe(`${base}/stickers/${id}.png`);
|
||||
});
|
||||
|
||||
test('stickerPackBanner default', () => {
|
||||
expect(cdn.stickerPackBanner(id)).toBe(`${base}/app-assets/710982414301790216/store/${id}.png`);
|
||||
});
|
||||
|
||||
test('teamIcon default', () => {
|
||||
expect(cdn.teamIcon(id, hash)).toBe(`${base}/team-icons/${id}/${hash}.png`);
|
||||
});
|
||||
|
||||
test('makeURL throws on invalid size', () => {
|
||||
// @ts-expect-error: Invalid size
|
||||
expect(() => cdn.avatar(id, animatedHash, { size: 5 })).toThrow(RangeError);
|
||||
});
|
||||
|
||||
test('makeURL throws on invalid extension', () => {
|
||||
// @ts-expect-error: Invalid extension
|
||||
expect(() => cdn.avatar(id, animatedHash, { extension: 'tif' })).toThrow(RangeError);
|
||||
});
|
||||
|
||||
test('makeURL valid size', () => {
|
||||
expect(cdn.avatar(id, animatedHash, { size: 512 })).toBe(`${base}/avatars/${id}/${animatedHash}.png?size=512`);
|
||||
});
|
||||
142
packages/rest/__tests__/DiscordAPIError.test.ts
Normal file
142
packages/rest/__tests__/DiscordAPIError.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { DiscordAPIError } from '../src';
|
||||
|
||||
test('Unauthorized', () => {
|
||||
const error = new DiscordAPIError(
|
||||
{ message: '401: Unauthorized', code: 0 },
|
||||
0,
|
||||
401,
|
||||
'PATCH',
|
||||
'https://discord.com/api/v9/guilds/:id',
|
||||
{
|
||||
attachments: undefined,
|
||||
body: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
expect(error.code).toBe(0);
|
||||
expect(error.message).toBe('401: Unauthorized');
|
||||
expect(error.method).toBe('PATCH');
|
||||
expect(error.name).toBe('DiscordAPIError[0]');
|
||||
expect(error.status).toBe(401);
|
||||
expect(error.url).toBe('https://discord.com/api/v9/guilds/:id');
|
||||
expect(error.requestBody.attachments).toBe(undefined);
|
||||
expect(error.requestBody.json).toBe(undefined);
|
||||
});
|
||||
|
||||
test('Invalid Form Body Error (error.{property}._errors.{index})', () => {
|
||||
const error = new DiscordAPIError(
|
||||
{
|
||||
code: 50035,
|
||||
errors: {
|
||||
username: { _errors: [{ code: 'BASE_TYPE_BAD_LENGTH', message: 'Must be between 2 and 32 in length.' }] },
|
||||
},
|
||||
message: 'Invalid Form Body',
|
||||
},
|
||||
50035,
|
||||
400,
|
||||
'PATCH',
|
||||
'https://discord.com/api/v9/users/@me',
|
||||
{
|
||||
attachments: undefined,
|
||||
body: {
|
||||
username: 'a',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(error.code).toBe(50035);
|
||||
expect(error.message).toBe(
|
||||
['Invalid Form Body', 'username[BASE_TYPE_BAD_LENGTH]: Must be between 2 and 32 in length.'].join('\n'),
|
||||
);
|
||||
expect(error.method).toBe('PATCH');
|
||||
expect(error.name).toBe('DiscordAPIError[50035]');
|
||||
expect(error.status).toBe(400);
|
||||
expect(error.url).toBe('https://discord.com/api/v9/users/@me');
|
||||
expect(error.requestBody.attachments).toBe(undefined);
|
||||
expect(error.requestBody.json).toStrictEqual({ username: 'a' });
|
||||
});
|
||||
|
||||
test('Invalid FormFields Error (error.errors.{property}.{property}.{index}.{property}._errors.{index})', () => {
|
||||
const error = new DiscordAPIError(
|
||||
{
|
||||
code: 50035,
|
||||
errors: {
|
||||
embed: {
|
||||
fields: { '0': { value: { _errors: [{ code: 'BASE_TYPE_REQUIRED', message: 'This field is required' }] } } },
|
||||
},
|
||||
},
|
||||
message: 'Invalid Form Body',
|
||||
},
|
||||
50035,
|
||||
400,
|
||||
'POST',
|
||||
'https://discord.com/api/v9/channels/:id',
|
||||
{},
|
||||
);
|
||||
|
||||
expect(error.code).toBe(50035);
|
||||
expect(error.message).toBe(
|
||||
['Invalid Form Body', 'embed.fields[0].value[BASE_TYPE_REQUIRED]: This field is required'].join('\n'),
|
||||
);
|
||||
expect(error.method).toBe('POST');
|
||||
expect(error.name).toBe('DiscordAPIError[50035]');
|
||||
expect(error.status).toBe(400);
|
||||
expect(error.url).toBe('https://discord.com/api/v9/channels/:id');
|
||||
});
|
||||
|
||||
test('Invalid FormFields Error (error.errors.{property}.{property}._errors.{index}._errors)', () => {
|
||||
const error = new DiscordAPIError(
|
||||
{
|
||||
code: 50035,
|
||||
errors: {
|
||||
form_fields: {
|
||||
label: { _errors: [{ _errors: [{ code: 'BASE_TYPE_REQUIRED', message: 'This field is required' }] }] },
|
||||
},
|
||||
},
|
||||
message: 'Invalid Form Body',
|
||||
},
|
||||
50035,
|
||||
400,
|
||||
'PATCH',
|
||||
'https://discord.com/api/v9/guilds/:id',
|
||||
{},
|
||||
);
|
||||
|
||||
expect(error.code).toBe(50035);
|
||||
expect(error.message).toBe(
|
||||
['Invalid Form Body', 'form_fields.label[0][BASE_TYPE_REQUIRED]: This field is required'].join('\n'),
|
||||
);
|
||||
expect(error.method).toBe('PATCH');
|
||||
expect(error.name).toBe('DiscordAPIError[50035]');
|
||||
expect(error.status).toBe(400);
|
||||
expect(error.url).toBe('https://discord.com/api/v9/guilds/:id');
|
||||
});
|
||||
|
||||
test('Invalid Oauth Code Error (error.error)', () => {
|
||||
const error = new DiscordAPIError(
|
||||
{
|
||||
error: 'invalid_request',
|
||||
error_description: 'Invalid "code" in request.',
|
||||
},
|
||||
'invalid_request',
|
||||
400,
|
||||
'POST',
|
||||
'https://discord.com/api/v9/oauth2/token',
|
||||
{
|
||||
body: new URLSearchParams([
|
||||
['client_id', '1234567890123545678'],
|
||||
['client_secret', 'totally-valid-secret'],
|
||||
['redirect_uri', 'http://localhost'],
|
||||
['grant_type', 'authorization_code'],
|
||||
['code', 'very-invalid-code'],
|
||||
]),
|
||||
},
|
||||
);
|
||||
|
||||
expect(error.code).toBe('invalid_request');
|
||||
expect(error.message).toBe('Invalid "code" in request.');
|
||||
expect(error.method).toBe('POST');
|
||||
expect(error.name).toBe('DiscordAPIError[invalid_request]');
|
||||
expect(error.status).toBe(400);
|
||||
expect(error.url).toBe('https://discord.com/api/v9/oauth2/token');
|
||||
});
|
||||
267
packages/rest/__tests__/REST.test.ts
Normal file
267
packages/rest/__tests__/REST.test.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import nock from 'nock';
|
||||
import { DiscordSnowflake } from '@sapphire/snowflake';
|
||||
import { REST, DefaultRestOptions, APIRequest } from '../src';
|
||||
import { Routes, Snowflake } from 'discord-api-types/v9';
|
||||
import { Response } from 'node-fetch';
|
||||
|
||||
const newSnowflake: Snowflake = DiscordSnowflake.generate().toString();
|
||||
|
||||
const api = new REST().setToken('A-Very-Fake-Token');
|
||||
|
||||
nock(`${DefaultRestOptions.api}/v${DefaultRestOptions.version}`)
|
||||
.get('/simpleGet')
|
||||
.reply(200, { test: true })
|
||||
.delete('/simpleDelete')
|
||||
.reply(200, { test: true })
|
||||
.patch('/simplePatch')
|
||||
.reply(200, { test: true })
|
||||
.put('/simplePut')
|
||||
.reply(200, { test: true })
|
||||
.post('/simplePost')
|
||||
.reply(200, { test: true })
|
||||
.get('/getQuery')
|
||||
.query({ foo: 'bar', hello: 'world' })
|
||||
.reply(200, { test: true })
|
||||
.get('/getAuth')
|
||||
.times(3)
|
||||
.reply(200, function handler() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
return { auth: this.req.headers.authorization?.[0] ?? null };
|
||||
})
|
||||
.get('/getReason')
|
||||
.times(3)
|
||||
.reply(200, function handler() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
return { reason: this.req.headers['x-audit-log-reason']?.[0] ?? null };
|
||||
})
|
||||
.post('/urlEncoded')
|
||||
.reply(200, (_, body) => body)
|
||||
.post('/postEcho')
|
||||
.reply(200, (_, body) => body)
|
||||
.post('/postAttachment')
|
||||
.times(5)
|
||||
.reply(200, (_, body) => ({
|
||||
body: body
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/-+\d+-*\n?/g, '')
|
||||
.trim(),
|
||||
}))
|
||||
.delete('/channels/339942739275677727/messages/392063687801700356')
|
||||
.reply(200, { test: true })
|
||||
.delete(`/channels/339942739275677727/messages/${newSnowflake}`)
|
||||
.reply(200, { test: true })
|
||||
.get('/request')
|
||||
.times(2)
|
||||
.reply(200, { test: true });
|
||||
|
||||
test('simple GET', async () => {
|
||||
expect(await api.get('/simpleGet')).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('simple DELETE', async () => {
|
||||
expect(await api.delete('/simpleDelete')).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('simple PATCH', async () => {
|
||||
expect(await api.patch('/simplePatch')).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('simple PUT', async () => {
|
||||
expect(await api.put('/simplePut')).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('simple POST', async () => {
|
||||
expect(await api.post('/simplePost')).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('getQuery', async () => {
|
||||
expect(
|
||||
await api.get('/getQuery', {
|
||||
query: new URLSearchParams([
|
||||
['foo', 'bar'],
|
||||
['hello', 'world'],
|
||||
]),
|
||||
}),
|
||||
).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('getAuth default', async () => {
|
||||
expect(await api.get('/getAuth')).toStrictEqual({ auth: 'Bot A-Very-Fake-Token' });
|
||||
});
|
||||
|
||||
test('getAuth unauthorized', async () => {
|
||||
expect(await api.get('/getAuth', { auth: false })).toStrictEqual({ auth: null });
|
||||
});
|
||||
|
||||
test('getAuth authorized', async () => {
|
||||
expect(await api.get('/getAuth', { auth: true })).toStrictEqual({ auth: 'Bot A-Very-Fake-Token' });
|
||||
});
|
||||
|
||||
test('getReason default', async () => {
|
||||
expect(await api.get('/getReason')).toStrictEqual({ reason: null });
|
||||
});
|
||||
|
||||
test('getReason plain text', async () => {
|
||||
expect(await api.get('/getReason', { reason: 'Hello' })).toStrictEqual({ reason: 'Hello' });
|
||||
});
|
||||
|
||||
test('getReason encoded', async () => {
|
||||
expect(await api.get('/getReason', { reason: '😄' })).toStrictEqual({ reason: '%F0%9F%98%84' });
|
||||
});
|
||||
|
||||
test('postAttachment empty', async () => {
|
||||
expect(await api.post('/postAttachment', { attachments: [] })).toStrictEqual({
|
||||
body: '',
|
||||
});
|
||||
});
|
||||
|
||||
test('postAttachment attachment', async () => {
|
||||
expect(
|
||||
await api.post('/postAttachment', {
|
||||
attachments: [{ fileName: 'out.txt', rawBuffer: Buffer.from('Hello') }],
|
||||
}),
|
||||
).toStrictEqual({
|
||||
body: [
|
||||
'Content-Disposition: form-data; name="files[0]"; filename="out.txt"',
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'Hello',
|
||||
].join('\n'),
|
||||
});
|
||||
});
|
||||
|
||||
test('postAttachment attachment and JSON', async () => {
|
||||
expect(
|
||||
await api.post('/postAttachment', {
|
||||
attachments: [{ fileName: 'out.txt', rawBuffer: Buffer.from('Hello') }],
|
||||
body: { foo: 'bar' },
|
||||
}),
|
||||
).toStrictEqual({
|
||||
body: [
|
||||
'Content-Disposition: form-data; name="files[0]"; filename="out.txt"',
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'Hello',
|
||||
'Content-Disposition: form-data; name="payload_json"',
|
||||
'',
|
||||
'{"foo":"bar"}',
|
||||
].join('\n'),
|
||||
});
|
||||
});
|
||||
|
||||
test('postAttachment attachments and JSON', async () => {
|
||||
expect(
|
||||
await api.post('/postAttachment', {
|
||||
attachments: [
|
||||
{ fileName: 'out.txt', rawBuffer: Buffer.from('Hello') },
|
||||
{ fileName: 'out.txt', rawBuffer: Buffer.from('Hi') },
|
||||
],
|
||||
body: { attachments: [{ id: 0, description: 'test' }] },
|
||||
}),
|
||||
).toStrictEqual({
|
||||
body: [
|
||||
'Content-Disposition: form-data; name="files[0]"; filename="out.txt"',
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'Hello',
|
||||
'Content-Disposition: form-data; name="files[1]"; filename="out.txt"',
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'Hi',
|
||||
'Content-Disposition: form-data; name="payload_json"',
|
||||
'',
|
||||
'{"attachments":[{"id":0,"description":"test"}]}',
|
||||
].join('\n'),
|
||||
});
|
||||
});
|
||||
|
||||
test('postAttachment sticker and JSON', async () => {
|
||||
expect(
|
||||
await api.post('/postAttachment', {
|
||||
attachments: [{ key: 'file', fileName: 'sticker.png', rawBuffer: Buffer.from('Sticker') }],
|
||||
body: { foo: 'bar' },
|
||||
appendToFormData: true,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
body: [
|
||||
'Content-Disposition: form-data; name="file"; filename="sticker.png"',
|
||||
'Content-Type: image/png',
|
||||
'',
|
||||
'Sticker',
|
||||
'Content-Disposition: form-data; name="foo"',
|
||||
'',
|
||||
'bar',
|
||||
].join('\n'),
|
||||
});
|
||||
});
|
||||
|
||||
test('urlEncoded', async () => {
|
||||
const body = new URLSearchParams([
|
||||
['client_id', '1234567890123545678'],
|
||||
['client_secret', 'totally-valid-secret'],
|
||||
['redirect_uri', 'http://localhost'],
|
||||
['grant_type', 'authorization_code'],
|
||||
['code', 'very-invalid-code'],
|
||||
]);
|
||||
expect(
|
||||
await api.post('/urlEncoded', {
|
||||
body,
|
||||
passThroughBody: true,
|
||||
auth: false,
|
||||
}),
|
||||
).toStrictEqual(Buffer.from(body.toString()));
|
||||
});
|
||||
|
||||
test('postEcho', async () => {
|
||||
expect(await api.post('/postEcho', { body: { foo: 'bar' } })).toStrictEqual({ foo: 'bar' });
|
||||
});
|
||||
|
||||
test('Old Message Delete Edge-Case: Old message', async () => {
|
||||
expect(await api.delete(Routes.channelMessage('339942739275677727', '392063687801700356'))).toStrictEqual({
|
||||
test: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Old Message Delete Edge-Case: New message', async () => {
|
||||
expect(await api.delete(Routes.channelMessage('339942739275677727', newSnowflake))).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('Request and Response Events', async () => {
|
||||
const requestListener = jest.fn();
|
||||
const responseListener = jest.fn();
|
||||
|
||||
api.on('request', requestListener);
|
||||
api.on('response', responseListener);
|
||||
|
||||
await api.get('/request');
|
||||
|
||||
expect(requestListener).toHaveBeenCalledTimes(1);
|
||||
expect(responseListener).toHaveBeenCalledTimes(1);
|
||||
expect(requestListener).toHaveBeenLastCalledWith<[APIRequest]>(
|
||||
expect.objectContaining({
|
||||
method: 'get',
|
||||
path: '/request',
|
||||
route: '/request',
|
||||
data: { attachments: undefined, body: undefined },
|
||||
retries: 0,
|
||||
}) as APIRequest,
|
||||
);
|
||||
expect(responseListener).toHaveBeenLastCalledWith<[APIRequest, Response]>(
|
||||
expect.objectContaining({
|
||||
method: 'get',
|
||||
path: '/request',
|
||||
route: '/request',
|
||||
data: { attachments: undefined, body: undefined },
|
||||
retries: 0,
|
||||
}) as APIRequest,
|
||||
expect.objectContaining({ status: 200, statusText: 'OK' }) as Response,
|
||||
);
|
||||
|
||||
api.off('request', requestListener);
|
||||
api.off('response', responseListener);
|
||||
|
||||
await api.get('/request');
|
||||
|
||||
expect(requestListener).toHaveBeenCalledTimes(1);
|
||||
expect(responseListener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
366
packages/rest/__tests__/RequestHandler.test.ts
Normal file
366
packages/rest/__tests__/RequestHandler.test.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
import nock from 'nock';
|
||||
import { DefaultRestOptions, DiscordAPIError, HTTPError, RateLimitError, REST, RESTEvents } from '../src';
|
||||
|
||||
const api = new REST({ timeout: 2000, offset: 5 }).setToken('A-Very-Fake-Token');
|
||||
const invalidAuthApi = new REST({ timeout: 2000 }).setToken('Definitely-Not-A-Fake-Token');
|
||||
const rateLimitErrorApi = new REST({ rejectOnRateLimit: ['/channels'] }).setToken('Obviously-Not-A-Fake-Token');
|
||||
|
||||
let resetAfter = 0;
|
||||
let sublimitResetAfter = 0;
|
||||
let retryAfter = 0;
|
||||
let sublimitRequests = 0;
|
||||
let sublimitHits = 0;
|
||||
let serverOutage = true;
|
||||
let unexpected429 = true;
|
||||
let unexpected429cf = true;
|
||||
const sublimitIntervals = {
|
||||
reset: null,
|
||||
retry: null,
|
||||
};
|
||||
|
||||
const sublimit = { body: { name: 'newname' } };
|
||||
const noSublimit = { body: { bitrate: 40000 } };
|
||||
|
||||
nock(`${DefaultRestOptions.api}/v${DefaultRestOptions.version}`)
|
||||
.persist()
|
||||
.replyDate()
|
||||
.get('/standard')
|
||||
.times(3)
|
||||
.reply(function handler(): nock.ReplyFnResult {
|
||||
const response = Date.now() >= resetAfter ? 204 : 429;
|
||||
resetAfter = Date.now() + 250;
|
||||
if (response === 204) {
|
||||
return [
|
||||
204,
|
||||
undefined,
|
||||
{
|
||||
'x-ratelimit-limit': '1',
|
||||
'x-ratelimit-remaining': '0',
|
||||
'x-ratelimit-reset-after': ((resetAfter - Date.now()) / 1000).toString(),
|
||||
'x-ratelimit-bucket': '80c17d2f203122d936070c88c8d10f33',
|
||||
via: '1.1 google',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
429,
|
||||
{
|
||||
limit: '1',
|
||||
remaining: '0',
|
||||
resetAfter: (resetAfter / 1000).toString(),
|
||||
bucket: '80c17d2f203122d936070c88c8d10f33',
|
||||
retryAfter: (resetAfter - Date.now()).toString(),
|
||||
},
|
||||
{
|
||||
'x-ratelimit-limit': '1',
|
||||
'x-ratelimit-remaining': '0',
|
||||
'x-ratelimit-reset-after': ((resetAfter - Date.now()) / 1000).toString(),
|
||||
'x-ratelimit-bucket': '80c17d2f203122d936070c88c8d10f33',
|
||||
'retry-after': (resetAfter - Date.now()).toString(),
|
||||
via: '1.1 google',
|
||||
},
|
||||
];
|
||||
})
|
||||
.get('/triggerGlobal')
|
||||
.reply(function handler(): nock.ReplyFnResult {
|
||||
return [
|
||||
204,
|
||||
{ global: true },
|
||||
{
|
||||
'x-ratelimit-global': 'true',
|
||||
'retry-after': '1',
|
||||
via: '1.1 google',
|
||||
},
|
||||
];
|
||||
})
|
||||
.get('/regularRequest')
|
||||
.reply(204, { test: true })
|
||||
.patch('/channels/:id', (body) => ['name', 'topic'].some((key) => Reflect.has(body as Record<string, unknown>, key)))
|
||||
.reply(function handler(): nock.ReplyFnResult {
|
||||
sublimitHits += 1;
|
||||
sublimitRequests += 1;
|
||||
const response = 2 - sublimitHits >= 0 && 10 - sublimitRequests >= 0 ? 204 : 429;
|
||||
startSublimitIntervals();
|
||||
if (response === 204) {
|
||||
return [
|
||||
204,
|
||||
undefined,
|
||||
{
|
||||
'x-ratelimit-limit': '10',
|
||||
'x-ratelimit-remaining': `${10 - sublimitRequests}`,
|
||||
'x-ratelimit-reset-after': ((sublimitResetAfter - Date.now()) / 1000).toString(),
|
||||
via: '1.1 google',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
429,
|
||||
{
|
||||
limit: '10',
|
||||
remaining: `${10 - sublimitRequests}`,
|
||||
resetAfter: (sublimitResetAfter / 1000).toString(),
|
||||
retryAfter: ((retryAfter - Date.now()) / 1000).toString(),
|
||||
},
|
||||
{
|
||||
'x-ratelimit-limit': '10',
|
||||
'x-ratelimit-remaining': `${10 - sublimitRequests}`,
|
||||
'x-ratelimit-reset-after': ((sublimitResetAfter - Date.now()) / 1000).toString(),
|
||||
'retry-after': ((retryAfter - Date.now()) / 1000).toString(),
|
||||
via: '1.1 google',
|
||||
},
|
||||
];
|
||||
})
|
||||
.patch('/channels/:id', (body) =>
|
||||
['name', 'topic'].every((key) => !Reflect.has(body as Record<string, unknown>, key)),
|
||||
)
|
||||
.reply(function handler(): nock.ReplyFnResult {
|
||||
sublimitRequests += 1;
|
||||
const response = 10 - sublimitRequests >= 0 ? 204 : 429;
|
||||
startSublimitIntervals();
|
||||
if (response === 204) {
|
||||
return [
|
||||
204,
|
||||
undefined,
|
||||
{
|
||||
'x-ratelimit-limit': '10',
|
||||
'x-ratelimit-remaining': `${10 - sublimitRequests}`,
|
||||
'x-ratelimit-reset-after': ((sublimitResetAfter - Date.now()) / 1000).toString(),
|
||||
via: '1.1 google',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
429,
|
||||
{
|
||||
limit: '10',
|
||||
remaining: `${10 - sublimitRequests}`,
|
||||
resetAfter: (sublimitResetAfter / 1000).toString(),
|
||||
retryAfter: ((sublimitResetAfter - Date.now()) / 1000).toString(),
|
||||
},
|
||||
{
|
||||
'x-ratelimit-limit': '10',
|
||||
'x-ratelimit-remaining': `${10 - sublimitRequests}`,
|
||||
'x-ratelimit-reset-after': ((sublimitResetAfter - Date.now()) / 1000).toString(),
|
||||
'retry-after': ((sublimitResetAfter - Date.now()) / 1000).toString(),
|
||||
via: '1.1 google',
|
||||
},
|
||||
];
|
||||
})
|
||||
.get('/unexpected')
|
||||
.times(2)
|
||||
.reply(function handler(): nock.ReplyFnResult {
|
||||
if (unexpected429) {
|
||||
unexpected429 = false;
|
||||
return [
|
||||
429,
|
||||
undefined,
|
||||
{
|
||||
'retry-after': '1',
|
||||
via: '1.1 google',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [204, { test: true }];
|
||||
})
|
||||
.get('/unexpected-cf')
|
||||
.times(2)
|
||||
.reply(function handler(): nock.ReplyFnResult {
|
||||
if (unexpected429cf) {
|
||||
unexpected429cf = false;
|
||||
return [
|
||||
429,
|
||||
undefined,
|
||||
{
|
||||
'retry-after': '1',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [204, { test: true }];
|
||||
})
|
||||
.get('/temp')
|
||||
.times(2)
|
||||
.reply(function handler(): nock.ReplyFnResult {
|
||||
if (serverOutage) {
|
||||
serverOutage = false;
|
||||
return [500];
|
||||
}
|
||||
return [204, { test: true }];
|
||||
})
|
||||
.get('/outage')
|
||||
.times(2)
|
||||
.reply(500)
|
||||
.get('/slow')
|
||||
.times(2)
|
||||
.delay(3000)
|
||||
.reply(200)
|
||||
.get('/badRequest')
|
||||
.reply(403, { message: 'Missing Permissions', code: 50013 })
|
||||
.get('/unauthorized')
|
||||
.reply(401, { message: '401: Unauthorized', code: 0 })
|
||||
.get('/malformedRequest')
|
||||
.reply(601);
|
||||
|
||||
// This is tested first to ensure the count remains accurate
|
||||
test('Significant Invalid Requests', async () => {
|
||||
const invalidListener = jest.fn();
|
||||
const invalidListener2 = jest.fn();
|
||||
api.on(RESTEvents.InvalidRequestWarning, invalidListener);
|
||||
// Ensure listeners on REST do not get double added
|
||||
api.on(RESTEvents.InvalidRequestWarning, invalidListener2);
|
||||
api.off(RESTEvents.InvalidRequestWarning, invalidListener2);
|
||||
const [a, b, c, d, e] = [
|
||||
api.get('/badRequest'),
|
||||
api.get('/badRequest'),
|
||||
api.get('/badRequest'),
|
||||
api.get('/badRequest'),
|
||||
api.get('/badRequest'),
|
||||
];
|
||||
await expect(a).rejects.toThrowError('Missing Permissions');
|
||||
await expect(b).rejects.toThrowError('Missing Permissions');
|
||||
await expect(c).rejects.toThrowError('Missing Permissions');
|
||||
await expect(d).rejects.toThrowError('Missing Permissions');
|
||||
await expect(e).rejects.toThrowError('Missing Permissions');
|
||||
expect(invalidListener).toHaveBeenCalledTimes(0);
|
||||
api.requestManager.options.invalidRequestWarningInterval = 2;
|
||||
const [f, g, h, i, j] = [
|
||||
api.get('/badRequest'),
|
||||
api.get('/badRequest'),
|
||||
api.get('/badRequest'),
|
||||
api.get('/badRequest'),
|
||||
api.get('/badRequest'),
|
||||
];
|
||||
await expect(f).rejects.toThrowError('Missing Permissions');
|
||||
await expect(g).rejects.toThrowError('Missing Permissions');
|
||||
await expect(h).rejects.toThrowError('Missing Permissions');
|
||||
await expect(i).rejects.toThrowError('Missing Permissions');
|
||||
await expect(j).rejects.toThrowError('Missing Permissions');
|
||||
expect(invalidListener).toHaveBeenCalledTimes(3);
|
||||
api.off(RESTEvents.InvalidRequestWarning, invalidListener);
|
||||
});
|
||||
|
||||
test('Handle standard rate limits', async () => {
|
||||
const [a, b, c] = [api.get('/standard'), api.get('/standard'), api.get('/standard')];
|
||||
|
||||
expect(await a).toStrictEqual(Buffer.alloc(0));
|
||||
const previous1 = Date.now();
|
||||
expect(await b).toStrictEqual(Buffer.alloc(0));
|
||||
const previous2 = Date.now();
|
||||
expect(await c).toStrictEqual(Buffer.alloc(0));
|
||||
const now = Date.now();
|
||||
expect(previous2).toBeGreaterThanOrEqual(previous1 + 250);
|
||||
expect(now).toBeGreaterThanOrEqual(previous2 + 250);
|
||||
});
|
||||
|
||||
test('Handle global rate limits', async () => {
|
||||
const earlier = Date.now();
|
||||
expect(await api.get('/triggerGlobal')).toStrictEqual({ global: true });
|
||||
expect(await api.get('/regularRequest')).toStrictEqual({ test: true });
|
||||
expect(Date.now()).toBeGreaterThanOrEqual(earlier + 100);
|
||||
});
|
||||
|
||||
test('Handle sublimits', async () => {
|
||||
// Return the current time on these results as their response does not indicate anything
|
||||
// Queue all requests, don't wait, to allow retroactive check
|
||||
const [aP, bP, cP, dP, eP] = [
|
||||
api.patch('/channels/:id', sublimit).then(() => Date.now()),
|
||||
api.patch('/channels/:id', sublimit).then(() => Date.now()),
|
||||
api.patch('/channels/:id', sublimit).then(() => Date.now()), // Limit hits
|
||||
api.patch('/channels/:id', noSublimit).then(() => Date.now()), // Ensure normal request passes
|
||||
api.patch('/channels/:id', sublimit).then(() => Date.now()), // For retroactive check
|
||||
];
|
||||
|
||||
const [a, b, c, d] = await Promise.all([aP, bP, cP, dP]);
|
||||
|
||||
const [f, g] = await Promise.all([
|
||||
api.patch('/channels/:id', sublimit).then(() => Date.now()),
|
||||
api.patch('/channels/:id', noSublimit).then(() => Date.now()),
|
||||
]); // For additional sublimited checks
|
||||
const e = await eP;
|
||||
|
||||
expect(a).toBeLessThan(b);
|
||||
expect(b).toBeLessThan(c);
|
||||
expect(d).toBeLessThan(c);
|
||||
expect(c).toBeLessThan(e);
|
||||
expect(d).toBeLessThan(e);
|
||||
expect(e).toBeLessThan(f);
|
||||
expect(e).toBeLessThan(g);
|
||||
expect(g).toBeLessThan(f);
|
||||
|
||||
clearInterval(sublimitIntervals.reset);
|
||||
clearInterval(sublimitIntervals.retry);
|
||||
});
|
||||
|
||||
test('Handle unexpected 429', async () => {
|
||||
const previous = Date.now();
|
||||
expect(await api.get('/unexpected')).toStrictEqual({ test: true });
|
||||
expect(Date.now()).toBeGreaterThanOrEqual(previous + 1000);
|
||||
});
|
||||
|
||||
test('Handle unexpected 429 cloudflare', async () => {
|
||||
const previous = Date.now();
|
||||
expect(await api.get('/unexpected-cf')).toStrictEqual({ test: true });
|
||||
expect(Date.now()).toBeGreaterThanOrEqual(previous + 1000);
|
||||
});
|
||||
|
||||
test('Handle temp server outage', async () => {
|
||||
expect(await api.get('/temp')).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('perm server outage', async () => {
|
||||
const promise = api.get('/outage');
|
||||
await expect(promise).rejects.toThrowError();
|
||||
await expect(promise).rejects.toBeInstanceOf(HTTPError);
|
||||
});
|
||||
|
||||
test('server responding too slow', async () => {
|
||||
const promise = api.get('/slow');
|
||||
await expect(promise).rejects.toThrowError('The user aborted a request.');
|
||||
}, 10000);
|
||||
|
||||
test('Bad Request', async () => {
|
||||
const promise = api.get('/badRequest');
|
||||
await expect(promise).rejects.toThrowError('Missing Permissions');
|
||||
await expect(promise).rejects.toBeInstanceOf(DiscordAPIError);
|
||||
});
|
||||
|
||||
test('Unauthorized', async () => {
|
||||
const promise = invalidAuthApi.get('/unauthorized');
|
||||
await expect(promise).rejects.toThrowError('401: Unauthorized');
|
||||
await expect(promise).rejects.toBeInstanceOf(DiscordAPIError);
|
||||
});
|
||||
|
||||
test('Reject on RateLimit', async () => {
|
||||
const [aP, bP, cP] = [
|
||||
rateLimitErrorApi.patch('/channels/:id', sublimit),
|
||||
rateLimitErrorApi.patch('/channels/:id', sublimit),
|
||||
rateLimitErrorApi.patch('/channels/:id', sublimit),
|
||||
];
|
||||
await expect(aP).resolves;
|
||||
await expect(bP).rejects.toThrowError();
|
||||
await expect(bP).rejects.toBeInstanceOf(RateLimitError);
|
||||
await expect(cP).rejects.toThrowError();
|
||||
await expect(cP).rejects.toBeInstanceOf(RateLimitError);
|
||||
});
|
||||
|
||||
test('malformedRequest', async () => {
|
||||
expect(await api.get('/malformedRequest')).toBe(null);
|
||||
});
|
||||
|
||||
function startSublimitIntervals() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!sublimitIntervals.reset) {
|
||||
sublimitResetAfter = Date.now() + 250;
|
||||
sublimitIntervals.reset = setInterval(() => {
|
||||
sublimitRequests = 0;
|
||||
sublimitResetAfter = Date.now() + 250;
|
||||
}, 250);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!sublimitIntervals.retry) {
|
||||
retryAfter = Date.now() + 1000;
|
||||
sublimitIntervals.retry = setInterval(() => {
|
||||
sublimitHits = 0;
|
||||
retryAfter = Date.now() + 1000;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
18
packages/rest/__tests__/RequestManager.test.ts
Normal file
18
packages/rest/__tests__/RequestManager.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import nock from 'nock';
|
||||
import { DefaultRestOptions, REST } from '../src';
|
||||
|
||||
const api = new REST();
|
||||
|
||||
nock(`${DefaultRestOptions.api}/v${DefaultRestOptions.version}`).get('/simpleGet').reply(200, { test: true });
|
||||
|
||||
test('no token', async () => {
|
||||
const promise = api.get('/simpleGet');
|
||||
await expect(promise).rejects.toThrowError('Expected token to be set for this request, but none was present');
|
||||
await expect(promise).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
test('negative offset', () => {
|
||||
const badREST = new REST({ offset: -5000 });
|
||||
|
||||
expect(badREST.requestManager.options.offset).toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user