diff --git a/packages/bot/tests/e2e/resetguilds.spec.ts b/packages/bot/tests/e2e/resetguilds.spec.ts index c71635064..1ec5c8771 100644 --- a/packages/bot/tests/e2e/resetguilds.spec.ts +++ b/packages/bot/tests/e2e/resetguilds.spec.ts @@ -1,5 +1,5 @@ import { Intents } from '@discordeno/types' -import { delay, logger, snakeToCamelCase } from '@discordeno/utils' +import { delay, logger } from '@discordeno/utils' import { use as chaiUse } from 'chai' import chaiAsPromised from 'chai-as-promised' import { describe, it } from 'mocha' @@ -18,24 +18,7 @@ describe('[Bot] Delete any guild owned guilds', () => { ownerId: true, }, }, - gateway: { - token, - events: { - message: async (shard, data) => { - // TRIGGER RAW EVENT - bot.events.raw?.(data, shard.id) - - if (!data.t) return - - // RUN DISPATCH CHECK - await bot.events.dispatchRequirements?.(data, shard.id) - - const eventName = snakeToCamelCase(data.t) - bot.events[eventName as keyof typeof bot.events]?.(data.d as never, shard as never) - }, - }, - intents: Intents.Guilds, - }, + intents: Intents.Guilds, events: { async guildCreate(guild) { if (guild.joinedAt && Date.now() - guild.joinedAt < 360000) { @@ -43,7 +26,7 @@ describe('[Bot] Delete any guild owned guilds', () => { } if (bot.rest.applicationId === guild.ownerId) { - logger.debug(`Deleting one of the bot created guilds.`, guild.id) + logger.debug(`Deleting one of the bot created guilds. ${guild.id}`) await bot.rest.deleteGuild(guild.id) } }, diff --git a/packages/gateway/package.json b/packages/gateway/package.json index 848f224cb..79d70c762 100644 --- a/packages/gateway/package.json +++ b/packages/gateway/package.json @@ -25,7 +25,7 @@ "test:unit:watch": "mocha --no-warnings --watch --parallel 'tests/unit/**/*.spec.ts'", "test:deno-unit": "deno -A ../../node_modules/mocha/bin/mocha.js --config ../../.mocharc.base.cjs 'tests/unit/**/*.spec.ts'", "test:bun-unit": "bun ../../node_modules/mocha/bin/mocha.js --config ../../.mocharc.base.cjs 'tests/unit/**/*.spec.ts'", - "test:integration": "c8 --r lcov mocha --no-warnings 'tests/integration/**/*.spec.ts' && node ../../scripts/coveragePathFixing.js gateway", + "test:integration": "c8 --r lcov mocha --no-warnings --t 15000 'tests/integration/**/*.spec.ts' && node ../../scripts/coveragePathFixing.js gateway", "test:type": "tsc --noEmit", "test:test-type": "tsc --project tests/tsconfig.json" }, @@ -52,7 +52,6 @@ "sinon": "^19.0.2", "ts-node": "^10.9.2", "tsconfig": "*", - "typescript": "^5.7.2", - "uWebSockets.js": "uNetworking/uWebSockets.js#54e5db370db746ed1c85021bdf215578a2164901" + "typescript": "^5.7.2" } } diff --git a/packages/gateway/tests/integration/connection.spec.ts b/packages/gateway/tests/integration/connection.spec.ts index 4e31d5029..b8cb359e8 100644 --- a/packages/gateway/tests/integration/connection.spec.ts +++ b/packages/gateway/tests/integration/connection.spec.ts @@ -1,14 +1,57 @@ -import { type DiscordGatewayPayload, GatewayCloseEventCodes, GatewayOpcodes, Intents } from '@discordeno/types' -import uWS from 'uWebSockets.js' -import { type GatewayManager, createGatewayManager } from '../../src/index.js' +import { GatewayOpcodes, Intents } from '@discordeno/types' +import { type GatewayManager, ShardSocketCloseCodes, createGatewayManager } from '../../src/index.js' +import { creatWSServer, heartbeatInterval } from './websocket.js' -/** - * This value needs to be AT LEAST `1017` - * - * The reason for this is because the calculation in Shard.calculateSafeRequests will return 0 not allowing any sort of message to the websocket server. - * Discord uses a way higher number for this value, but during this test we lower it since it would be annoying and useless make the test last 40+ seconds to test the heartbeat, but to make this work it needs to be at least 1017 so that calculateSafeRequests return 2 allowing for the shard to send messages. - */ -const heartbeatInterval = 1050 +describe('Gateway Integration', () => { + it('Can connect to server', async () => { + const { promise: connected, resolve: resolveConnected } = promiseWithResolvers() + + const { port, close } = creatWSServer({ + onOpen: resolveConnected, + }) + + const gateway = createGatewayManagerWithPort(port) + await gateway.spawnShards() + await connected + + await gateway.shutdown(ShardSocketCloseCodes.TestingFinished, 'Testing finished') + + // To avoid needing to wait 1m to get the bucket refil timer to fire we cancel it + clearTimeout(gateway.shards.get(0)?.bucket.timeoutId) + + close() + }) + + it('Can heartbeat', async () => { + const { promise: connected, resolve: resolveConnected } = promiseWithResolvers() + const { promise: heartbeated, resolve: resolveHeartbeat, reject: rejectHeartbeat } = promiseWithResolvers() + + const { port, close } = creatWSServer({ + onOpen: resolveConnected, + onMessage: (message) => { + if (message.op === GatewayOpcodes.Heartbeat) { + resolveHeartbeat() + } + }, + }) + + const gateway = createGatewayManagerWithPort(port) + await gateway.spawnShards() + await connected + + const timeout = setTimeout(() => rejectHeartbeat(new Error('Not heartbeat in time')), heartbeatInterval) + await heartbeated + + clearTimeout(timeout) + + await gateway.shutdown(ShardSocketCloseCodes.TestingFinished, 'Testing finished') + + // To avoid needing to wait 1m to get the bucket refil timer to fire we cancel it + clearTimeout(gateway.shards.get(0)?.bucket.timeoutId) + + close() + }) +}) function createGatewayManagerWithPort(port: number): GatewayManager { return createGatewayManager({ @@ -17,164 +60,22 @@ function createGatewayManagerWithPort(port: number): GatewayManager { shards: 1, sessionStartLimit: { total: 1000, - remaining: 998, - resetAfter: 36579894, + remaining: 1000, + resetAfter: 0, maxConcurrency: 1, }, }, - token: ' ', + token: '', url: `ws://localhost:${port}`, intents: Intents.Guilds, - events: {}, - }) -} - -async function createUws(options: CreateUwsOptions) { - let port: number - - const { promise, resolve, reject } = promiseWithResolvers<{ port: number; uwsToken: uWS.us_listen_socket }>() - - const app = uWS.App() - - app.ws('/*', { - open: async (ws) => { - ws.send( - JSON.stringify({ - op: GatewayOpcodes.Hello, - d: { - heartbeat_interval: heartbeatInterval, - }, - }), - ) - options.onOpen?.() - }, - message: async (ws, message, _isBinary) => { - const msg = JSON.parse(Buffer.from(message).toString()) - options.onMessage?.(msg) - - if (msg.op === GatewayOpcodes.Heartbeat) { - ws.send( - JSON.stringify({ - op: GatewayOpcodes.HeartbeatACK, - }), - ) - - return - } - if (msg.op === GatewayOpcodes.Identify) { - ws.send( - JSON.stringify({ - t: 'READY', - s: 1, - op: GatewayOpcodes.Dispatch, - d: { - v: 10, - user_settings: {}, - user: { - verified: true, - username: 'testing bot', - mfa_enabled: false, - id: '000000707882254000', - flags: 0, - email: null, - discriminator: '1687', - bot: true, - avatar: null, - }, - shard: [0, 1], - session_type: 'normal', - session_id: '0dff79e1a6f2697388eb08924a0805c8', - resume_gateway_url: `ws://localhost:${port}`, - relationships: [], - private_channels: [], - presences: [], - guilds: [], - guild_join_requests: [], - geo_ordered_rtc_regions: [], - application: { id: '000000707882254000', flags: 27828224 }, - }, - }), - ) - - return - } + resharding: { + enabled: false, + checkInterval: 0, + shardsFullPercentage: 0, }, }) - - app.listen(0, (token) => { - if (!token) { - reject('Unable to get the socket token') - } - - port = uWS.us_socket_local_port(token) - - resolve({ - port, - uwsToken: token, - }) - }) - - return promise } -describe('gateway', () => { - it('can connect to server', async function () { - this.timeout(15000) - - const { promise: connected, resolve: resolveConnected } = promiseWithResolvers() - - const uwsOptions: CreateUwsOptions = { - onOpen: resolveConnected, - } - - const { port, uwsToken } = await createUws(uwsOptions) - - const gateway = createGatewayManagerWithPort(port) - await gateway.spawnShards() - await connected - - // TODO: We should use ShardSocketCloseCodes.TestingFinished but there is an issue with sending 3xxx codes to uWS - await gateway.shutdown(GatewayCloseEventCodes.InvalidShard, 'User requested bot stop', true) - - uWS.us_listen_socket_close(uwsToken) - }) - - it('will heartbeat', async function () { - this.timeout(15000) - - const { promise: connected, resolve: resolveConnected } = promiseWithResolvers() - const { promise: heartbeated, resolve: resolveHeartbeat } = promiseWithResolvers() - - const uwsOptions: CreateUwsOptions = { - onOpen: resolveConnected, - onMessage: (message) => { - if (message.op !== 1) return - - resolveHeartbeat() - }, - } - - const { port, uwsToken } = await createUws(uwsOptions) - - const gateway = createGatewayManagerWithPort(port) - await gateway.spawnShards() - await connected - - const timeout = setTimeout(() => { - throw new Error('Not heartbeat in time') - }, heartbeatInterval) - - await heartbeated - - clearTimeout(timeout) - - // TODO: We should use ShardSocketCloseCodes.TestingFinished but there is an issue with sending 3xxx codes to uWS - await gateway.shutdown(GatewayCloseEventCodes.InvalidShard, 'User requested bot stop', true) - - uWS.us_listen_socket_close(uwsToken) - }) -}) - // Polyfill for https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers function promiseWithResolvers() { let resolve!: (value: T | PromiseLike) => void @@ -191,8 +92,3 @@ function promiseWithResolvers() { reject, } } - -interface CreateUwsOptions { - onOpen?: () => any - onMessage?: (message: DiscordGatewayPayload) => any -} diff --git a/packages/gateway/tests/integration/websocket.ts b/packages/gateway/tests/integration/websocket.ts new file mode 100644 index 000000000..b7dbceaad --- /dev/null +++ b/packages/gateway/tests/integration/websocket.ts @@ -0,0 +1,91 @@ +import { type DiscordGatewayPayload, GatewayOpcodes } from '@discordeno/types' +import { type WebSocket, WebSocketServer } from 'ws' + +/** + * This value needs to be AT LEAST `1017` + * + * The reason for this is because the calculation in Shard.calculateSafeRequests will return 0 not allowing any sort of message to the websocket server. + * Discord uses a way higher number for this value, but during this test we lower it since it would be annoying and useless make the test last 40+ seconds to test the heartbeat, but to make this work it needs to be at least 1017 so that calculateSafeRequests return 2 allowing for the shard to send messages. + */ +export const heartbeatInterval = 1017 + +export function creatWSServer(options: CreateWsServerOptions) { + // Port 0 according to the node:http docs is for requesting the OS a random unused port + const server = new WebSocketServer({ port: 0 }) + + const address = server.address() + if (typeof address !== 'object' || !address) { + throw new TypeError('The address of the WebSocketServer should be an non-null object') + } + + server.on('connection', (socket) => { + send(socket, { + op: GatewayOpcodes.Hello, + s: null, + d: { + heartbeat_interval: heartbeatInterval, + }, + }) + + options.onOpen?.() + + socket.on('message', (data) => { + const msg = JSON.parse(data.toString('utf-8')) + options.onMessage?.(msg) + + switch (msg.op) { + case GatewayOpcodes.Heartbeat: { + send(socket, { + op: GatewayOpcodes.HeartbeatACK, + s: null, + }) + + break + } + + case GatewayOpcodes.Identify: { + send(socket, { + op: GatewayOpcodes.Dispatch, + t: 'READY', + s: 1, + d: { + v: 10, + user: { + id: '0', + username: 'test', + discriminator: '0', + global_name: 'test', + avatar: null, + bot: true, + verified: true, + email: null, + flags: 0, + }, + shard: [0, 1], + session_id: '0', + resume_gateway_url: `ws://localhost:${address.port}`, + guilds: [], + application: { id: '0', flags: 0 }, + }, + }) + + break + } + } + }) + }) + + return { + port: address.port, + close: () => server.close(), + } +} + +export interface CreateWsServerOptions { + onOpen?: () => any + onMessage?: (message: DiscordGatewayPayload) => any +} + +function send(ws: WebSocket, payload: object) { + ws.send(JSON.stringify(payload)) +} diff --git a/yarn.lock b/yarn.lock index ac51b40e2..826759bb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -159,7 +159,6 @@ __metadata: ts-node: "npm:^10.9.2" tsconfig: "npm:*" typescript: "npm:^5.7.2" - uWebSockets.js: "uNetworking/uWebSockets.js#54e5db370db746ed1c85021bdf215578a2164901" ws: "npm:^8.18.0" dependenciesMeta: fzstd: @@ -4326,13 +4325,6 @@ __metadata: languageName: node linkType: hard -"uWebSockets.js@uNetworking/uWebSockets.js#54e5db370db746ed1c85021bdf215578a2164901": - version: 20.47.0 - resolution: "uWebSockets.js@https://github.com/uNetworking/uWebSockets.js.git#commit=54e5db370db746ed1c85021bdf215578a2164901" - checksum: c6ed6796b1ed86118e7ea32fa5e486a9aa8d2cc5e3757eb20e8a3120779b47130c318c5f84bbdfbb744c816af272ea7c0220e163ff1c7b85bcd848b04d521408 - languageName: node - linkType: hard - "uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0": version: 2.1.0 resolution: "uc.micro@npm:2.1.0"