mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-16 03:23:29 +01:00
refactor(rest): switch api to fetch-like and provide strategies (#9416)
BREAKING CHANGE: NodeJS v18+ is required when using node due to the use of global `fetch` BREAKING CHANGE: The raw method of REST now returns a web compatible `Respone` object. BREAKING CHANGE: The `parseResponse` utility method has been updated to operate on a web compatible `Response` object. BREAKING CHANGE: Many underlying internals have changed, some of which were exported. BREAKING CHANGE: `DefaultRestOptions` used to contain a default `agent`, which is now set to `null` instead.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import { MockAgent, setGlobalDispatcher } from 'undici';
|
||||
import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor';
|
||||
import { beforeEach, afterEach, test, expect, vitest } from 'vitest';
|
||||
import { beforeEach, afterEach, test, expect } from 'vitest';
|
||||
import { DiscordAPIError, REST, BurstHandlerMajorIdKey } from '../src/index.js';
|
||||
import { BurstHandler } from '../src/lib/handlers/BurstHandler.js';
|
||||
import { genPath } from './util.js';
|
||||
@@ -46,6 +46,7 @@ test('Interaction callback creates burst handler', async () => {
|
||||
auth: false,
|
||||
body: { type: 4, data: { content: 'Reply' } },
|
||||
}),
|
||||
// TODO: This should be ArrayBuffer, there is a bug in undici request
|
||||
).toBeInstanceOf(Uint8Array);
|
||||
expect(api.requestManager.handlers.get(callbackKey)).toBeInstanceOf(BurstHandler);
|
||||
});
|
||||
|
||||
@@ -3,10 +3,10 @@ import { URLSearchParams } from 'node:url';
|
||||
import { DiscordSnowflake } from '@sapphire/snowflake';
|
||||
import type { Snowflake } from 'discord-api-types/v10';
|
||||
import { Routes } from 'discord-api-types/v10';
|
||||
import type { FormData } from 'undici';
|
||||
import { type FormData, fetch } from 'undici';
|
||||
import { File as UndiciFile, MockAgent, setGlobalDispatcher } from 'undici';
|
||||
import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor.js';
|
||||
import { beforeEach, afterEach, test, expect } from 'vitest';
|
||||
import { beforeEach, afterEach, test, expect, vitest } from 'vitest';
|
||||
import { REST } from '../src/index.js';
|
||||
import { genPath } from './util.js';
|
||||
|
||||
@@ -16,6 +16,10 @@ const newSnowflake: Snowflake = DiscordSnowflake.generate().toString();
|
||||
|
||||
const api = new REST().setToken('A-Very-Fake-Token');
|
||||
|
||||
const makeRequestMock = vitest.fn(fetch);
|
||||
|
||||
const fetchApi = new REST({ makeRequest: makeRequestMock }).setToken('A-Very-Fake-Token');
|
||||
|
||||
// @discordjs/rest uses the `content-type` header to detect whether to parse
|
||||
// the response as JSON or as an ArrayBuffer.
|
||||
const responseOptions: MockInterceptor.MockResponseOptions = {
|
||||
@@ -114,6 +118,22 @@ test('simple POST', async () => {
|
||||
expect(await api.post('/simplePost')).toStrictEqual({ test: true });
|
||||
});
|
||||
|
||||
test('simple POST with fetch', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/fetchSimplePost'),
|
||||
method: 'POST',
|
||||
})
|
||||
.reply(() => ({
|
||||
data: { test: true },
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}));
|
||||
|
||||
expect(await fetchApi.post('/fetchSimplePost')).toStrictEqual({ test: true });
|
||||
expect(makeRequestMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('simple PUT 2', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
@@ -159,11 +179,11 @@ test('getAuth', async () => {
|
||||
path: genPath('/getAuth'),
|
||||
method: 'GET',
|
||||
})
|
||||
.reply((from) => ({
|
||||
data: { auth: (from.headers as unknown as Record<string, string | undefined>).Authorization ?? null },
|
||||
statusCode: 200,
|
||||
.reply(
|
||||
200,
|
||||
(from) => ({ auth: (from.headers as unknown as Record<string, string | undefined>).Authorization ?? null }),
|
||||
responseOptions,
|
||||
}))
|
||||
)
|
||||
.times(3);
|
||||
|
||||
// default
|
||||
@@ -190,11 +210,13 @@ test('getReason', async () => {
|
||||
path: genPath('/getReason'),
|
||||
method: 'GET',
|
||||
})
|
||||
.reply((from) => ({
|
||||
data: { reason: (from.headers as unknown as Record<string, string | undefined>)['X-Audit-Log-Reason'] ?? null },
|
||||
statusCode: 200,
|
||||
.reply(
|
||||
200,
|
||||
(from) => ({
|
||||
reason: (from.headers as unknown as Record<string, string | undefined>)['X-Audit-Log-Reason'] ?? null,
|
||||
}),
|
||||
responseOptions,
|
||||
}))
|
||||
)
|
||||
.times(3);
|
||||
|
||||
// default
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable id-length */
|
||||
/* eslint-disable promise/prefer-await-to-then */
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import { setInterval, clearInterval, setTimeout } from 'node:timers';
|
||||
import { setInterval, clearInterval } from 'node:timers';
|
||||
import { MockAgent, setGlobalDispatcher } from 'undici';
|
||||
import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor.js';
|
||||
import { beforeEach, afterEach, test, expect, vitest } from 'vitest';
|
||||
@@ -492,7 +492,7 @@ test('server responding too slow', async () => {
|
||||
|
||||
const promise = api2.get('/slow');
|
||||
|
||||
await expect(promise).rejects.toThrowError('Request aborted');
|
||||
await expect(promise).rejects.toThrowError('aborted');
|
||||
}, 1_000);
|
||||
|
||||
test('Unauthorized', async () => {
|
||||
@@ -570,8 +570,8 @@ test('abort', async () => {
|
||||
controller.abort();
|
||||
|
||||
// Abort mid-execution:
|
||||
await expect(bP2).rejects.toThrowError('Request aborted');
|
||||
await expect(bP2).rejects.toThrowError('aborted');
|
||||
|
||||
// Abort scheduled:
|
||||
await expect(cP2).rejects.toThrowError('Request aborted');
|
||||
await expect(cP2).rejects.toThrowError('Request aborted manually');
|
||||
});
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
import { Blob, Buffer } from 'node:buffer';
|
||||
import { URLSearchParams } from 'node:url';
|
||||
import { test, expect } from 'vitest';
|
||||
import { resolveBody, parseHeader } from '../src/lib/utils/utils.js';
|
||||
import { MockAgent, setGlobalDispatcher } from 'undici';
|
||||
import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor.js';
|
||||
import { beforeEach, afterEach, test, expect, vitest } from 'vitest';
|
||||
import { REST } from '../src/index.js';
|
||||
import { makeRequest, resolveBody } from '../src/strategies/undiciRequest.js';
|
||||
import { genPath } from './util.js';
|
||||
|
||||
test('GIVEN string parseHeader returns string', () => {
|
||||
const header = 'application/json';
|
||||
const makeRequestMock = vitest.fn(makeRequest);
|
||||
|
||||
expect(parseHeader(header)).toEqual(header);
|
||||
const api = new REST({ makeRequest: makeRequestMock }).setToken('A-Very-Fake-Token');
|
||||
|
||||
// @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');
|
||||
});
|
||||
|
||||
test('GIVEN string[] parseHeader returns string', () => {
|
||||
const header = ['application/json', 'wait sorry I meant text/html'];
|
||||
|
||||
expect(parseHeader(header)).toEqual(header.join(';'));
|
||||
});
|
||||
|
||||
test('GIVEN undefined parseHeader return undefined', () => {
|
||||
expect(parseHeader(undefined)).toBeUndefined();
|
||||
afterEach(async () => {
|
||||
await mockAgent.close();
|
||||
});
|
||||
|
||||
test('resolveBody', async () => {
|
||||
@@ -43,7 +58,7 @@ test('resolveBody', async () => {
|
||||
}
|
||||
},
|
||||
};
|
||||
await expect(resolveBody(iterable)).resolves.toStrictEqual(new Uint8Array([1, 2, 3, 1, 2, 3, 1, 2, 3]));
|
||||
await expect(resolveBody(iterable)).resolves.toStrictEqual(Buffer.from([1, 2, 3, 1, 2, 3, 1, 2, 3]));
|
||||
|
||||
const asyncIterable: AsyncIterable<Uint8Array> = {
|
||||
[Symbol.asyncIterator]() {
|
||||
@@ -66,3 +81,19 @@ test('resolveBody', async () => {
|
||||
// @ts-expect-error: This test is ensuring that this throws
|
||||
await expect(resolveBody(true)).rejects.toThrow(TypeError);
|
||||
});
|
||||
|
||||
test('use passed undici request', async () => {
|
||||
mockPool
|
||||
.intercept({
|
||||
path: genPath('/simplePost'),
|
||||
method: 'POST',
|
||||
})
|
||||
.reply(() => ({
|
||||
data: { test: true },
|
||||
statusCode: 200,
|
||||
responseOptions,
|
||||
}));
|
||||
|
||||
expect(await api.post('/simplePost')).toStrictEqual({ test: true });
|
||||
expect(makeRequestMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
Reference in New Issue
Block a user