test(bot,gateway): Fix Bot E2E and Gateway Integration tests (#4084)

* Fix bot e2e, refactor gateway integration tests

* use debug for bot e2e log

* Discard changes to packages/bot/src/bot.ts
This commit is contained in:
Fleny
2025-01-06 04:04:12 -05:00
committed by GitHub
parent cd396cf6bd
commit 27808bea7d
5 changed files with 156 additions and 195 deletions
+3 -20
View File
@@ -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)
}
},
+2 -3
View File
@@ -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"
}
}
@@ -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<void>()
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<void>()
const { promise: heartbeated, resolve: resolveHeartbeat, reject: rejectHeartbeat } = promiseWithResolvers<void>()
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<void>()
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<void>()
const { promise: heartbeated, resolve: resolveHeartbeat } = promiseWithResolvers<void>()
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<T>() {
let resolve!: (value: T | PromiseLike<T>) => void
@@ -191,8 +92,3 @@ function promiseWithResolvers<T>() {
reject,
}
}
interface CreateUwsOptions {
onOpen?: () => any
onMessage?: (message: DiscordGatewayPayload) => any
}
@@ -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))
}
-8
View File
@@ -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"