From 4451608dfa1f6cc27ebe05da4aa87ec40c57d3a3 Mon Sep 17 00:00:00 2001 From: Jonathan Ho Date: Sun, 5 Feb 2023 08:58:40 -0800 Subject: [PATCH] test: add gateway integration test (#2756) * test: add gateway integration test * test(gateway): fix connection test * test(gateway): add heartbeat test * ci: add integration test * fix: add uWebSockets.js * ci: add timeout * test(utils): remove old test * Revert "test(utils): remove old test" This reverts commit 04fb6dd4b57b153c7ae418ce0798eb93e687857a. * test(gateway): fix uws server * test(gateway): fix type * chore: update codecov flag * test(gateway): remove dev code --------- Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> --- .github/workflows/integration-test.yml | 48 +++++ ...e-test.yml => other-runtime-unit-test.yml} | 0 .github/workflows/test.yml | 58 +++--- .../{package-test.yml => unit-test.yml} | 1 + codecov.yml | 6 + package.json | 1 + packages/gateway/package.json | 10 +- .../tests/integration/connection.spec.ts | 171 ++++++++++++++++++ turbo.json | 8 + yarn.lock | 8 + 10 files changed, 281 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/integration-test.yml rename .github/workflows/{other-runtime-package-test.yml => other-runtime-unit-test.yml} (100%) rename .github/workflows/{package-test.yml => unit-test.yml} (98%) create mode 100644 packages/gateway/tests/integration/connection.spec.ts diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 000000000..c03ffac91 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,48 @@ +name: Package Test + +on: + workflow_call: + inputs: + package: + required: true + type: string + +jobs: + integration-test: + name: Integration Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT + - uses: actions/cache@v3 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - run: yarn install --immutable + - name: Turbo Cache + id: turbo-cache + uses: actions/cache@v3 + with: + path: .turbo + key: ${{ runner.os }}-turbo-test:integration-${{ inputs.package }}-${{ github.sha }} + - name: Build dist cache + if: steps.turbo-cache.outputs.cache-hit != 'true' + uses: actions/cache@v3 + with: + path: .turbo + key: ${{ runner.os }}-turbo-build-${{ github.sha }} + - name: Integration Test + run: yarn test:integration --cache-dir=".turbo" --filter=./packages/${{ inputs.package }} + timeout-minutes: 1 + - name: Collect and upload the coverage report + uses: codecov/codecov-action@v3 + with: + files: ./packages/${{ inputs.package }}/coverage/lcov.info + flags: ${{ inputs.package }}-integration diff --git a/.github/workflows/other-runtime-package-test.yml b/.github/workflows/other-runtime-unit-test.yml similarity index 100% rename from .github/workflows/other-runtime-package-test.yml rename to .github/workflows/other-runtime-unit-test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 25a79d013..83bf17309 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -128,51 +128,57 @@ jobs: run: yarn test:test-type --cache-dir=".turbo" # Not using matrix because test later on cant needs a specific job - client-unit-and-integration-test: + client-unit-test: name: Client needs: build-dist - uses: ./.github/workflows/package-test.yml + uses: ./.github/workflows/unit-test.yml with: package: client client-other-runtime-test: name: Client - needs: client-unit-and-integration-test - uses: ./.github/workflows/other-runtime-package-test.yml + needs: client-unit-test + uses: ./.github/workflows/other-runtime-unit-test.yml with: package: client - discordeno-unit-and-integration-test: + discordeno-unit-test: name: Discordeno needs: build-dist - uses: ./.github/workflows/package-test.yml + uses: ./.github/workflows/unit-test.yml with: package: discordeno discordeno-other-runtime-test: name: Discordeno - needs: discordeno-unit-and-integration-test - uses: ./.github/workflows/other-runtime-package-test.yml + needs: discordeno-unit-test + uses: ./.github/workflows/other-runtime-unit-test.yml with: package: discordeno - gateway-unit-and-integration-test: + gateway-unit-test: name: Gateway needs: build-dist - uses: ./.github/workflows/package-test.yml + uses: ./.github/workflows/unit-test.yml + with: + package: gateway + gateway-integration-test: + name: Gateway + needs: build-dist + uses: ./.github/workflows/integration-test.yml with: package: gateway gateway-other-runtime-test: name: Gateway - needs: gateway-unit-and-integration-test - uses: ./.github/workflows/other-runtime-package-test.yml + needs: [gateway-unit-test, gateway-integration-test] + uses: ./.github/workflows/other-runtime-unit-test.yml with: package: gateway - rest-unit-and-integration-test: + rest-unit-test: name: Rest needs: build-dist - uses: ./.github/workflows/package-test.yml + uses: ./.github/workflows/unit-test.yml with: package: rest rest-e2e-test: name: Rest - needs: rest-unit-and-integration-test + needs: rest-unit-test if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/node-migration' || github.ref == 'refs/heads/node-migration-clean' }} uses: ./.github/workflows/e2e-test.yml secrets: inherit @@ -180,31 +186,31 @@ jobs: package: rest rest-other-runtime-test: name: Rest - needs: rest-unit-and-integration-test - uses: ./.github/workflows/other-runtime-package-test.yml + needs: rest-unit-test + uses: ./.github/workflows/other-runtime-unit-test.yml with: package: rest - utils-unit-and-integration-test: + utils-unit-test: name: Utils needs: build-dist - uses: ./.github/workflows/package-test.yml + uses: ./.github/workflows/unit-test.yml with: package: utils utils-other-runtime-test: name: Utils - needs: utils-unit-and-integration-test - uses: ./.github/workflows/other-runtime-package-test.yml + needs: utils-unit-test + uses: ./.github/workflows/other-runtime-unit-test.yml with: package: utils - bot-unit-and-integration-test: + bot-unit-test: name: Bot needs: build-dist - uses: ./.github/workflows/package-test.yml + uses: ./.github/workflows/unit-test.yml with: package: bot bot-e2e-test: name: Bot - needs: bot-unit-and-integration-test + needs: bot-unit-test if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/node-migration' || github.ref == 'refs/heads/node-migration-clean' }} uses: ./.github/workflows/e2e-test.yml secrets: inherit @@ -212,7 +218,7 @@ jobs: package: bot bot-other-runtime-test: name: Bot - needs: bot-unit-and-integration-test - uses: ./.github/workflows/other-runtime-package-test.yml + needs: bot-unit-test + uses: ./.github/workflows/other-runtime-unit-test.yml with: package: bot diff --git a/.github/workflows/package-test.yml b/.github/workflows/unit-test.yml similarity index 98% rename from .github/workflows/package-test.yml rename to .github/workflows/unit-test.yml index 558db2368..b8d250e11 100644 --- a/.github/workflows/package-test.yml +++ b/.github/workflows/unit-test.yml @@ -40,6 +40,7 @@ jobs: key: ${{ runner.os }}-turbo-build-${{ github.sha }} - name: Unit Test run: yarn test:unit --cache-dir=".turbo" --filter=./packages/${{ inputs.package }} + timeout-minutes: 1 - name: Collect and upload the coverage report uses: codecov/codecov-action@v3 with: diff --git a/codecov.yml b/codecov.yml index 20e556be1..20aac09ca 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,8 +5,14 @@ flags: carryforward: false embeds-unit: carryforward: false + bot-unit: + carryforward: false + bot-e2e: + carryforward: true gatway-unit: carryforward: false + gatway-integration: + carryforward: false logger-unit: carryforward: false rest-unit: diff --git a/package.json b/package.json index 63db46512..c0cbcd182 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "test:type": "turbo run build:type", "test:unit-coverage": "turbo run test:unit-coverage", "test:unit": "turbo run test:unit", + "test:integration": "turbo run test:integration", "test:deno-unit": "turbo run test:deno-unit", "test:e2e": "turbo run test:e2e", "test:test-type": "turbo run test:test-type", diff --git a/packages/gateway/package.json b/packages/gateway/package.json index f31e0508c..95df54c6c 100644 --- a/packages/gateway/package.json +++ b/packages/gateway/package.json @@ -15,10 +15,11 @@ "release-build": "yarn build && yarn build:type", "fmt": "eslint --fix \"src/**/*.ts*\"", "lint": "eslint \"src/**/*.ts*\"", - "test:unit-coverage": "c8 mocha --no-warnings 'tests/**/*.spec.ts'", - "test:unit": "c8 --r lcov mocha --no-warnings 'tests/**/*.spec.ts' && node ../../scripts/coveragePathFixing.js gateway", + "test:unit-coverage": "c8 mocha --no-warnings 'tests/unit/**/*.spec.ts'", + "test:unit": "c8 --r lcov mocha --no-warnings 'tests/unit/**/*.spec.ts' && node ../../scripts/coveragePathFixing.js gateway", "test:deno-unit": "swc tests --delete-dir-on-start --out-dir denoTestsDist && node ../../scripts/fixDenoTestExtension.js && deno test -A --import-map ../../denoImportMap.json denoTestsDist/unit", - "test:unit:watch": "mocha --no-warnings --watch --parallel 'tests/**/*.spec.ts'", + "test:unit:watch": "mocha --no-warnings --watch --parallel 'tests/unit/**/*.spec.ts'", + "test:integration": "c8 --r lcov mocha --no-warnings 'tests/integration/**/*.spec.ts' && node ../../scripts/coveragePathFixing.js gateway", "test:type": "tsc --noEmit", "test:test-type": "tsc --project tsconfig.test.json" }, @@ -43,6 +44,7 @@ "sinon": "^15.0.0", "ts-node": "^10.9.1", "tsconfig": "*", - "typescript": "^4.9.3" + "typescript": "^4.9.3", + "uWebSockets.js": "uNetworking/uWebSockets.js#v20.19.0" } } diff --git a/packages/gateway/tests/integration/connection.spec.ts b/packages/gateway/tests/integration/connection.spec.ts new file mode 100644 index 000000000..43368c325 --- /dev/null +++ b/packages/gateway/tests/integration/connection.spec.ts @@ -0,0 +1,171 @@ +import { Intents } from '@discordeno/types' +import uWS from 'uWebSockets.js' +import { createGatewayManager, ShardSocketCloseCodes } from '../../src/index.js' + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const createGatewayManagerWithPort = (port: number) => + createGatewayManager({ + connection: { + url: `ws://localhost:${port}`, + shards: 1, + sessionStartLimit: { + total: 1000, + remaining: 998, + resetAfter: 36579894, + maxConcurrency: 1, + }, + }, + token: ' ', + url: `ws://localhost:${port}`, + intents: Intents.Guilds, + events: {}, + }) + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const createUws = async (options: { + onOpen?: () => any + onMessage?: (message: any) => any + onClose?: (code: number, message: string) => any + closing?: boolean +}) => { + options.onOpen ??= () => {} + options.onMessage ??= (message: any) => {} + options.onClose ??= (code: number, message: string) => {} + options.closing ??= false + + return await new Promise<{ port: number; uwsToken: any }>((resolve, reject) => { + let port = 0 + let uwsToken = 0 + uWS + .App() + .ws('/*', { + compression: uWS.SHARED_COMPRESSOR, + maxPayloadLength: 16 * 1024 * 1024, + idleTimeout: 10, + open: async (ws) => { + if (options.closing) { + ws.end(3000) + return + } + ws.send( + JSON.stringify({ + op: 10, + d: { + heartbeat_interval: 100, + }, + }), + ) + options.onOpen!() + }, + message: async (ws, message, isBinary) => { + const msg = JSON.parse(Buffer.from(message).toString()) + options.onMessage!(msg) + if (msg.op === 1) { + ws.send( + JSON.stringify({ + op: 11, + }), + ) + return + } + if (msg.op === 2) { + ws.send( + JSON.stringify({ + t: 'READY', + s: 1, + op: 0, + 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 + } + if (msg.op === 6) { + // resume + } + }, + close: (ws, code, message) => { + const msg = Buffer.from(message).toString() + options.onClose!(code, msg) + }, + }) + .listen(0, async (token) => { + if (!token) { + reject(new Error()) + } + // retrieve listening port + uwsToken = token + port = uWS.us_socket_local_port(token) + resolve({ port, uwsToken }) + }) + }) +} + +describe('gateway', () => { + it('can connect to server', async function () { + this.timeout(6000) + let resolveConnected: () => void + const connected = new Promise((resolve) => (resolveConnected = resolve)) + const uwsOptions = { onOpen: resolveConnected!, closing: false } + const { port, uwsToken } = await createUws(uwsOptions) + const gateway = createGatewayManagerWithPort(port) + await gateway.spawnShards() + await connected + uwsOptions.closing = true + await gateway.shutdown(ShardSocketCloseCodes.Shutdown, 'User requested bot stop') + uWS.us_listen_socket_close(uwsToken) + }) + + it('will heartbeat', async function () { + this.timeout(6000) + let resolveHeartbeat: () => void + let resolveConnected: () => void + const connected = new Promise((resolve) => (resolveConnected = resolve)) + const Heartbeated = new Promise((resolve) => (resolveHeartbeat = resolve)) + const uwsOptions = { + onOpen: resolveConnected!, + onMessage: (message: any) => { + if (message.op !== 1) return + resolveHeartbeat() + }, + closing: false, + } + 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') + }, 100) + await Heartbeated + clearTimeout(timeout) + uwsOptions.closing = true + await gateway.shutdown(ShardSocketCloseCodes.Shutdown, 'User requested bot stop') + uWS.us_listen_socket_close(uwsToken) + }) +}) diff --git a/turbo.json b/turbo.json index a0e9c96c8..9c6d648fb 100644 --- a/turbo.json +++ b/turbo.json @@ -43,6 +43,14 @@ "coverage/**" ] }, + "test:integration": { + "dependsOn": [ + "^build" + ], + "outputs": [ + "coverage/**" + ] + }, "test:test-type": { "dependsOn": [ "^build:type" diff --git a/yarn.lock b/yarn.lock index 847b67193..2cc00a215 100644 --- a/yarn.lock +++ b/yarn.lock @@ -95,6 +95,7 @@ __metadata: ts-node: ^10.9.1 tsconfig: "*" typescript: ^4.9.3 + uWebSockets.js: "uNetworking/uWebSockets.js#v20.19.0" ws: ^8.11.0 languageName: unknown linkType: soft @@ -5351,6 +5352,13 @@ __metadata: languageName: node linkType: hard +"uWebSockets.js@uNetworking/uWebSockets.js#v20.19.0": + version: 20.19.0 + resolution: "uWebSockets.js@https://github.com/uNetworking/uWebSockets.js.git#commit=42c9c0d5d31f46ca4115dc75672b0037ec970f28" + checksum: 72d623720c12a4d6e1ebbd1a71e33626d028511f5e7585b39dbdd6782d695f42abc54050a28398cd98fa25d9136ba39e1dc44c976c9e6ad113a561c3304024c4 + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4": version: 3.17.4 resolution: "uglify-js@npm:3.17.4"