mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-17 20:13:30 +01:00
feat(rest): use undici (#7747)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com> Co-authored-by: ckohen <chaikohen@gmail.com>
This commit is contained in:
@@ -1,202 +1,224 @@
|
||||
import { DiscordSnowflake } from '@sapphire/snowflake';
|
||||
import { Routes, Snowflake } from 'discord-api-types/v10';
|
||||
import nock from 'nock';
|
||||
import { Response } from 'node-fetch';
|
||||
import { REST, DefaultRestOptions, APIRequest } from '../src';
|
||||
import { File, FormData, MockAgent, setGlobalDispatcher } from 'undici';
|
||||
import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor';
|
||||
import { genPath } from './util';
|
||||
import { REST } from '../src';
|
||||
|
||||
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('/postFile')
|
||||
.times(5)
|
||||
.reply(200, (_, body) => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
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 });
|
||||
// @discordjs/rest uses the `content-type` header to detect whether to parse
|
||||
// the response as JSON or as an ArrayBuffer.
|
||||
const responseOptions: MockInterceptor.MockResponseOptions = {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
let mockAgent: MockAgent;
|
||||
let mockPool: Interceptable;
|
||||
|
||||
beforeEach(() => {
|
||||
mockAgent = new MockAgent();
|
||||
mockAgent.disableNetConnect(); // prevent actual requests to Discord
|
||||
setGlobalDispatcher(mockAgent); // enabled the mock client to intercept requests
|
||||
|
||||
mockPool = mockAgent.get('https://discord.com');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await mockAgent.close();
|
||||
});
|
||||
|
||||
test('simple GET', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/simpleGet'),
|
||||
method: 'GET',
|
||||
})
|
||||
.reply(() => ({
|
||||
data: { test: true },
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}));
|
||||
|
||||
expect(await api.get('/simpleGet')).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('simple DELETE', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/simpleDelete'),
|
||||
method: 'DELETE',
|
||||
})
|
||||
.reply(() => ({
|
||||
data: { test: true },
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}));
|
||||
|
||||
expect(await api.delete('/simpleDelete')).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('simple PATCH', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/simplePatch'),
|
||||
method: 'PATCH',
|
||||
})
|
||||
.reply(() => ({
|
||||
data: { test: true },
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}));
|
||||
|
||||
expect(await api.patch('/simplePatch')).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('simple PUT', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/simplePut'),
|
||||
method: 'PUT',
|
||||
})
|
||||
.reply(() => ({
|
||||
data: { test: true },
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}));
|
||||
|
||||
expect(await api.put('/simplePut')).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('simple POST', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/simplePost'),
|
||||
method: 'POST',
|
||||
})
|
||||
.reply(() => ({
|
||||
data: { test: true },
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}));
|
||||
|
||||
expect(await api.post('/simplePost')).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('simple PUT', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/simplePut'),
|
||||
method: 'PUT',
|
||||
})
|
||||
.reply(() => ({
|
||||
data: { test: true },
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}));
|
||||
|
||||
expect(await api.put('/simplePut')).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('getQuery', async () => {
|
||||
const query = new URLSearchParams([
|
||||
['foo', 'bar'],
|
||||
['hello', 'world'],
|
||||
]);
|
||||
|
||||
mockPool
|
||||
.intercept({
|
||||
path: `${genPath('/getQuery')}?${query.toString()}`,
|
||||
method: 'GET',
|
||||
})
|
||||
.reply(() => ({
|
||||
data: { test: true },
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}));
|
||||
|
||||
expect(
|
||||
await api.get('/getQuery', {
|
||||
query: new URLSearchParams([
|
||||
['foo', 'bar'],
|
||||
['hello', 'world'],
|
||||
]),
|
||||
query: query,
|
||||
}),
|
||||
).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('getAuth default', async () => {
|
||||
test('getAuth', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/getAuth'),
|
||||
method: 'GET',
|
||||
})
|
||||
.reply((t) => ({
|
||||
data: { auth: (t.headers as unknown as Record<string, string | undefined>)['Authorization'] ?? null },
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}))
|
||||
.times(3);
|
||||
|
||||
// default
|
||||
expect(await api.get('/getAuth')).toStrictEqual({ auth: 'Bot A-Very-Fake-Token' });
|
||||
|
||||
// unauthorized
|
||||
expect(
|
||||
await api.get('/getAuth', {
|
||||
auth: false,
|
||||
}),
|
||||
).toStrictEqual({ auth: null });
|
||||
|
||||
// authorized
|
||||
expect(
|
||||
await api.get('/getAuth', {
|
||||
auth: true,
|
||||
}),
|
||||
).toStrictEqual({ auth: 'Bot A-Very-Fake-Token' });
|
||||
});
|
||||
|
||||
test('getAuth unauthorized', async () => {
|
||||
expect(await api.get('/getAuth', { auth: false })).toStrictEqual({ auth: null });
|
||||
});
|
||||
test('getReason', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/getReason'),
|
||||
method: 'GET',
|
||||
})
|
||||
.reply((t) => ({
|
||||
data: { reason: (t.headers as unknown as Record<string, string | undefined>)['X-Audit-Log-Reason'] ?? null },
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}))
|
||||
.times(3);
|
||||
|
||||
test('getAuth authorized', async () => {
|
||||
expect(await api.get('/getAuth', { auth: true })).toStrictEqual({ auth: 'Bot A-Very-Fake-Token' });
|
||||
});
|
||||
|
||||
test('getReason default', async () => {
|
||||
// default
|
||||
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('postFile empty', async () => {
|
||||
expect(await api.post('/postFile', { files: [] })).toStrictEqual({
|
||||
body: '',
|
||||
});
|
||||
});
|
||||
|
||||
test('postFile file (string)', async () => {
|
||||
// plain text
|
||||
expect(
|
||||
await api.post('/postFile', {
|
||||
files: [{ name: 'out.txt', data: 'Hello' }],
|
||||
await api.get('/getReason', {
|
||||
reason: 'Hello',
|
||||
}),
|
||||
).toStrictEqual({
|
||||
body: [
|
||||
'Content-Disposition: form-data; name="files[0]"; filename="out.txt"',
|
||||
'Content-Type: text/plain',
|
||||
'',
|
||||
'Hello',
|
||||
].join('\n'),
|
||||
});
|
||||
});
|
||||
).toStrictEqual({ reason: 'Hello' });
|
||||
|
||||
test('postFile file and JSON', async () => {
|
||||
// encoded
|
||||
expect(
|
||||
await api.post('/postFile', {
|
||||
files: [{ name: 'out.txt', data: Buffer.from('Hello') }],
|
||||
body: { foo: 'bar' },
|
||||
await api.get('/getReason', {
|
||||
reason: '😄',
|
||||
}),
|
||||
).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('postFile files and JSON', async () => {
|
||||
expect(
|
||||
await api.post('/postFile', {
|
||||
files: [
|
||||
{ name: 'out.txt', data: Buffer.from('Hello') },
|
||||
{ name: 'out.txt', data: Buffer.from('Hi') },
|
||||
],
|
||||
body: { files: [{ 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"',
|
||||
'',
|
||||
'{"files":[{"id":0,"description":"test"}]}',
|
||||
].join('\n'),
|
||||
});
|
||||
});
|
||||
|
||||
test('postFile sticker and JSON', async () => {
|
||||
expect(
|
||||
await api.post('/postFile', {
|
||||
files: [{ key: 'file', name: 'sticker.png', data: 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'),
|
||||
});
|
||||
).toStrictEqual({ reason: '%F0%9F%98%84' });
|
||||
});
|
||||
|
||||
test('urlEncoded', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/urlEncoded'),
|
||||
method: 'POST',
|
||||
})
|
||||
.reply((t) => ({
|
||||
data: t.body!,
|
||||
statusCode: 200,
|
||||
}));
|
||||
|
||||
const body = new URLSearchParams([
|
||||
['client_id', '1234567890123545678'],
|
||||
['client_secret', 'totally-valid-secret'],
|
||||
@@ -204,6 +226,7 @@ test('urlEncoded', async () => {
|
||||
['grant_type', 'authorization_code'],
|
||||
['code', 'very-invalid-code'],
|
||||
]);
|
||||
|
||||
expect(
|
||||
new Uint8Array(
|
||||
(await api.post('/urlEncoded', {
|
||||
@@ -216,55 +239,156 @@ test('urlEncoded', async () => {
|
||||
});
|
||||
|
||||
test('postEcho', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/postEcho'),
|
||||
method: 'POST',
|
||||
})
|
||||
.reply((t) => ({
|
||||
data: t.body!,
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}));
|
||||
|
||||
expect(await api.post('/postEcho', { body: { foo: 'bar' } })).toStrictEqual({ foo: 'bar' });
|
||||
});
|
||||
|
||||
test('Old Message Delete Edge-Case: Old message', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/channels/339942739275677727/messages/392063687801700356'),
|
||||
method: 'DELETE',
|
||||
})
|
||||
.reply(() => ({
|
||||
data: { test: true },
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}));
|
||||
|
||||
expect(await api.delete(Routes.channelMessage('339942739275677727', '392063687801700356'))).toStrictEqual({
|
||||
test: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Old Message Delete Edge-Case: New message', async () => {
|
||||
test('Old Message Delete Edge-Case: Old message', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath(`/channels/339942739275677727/messages/${newSnowflake}`),
|
||||
method: 'DELETE',
|
||||
})
|
||||
.reply(() => ({
|
||||
data: { test: true },
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}));
|
||||
|
||||
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();
|
||||
test('postFile', async () => {
|
||||
const mockData = {
|
||||
statusCode: 200,
|
||||
data: 'Hello',
|
||||
};
|
||||
|
||||
api.on('request', requestListener);
|
||||
api.on('response', responseListener);
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/postFileEmptyArray'),
|
||||
method: 'POST',
|
||||
})
|
||||
.reply(({ body }) => {
|
||||
expect(body).toBeNull();
|
||||
return mockData;
|
||||
});
|
||||
|
||||
await api.get('/request');
|
||||
// postFile empty
|
||||
await api.post('/postFileEmptyArray', { files: [] });
|
||||
|
||||
expect(requestListener).toHaveBeenCalledTimes(1);
|
||||
expect(responseListener).toHaveBeenCalledTimes(1);
|
||||
expect(requestListener).toHaveBeenLastCalledWith<[APIRequest]>(
|
||||
expect.objectContaining({
|
||||
method: 'get',
|
||||
path: '/request',
|
||||
route: '/request',
|
||||
data: { files: undefined, body: undefined, auth: true },
|
||||
retries: 0,
|
||||
}) as APIRequest,
|
||||
);
|
||||
expect(responseListener).toHaveBeenLastCalledWith<[APIRequest, Response]>(
|
||||
expect.objectContaining({
|
||||
method: 'get',
|
||||
path: '/request',
|
||||
route: '/request',
|
||||
data: { files: undefined, body: undefined, auth: true },
|
||||
retries: 0,
|
||||
}) as APIRequest,
|
||||
expect.objectContaining({ status: 200, statusText: 'OK' }) as Response,
|
||||
);
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/postFileStringData'),
|
||||
method: 'POST',
|
||||
})
|
||||
.reply(({ body }) => {
|
||||
const fd = body as FormData;
|
||||
|
||||
api.off('request', requestListener);
|
||||
api.off('response', responseListener);
|
||||
expect(fd.get('files[0]')).toBeInstanceOf(File);
|
||||
expect(fd.get('files[0]')).toHaveProperty('size', 5); // 'Hello'
|
||||
|
||||
await api.get('/request');
|
||||
return mockData;
|
||||
});
|
||||
|
||||
expect(requestListener).toHaveBeenCalledTimes(1);
|
||||
expect(responseListener).toHaveBeenCalledTimes(1);
|
||||
// postFile file (string)
|
||||
await api.post('/postFileStringData', {
|
||||
files: [{ name: 'out.txt', data: 'Hello' }],
|
||||
});
|
||||
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/postFileBufferWithJson'),
|
||||
method: 'POST',
|
||||
})
|
||||
.reply(({ body }) => {
|
||||
const fd = body as FormData;
|
||||
|
||||
expect(fd.get('files[0]')).toBeInstanceOf(File);
|
||||
expect(fd.get('files[0]')).toHaveProperty('size', 5); // Buffer.from('Hello')
|
||||
expect(fd.get('payload_json')).toStrictEqual(JSON.stringify({ foo: 'bar' }));
|
||||
|
||||
return mockData;
|
||||
});
|
||||
|
||||
// postFile file and JSON
|
||||
await api.post('/postFileBufferWithJson', {
|
||||
files: [{ name: 'out.txt', data: Buffer.from('Hello') }],
|
||||
body: { foo: 'bar' },
|
||||
});
|
||||
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/postFilesAndJson'),
|
||||
method: 'POST',
|
||||
})
|
||||
.reply(({ body }) => {
|
||||
const fd = body as FormData;
|
||||
|
||||
expect(fd.get('files[0]')).toBeInstanceOf(File);
|
||||
expect(fd.get('files[1]')).toBeInstanceOf(File);
|
||||
expect(fd.get('files[0]')).toHaveProperty('size', 5); // Buffer.from('Hello')
|
||||
expect(fd.get('files[1]')).toHaveProperty('size', 2); // Buffer.from('Hi')
|
||||
expect(fd.get('payload_json')).toStrictEqual(JSON.stringify({ files: [{ id: 0, description: 'test' }] }));
|
||||
|
||||
return mockData;
|
||||
});
|
||||
|
||||
// postFile files and JSON
|
||||
await api.post('/postFilesAndJson', {
|
||||
files: [
|
||||
{ name: 'out.txt', data: Buffer.from('Hello') },
|
||||
{ name: 'out.txt', data: Buffer.from('Hi') },
|
||||
],
|
||||
body: { files: [{ id: 0, description: 'test' }] },
|
||||
});
|
||||
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/postFileStickerAndJson'),
|
||||
method: 'POST',
|
||||
})
|
||||
.reply(({ body }) => {
|
||||
const fd = body as FormData;
|
||||
|
||||
expect(fd.get('file')).toBeInstanceOf(File);
|
||||
expect(fd.get('file')).toHaveProperty('size', 7); // Buffer.from('Sticker')
|
||||
expect(fd.get('foo')).toStrictEqual('bar');
|
||||
|
||||
return mockData;
|
||||
});
|
||||
|
||||
// postFile sticker and JSON
|
||||
await api.post('/postFileStickerAndJson', {
|
||||
files: [{ key: 'file', name: 'sticker.png', data: Buffer.from('Sticker') }],
|
||||
body: { foo: 'bar' },
|
||||
appendToFormData: true,
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user