diff --git a/.config/vite-utils/chai-setup.ts b/.config/vite-utils/chai-setup.ts new file mode 100644 index 00000000..b6a9bfa9 --- /dev/null +++ b/.config/vite-utils/chai-setup.ts @@ -0,0 +1,57 @@ +export function setupChai(chai: any, vitestExpect: any) { + chai.use(vitestExpect.JestExtend) + chai.use(vitestExpect.JestChaiExpect) + chai.use(vitestExpect.JestAsymmetricMatchers) + chai.use((chai: any, utils: any) => { + utils.addMethod( + chai.Assertion.prototype, + 'toMatchInlineSnapshot', + function (properties?: object, inlineSnapshot?: string, message?: string) { + // based on https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/integrations/snapshot/chai.ts + + const received = utils.flag(this, 'object') + if (typeof properties === 'string') { + message = inlineSnapshot + inlineSnapshot = properties + properties = undefined + } + + if (typeof inlineSnapshot !== 'string') { + throw new Error('toMatchInlineSnapshot requires a string argument') + } + + // todo use @vitest/snapshot + if (typeof received === 'string') { + const snapshot = '"' + received + '"' + return chai.expect(snapshot).eql(inlineSnapshot.trim()) + } else { + const obj = eval('(' + inlineSnapshot + ')') // idc lol + return chai.expect(received).eql(obj) + } + }, + ) + + utils.addMethod(chai.Assertion.prototype, 'toMatchSnapshot', function () { + // todo use @vitest/snapshot + }) + }) + + vitestExpect.setState( + { + assertionCalls: 0, + isExpectingAssertions: false, + isExpectingAssertionsError: null, + expectedAssertionsNumber: null, + expectedAssertionsNumberErrorGen: null, + environment: 'deno', + testPath: 'deno-test.ts', + currentTestName: 'deno-test', + }, + chai.expect, + ) + Object.defineProperty(globalThis, vitestExpect.GLOBAL_EXPECT, { + value: chai.expect, + writable: true, + configurable: true, + }) +} diff --git a/.config/vite-utils/collect-test-entrypoints.ts b/.config/vite-utils/collect-test-entrypoints.ts new file mode 100644 index 00000000..45aa9e44 --- /dev/null +++ b/.config/vite-utils/collect-test-entrypoints.ts @@ -0,0 +1,26 @@ +import { join, resolve } from 'path' +import * as fs from 'fs' +import { globSync } from 'glob' + +export function collectTestEntrypoints(params: { skipPackages: string[]; skipTests: string[] }) { + const files: string[] = [] + + const packages = resolve(__dirname, '../../packages') + + const skipTests = params.skipTests.map((path) => resolve(packages, path)) + + for (const dir of fs.readdirSync(packages)) { + if (dir.startsWith('.') || params.skipPackages.includes(dir)) continue + if (!fs.statSync(resolve(packages, dir)).isDirectory()) continue + + const fullDir = resolve(packages, dir) + + for (const file of globSync(join(fullDir, '**/*.test.ts'))) { + if (skipTests.includes(file)) continue + if (file.match(/\/(node_modules|dist)\//)) continue + files.push(file) + } + } + + return files +} diff --git a/.config/vite-utils/fixup-deno-test.ts b/.config/vite-utils/fixup-deno-test.ts new file mode 100644 index 00000000..5d0fb827 --- /dev/null +++ b/.config/vite-utils/fixup-deno-test.ts @@ -0,0 +1,83 @@ +// @ts-expect-error no typings +import { describe as _describe, it, beforeEach, afterEach, beforeAll, afterAll } from 'jsr:@std/testing/bdd' +// @ts-expect-error no typings +import * as vitestSpy from 'npm:@vitest/spy' +// @ts-expect-error no typings +import * as chai from 'npm:chai' +// @ts-expect-error no typings +import * as vitestExpect from 'npm:@vitest/expect' +import util from 'node:util' +import { setupChai } from './chai-setup' + +export { it, beforeEach, afterEach, beforeAll, afterAll } + +setupChai(chai, vitestExpect) + +Object.defineProperty(it, 'each', { + value: (items: any[][]) => (name: string, fn: Function) => { + return items.map((item) => { + return it(`${util.format(name, ...item)}`, () => fn(...item)) + }) + }, +}) + +export const describe = (...args) => { + const fn = args.find((arg) => typeof arg === 'function') + if (fn.toString().startsWith('async')) { + // https://github.com/denoland/deno_std/issues/4634 + return + } + + return _describe(...args) +} +describe.skip = _describe.skip +describe.only = _describe.only +describe.ignore = _describe.ignore + +export const expect = chai.expect + +const stubbedGlobal = new Map() +function stubGlobal(name, value) { + stubbedGlobal.set(name, globalThis[name]) + globalThis[name] = value +} + +function unstubAllGlobals() { + for (const [name, value] of stubbedGlobal) { + globalThis[name] = value + } + stubbedGlobal.clear() +} + +export const vi = { + ...vitestSpy, + mocked: (fn: any) => fn, + stubGlobal, + unstubAllGlobals, + waitFor: async (fn: Function) => { + // less customizations than vi.waitFor but it's good enough for now + const timeout = Date.now() + 5000 + + let lastError: unknown + while (Date.now() < timeout) { + try { + return await fn() + } catch (e) { + lastError = e + await new Promise((resolve) => setTimeout(resolve, 10)) + } + } + + throw lastError + }, + // todo use @sinonjs/fake-timers (see https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/integrations/mock/timers.ts) + ...['setSystemTime', 'advanceTimersByTimeAsync', 'advanceTimersByTime', 'doMock'].reduce( + (acc, name) => ({ + ...acc, + [name]: () => { + throw new Error(name) + }, + }), + {}, + ), +} diff --git a/.config/vite-utils/test-setup.mts b/.config/vite-utils/test-setup.mts index 5b9a57c9..38c5f7e3 100644 --- a/.config/vite-utils/test-setup.mts +++ b/.config/vite-utils/test-setup.mts @@ -2,7 +2,7 @@ import { setPlatform } from '../../packages/core/src/platform.js' // @ts-expect-error no .env here const TEST_ENV = import.meta.env.TEST_ENV -if (TEST_ENV === 'browser') { +if (TEST_ENV === 'browser' || TEST_ENV === 'deno') { setPlatform(new (await import('../../packages/web/src/platform.js')).WebPlatform()) } else { setPlatform(new (await import('../../packages/node/src/common-internals-node/platform.js')).NodePlatform()) diff --git a/.config/vite.bun.mts b/.config/vite.bun.mts index 10e0d3d8..ed8bffd8 100644 --- a/.config/vite.bun.mts +++ b/.config/vite.bun.mts @@ -4,47 +4,28 @@ import { resolve, join } from 'path' import * as fs from 'fs' import { fixupCjs } from './vite-utils/fixup-cjs' import { testSetup } from './vite-utils/test-setup-plugin' +import { collectTestEntrypoints } from './vite-utils/collect-test-entrypoints' -const SKIP_PACKAGES = ['create-bot', 'crypto-node'] - -// https://github.com/oven-sh/bun/issues/4145 prevents us from using vitest directly -// so we have to use bun's native test runner const FIXUP_TEST = resolve(__dirname, 'vite-utils/fixup-bun-test.ts') - -// bun:test doesn't support certain features of vitest, so we'll skip them for now -// https://github.com/oven-sh/bun/issues/1825 -const SKIP_TESTS = [ - // uses timers - 'core/src/network/config-manager.test.ts', - // incompatible spies - 'core/src/utils/crypto/mtproto.test.ts', - // snapshot format - 'tl-utils/src/codegen/errors.test.ts' -].map(path => resolve(__dirname, '../packages', path)) - export default defineConfig({ build: { lib: { - entry: process.env.ENTRYPOINT ? [process.env.ENTRYPOINT] : (() => { - const files: string[] = [] - - const packages = resolve(__dirname, '../packages') - - for (const dir of fs.readdirSync(packages)) { - if (dir.startsWith('.') || SKIP_PACKAGES.includes(dir)) continue - if (!fs.statSync(resolve(packages, dir)).isDirectory()) continue - - const fullDir = resolve(packages, dir) - - for (const file of globSync(join(fullDir, '**/*.test.ts'))) { - if (SKIP_TESTS.includes(file)) continue - files.push(file) - } - } - - return files - })(), + entry: process.env.ENTRYPOINT ? [process.env.ENTRYPOINT] : collectTestEntrypoints({ + // https://github.com/oven-sh/bun/issues/4145 prevents us from using vitest directly + // so we have to use bun's native test runner + skipPackages: ['create-bot', 'crypto-node'], + // bun:test doesn't support certain features of vitest, so we'll skip them for now + // https://github.com/oven-sh/bun/issues/1825 + skipTests: [ + // uses timers + 'core/src/network/config-manager.test.ts', + // incompatible spies + 'core/src/utils/crypto/mtproto.test.ts', + // snapshot format + 'tl-utils/src/codegen/errors.test.ts' + ], + }), formats: ['es'], }, rollupOptions: { diff --git a/.config/vite.deno.mts b/.config/vite.deno.mts new file mode 100644 index 00000000..e0bb6f18 --- /dev/null +++ b/.config/vite.deno.mts @@ -0,0 +1,100 @@ +import { defineConfig } from 'vite' +import { resolve, join } from 'path' +import { fixupCjs } from './vite-utils/fixup-cjs' +import { testSetup } from './vite-utils/test-setup-plugin' +import { collectTestEntrypoints } from './vite-utils/collect-test-entrypoints' + +const FIXUP_TEST = resolve(__dirname, 'vite-utils/fixup-deno-test.ts') + +export default defineConfig({ + build: { + lib: { + entry: process.env.ENTRYPOINT ? [process.env.ENTRYPOINT] : collectTestEntrypoints({ + // these packages rely on node apis and are not meant to be run under deno + skipPackages: ['create-bot', 'crypto-node', 'bun', 'node', 'http-proxy', 'socks-proxy', 'mtproxy'], + skipTests: [ + // uses timers + 'core/src/network/config-manager.test.ts', + 'core/src/network/persistent-connection.test.ts', + // https://github.com/denoland/deno/issues/22470 + 'wasm/tests/gunzip.test.ts', + 'wasm/tests/zlib.test.ts', + ], + }), + formats: ['es'], + }, + rollupOptions: { + external: [ + // todo which of these are actually needed? + 'zlib', + 'vitest', + 'stream', + 'net', + 'crypto', + 'module', + 'fs', + 'fs/promises', + 'readline', + 'worker_threads', + 'events', + 'path', + 'util', + 'os', + // + /^(jsr|npm|node|https?):/, + ], + output: { + chunkFileNames: 'chunk-[hash].js', + entryFileNames: '[name]-[hash].test.js', + minifyInternalExports: false, + }, + treeshake: false, + }, + commonjsOptions: { + ignoreDynamicRequires: true, + }, + outDir: process.env.OUT_DIR || 'dist/tests', + emptyOutDir: true, + target: 'esnext', + minify: false, + }, + plugins: [ + fixupCjs(), + { + name: 'fix-vitest', + transform(code) { + if (!code.includes('vitest')) return code + code = code.replace(/^import {(.+?)} from ['"]vitest['"]/gms, (_, names) => { + const namesParsed = names.split(',').map((name) => name.trim()) + + return `import {${namesParsed.join(', ')}} from '${FIXUP_TEST}'` + }) + return code + }, + }, + { + name: 'fix-events', + transform(code) { + if (!code.includes('events')) return code + return code.replace(/^import (.+?) from ['"]events['"]/gms, (_, name) => { + return `import ${name} from 'node:events'` + }) + }, + }, + // todo + // { + // name: 'fix-wasm-load', + // async transform(code, id) { + // if (code.includes('@mtcute/wasm/mtcute.wasm')) { + // return code.replace('@mtcute/wasm/mtcute.wasm', resolve(__dirname, '../packages/wasm/mtcute.wasm')) + // } + + // return code + // } + // }, + testSetup(), + ], + define: { + 'import.meta.env.TEST_ENV': '"deno"', + }, +}) diff --git a/packages/core/src/network/session-connection.ts b/packages/core/src/network/session-connection.ts index 0dc08df2..2452b019 100644 --- a/packages/core/src/network/session-connection.ts +++ b/packages/core/src/network/session-connection.ts @@ -167,6 +167,9 @@ export class SessionConnection extends PersistentConnection { if (forever) { this.removeAllListeners() + this.on('error', (err) => { + this.log.warn('caught error after destroying: %s', err) + }) } } @@ -309,6 +312,7 @@ export class SessionConnection extends PersistentConnection { }) .catch((err: Error) => { this._session.authorizationPending = false + if (this._destroyed) return this.log.error('Authorization error: %s', err.message) this.onError(err) this.reconnect() @@ -476,6 +480,7 @@ export class SessionConnection extends PersistentConnection { ) }) .catch((err: Error) => { + if (this._destroyed) return this.log.error('PFS Authorization error: %s', err.message) if (this._isPfsBindingPendingInBackground) { @@ -492,6 +497,9 @@ export class SessionConnection extends PersistentConnection { } waitForUnencryptedMessage(timeout = 5000): Promise { + if (this._destroyed) { + return Promise.reject(new MtcuteError('Connection destroyed')) + } const promise = createControllablePromise() const timeoutId = setTimeout(() => { promise.reject(new MtTimeoutError(timeout)) diff --git a/packages/test/src/crypto.ts b/packages/test/src/crypto.ts index dab13e34..99d52c80 100644 --- a/packages/test/src/crypto.ts +++ b/packages/test/src/crypto.ts @@ -1,5 +1,5 @@ +import { gzipSync, inflateSync } from 'node:zlib' import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' -import { gzipSync, inflateSync } from 'zlib' import { getPlatform } from '@mtcute/core/platform.js' import { dataViewFromBuffer, ICryptoProvider } from '@mtcute/core/utils.js' diff --git a/packages/wasm/tests/gunzip.test.ts b/packages/wasm/tests/gunzip.test.ts index 79aa59f2..cd7d7d40 100644 --- a/packages/wasm/tests/gunzip.test.ts +++ b/packages/wasm/tests/gunzip.test.ts @@ -1,5 +1,5 @@ +import { gzipSync } from 'node:zlib' import { beforeAll, describe, expect, it } from 'vitest' -import { gzipSync } from 'zlib' import { getPlatform } from '@mtcute/core/platform.js' @@ -13,7 +13,7 @@ beforeAll(async () => { const p = getPlatform() function gzipSyncWrap(data: Uint8Array) { - if (import.meta.env.TEST_ENV === 'browser') { + if (import.meta.env.TEST_ENV === 'browser' || import.meta.env.TEST_ENV === 'deno') { // @ts-expect-error fucking crutch because @jspm/core uses Buffer.isBuffer for some reason data._isBuffer = true diff --git a/packages/wasm/tests/zlib.test.ts b/packages/wasm/tests/zlib.test.ts index 848c4871..b21ad206 100644 --- a/packages/wasm/tests/zlib.test.ts +++ b/packages/wasm/tests/zlib.test.ts @@ -1,5 +1,5 @@ +import { inflateSync } from 'node:zlib' import { beforeAll, describe, expect, it } from 'vitest' -import { inflateSync } from 'zlib' import { getPlatform } from '@mtcute/core/platform.js' @@ -13,7 +13,7 @@ beforeAll(async () => { const p = getPlatform() function inflateSyncWrap(data: Uint8Array) { - if (import.meta.env.TEST_ENV === 'browser') { + if (import.meta.env.TEST_ENV === 'browser' || import.meta.env.TEST_ENV === 'deno') { // @ts-expect-error fucking crutch because @jspm/core uses Buffer.isBuffer for some reason data._isBuffer = true