diff --git a/e2e/config.js b/e2e/config.js index 1e9a7953..7aabe1c9 100644 --- a/e2e/config.js +++ b/e2e/config.js @@ -9,15 +9,20 @@ module.exports = { }, ts: { getFiles: () => 'tests/**/*.ts', - beforeAll: () => [ - 'tsc', - 'node build-esm.cjs', - ], - runFile: (file) => [ - `mocha -r ts-node/register ${file}`, - `mocha dist/${file.replace(/\.ts$/, '.js')}`, - `node run-esm.cjs ${file}`, - `mocha dist/esm/${file.replace(/\.ts$/, '.js')}`, - ], + beforeAll: () => ['tsc', 'node build-esm.cjs'], + runFile: (file) => { + if (file.startsWith('tests/packaging/')) { + // packaging tests - we need to make sure everything imports and works + return [ + `mocha -r ts-node/register ${file}`, + `mocha dist/${file.replace(/\.ts$/, '.js')}`, + `node run-esm.cjs ${file}`, + `mocha dist/esm/${file.replace(/\.ts$/, '.js')}`, + ] + } + + // normal e2e tests - testing features etc + return `mocha dist/${file.replace(/\.ts$/, '.js')}` + }, }, } diff --git a/e2e/runner.js b/e2e/runner.js index 810e96e6..412c821d 100644 --- a/e2e/runner.js +++ b/e2e/runner.js @@ -70,6 +70,7 @@ function runForDir(dir) { } const files = glob.sync(getFiles(), { cwd: path.join(__dirname, dir) }) + files.sort() for (const file of files) { runForFile(dir, file, false) diff --git a/e2e/ts/build-esm.cjs b/e2e/ts/build-esm.cjs index 2a95bfa7..64d189ef 100644 --- a/e2e/ts/build-esm.cjs +++ b/e2e/ts/build-esm.cjs @@ -1,4 +1,3 @@ -/* eslint-disable no-restricted-globals */ const fs = require('fs') const path = require('path') const cp = require('child_process') diff --git a/e2e/ts/tests/01.auth.ts b/e2e/ts/tests/01.auth.ts new file mode 100644 index 00000000..67de54f7 --- /dev/null +++ b/e2e/ts/tests/01.auth.ts @@ -0,0 +1,77 @@ +import { expect } from 'chai' +import { describe, it } from 'mocha' + +import { MtUnsupportedError, TelegramClient } from '@mtcute/client' + +import { getApiParams } from '../utils.js' + +const getAccountId = () => + Math.floor(Math.random() * 10000) + .toString() + .padStart(4, '0') + +describe('1. authorization', function () { + this.timeout(300_000) + + it('should authorize in default dc', async () => { + const tg = new TelegramClient(getApiParams('dc2.session')) + + // reset storage just in case + await tg.storage.load?.() + await tg.storage.reset(true) + + while (true) { + const phone = `999662${getAccountId()}` + let user + + try { + user = await tg.start({ + phone, + code: () => '22222', + }) + } catch (e) { + if (e instanceof MtUnsupportedError && e.message.includes('Signup is no longer supported')) { + // retry with another number + continue + } else throw e + } + + await tg.close() + + expect(user.isSelf).to.be.true + expect(user.phoneNumber).to.equal(phone) + break + } + }) + + it('should authorize in dc 1', async () => { + const tg = new TelegramClient(getApiParams('dc1.session')) + + // reset storage just in case + await tg.storage.load?.() + await tg.storage.reset(true) + + while (true) { + const phone = `999661${getAccountId()}` + let user + + try { + user = await tg.start({ + phone, + code: () => '11111', + }) + } catch (e) { + if (e instanceof MtUnsupportedError && e.message.includes('Signup is no longer supported')) { + // retry with another number + continue + } else throw e + } + + await tg.close() + + expect(user.isSelf).to.be.true + expect(user.phoneNumber).to.equal(phone) + break + } + }) +}) diff --git a/e2e/ts/tests/02.methods.ts b/e2e/ts/tests/02.methods.ts new file mode 100644 index 00000000..ea1ca009 --- /dev/null +++ b/e2e/ts/tests/02.methods.ts @@ -0,0 +1,48 @@ +import { expect } from 'chai' +import { describe, it } from 'mocha' + +import { TelegramClient } from '@mtcute/client' + +import { getApiParams } from '../utils.js' + +describe('2. calling methods', function () { + this.timeout(300_000) + const tg = new TelegramClient(getApiParams('dc2.session')) + + this.beforeAll(() => tg.connect()) + this.afterAll(() => tg.close()) + + it('getUsers(@BotFather)', async () => { + const [user] = await tg.getUsers('botfather') + + expect(user?.isBot).to.be.true + expect(user?.displayName).to.equal('BotFather') + }) + + it('getUsers(@BotFather) - cached', async () => { + const [user] = await tg.getUsers('botfather') + + expect(user?.isBot).to.be.true + expect(user?.displayName).to.equal('BotFather') + }) + + it('getHistory(777000)', async () => { + const history = await tg.getHistory(777000, { limit: 5 }) + + expect(history[0].chat.chatType).to.equal('private') + expect(history[0].chat.id).to.equal(777000) + expect(history[0].chat.firstName).to.equal('Telegram') + }) + + it('updateProfile', async () => { + const bio = `mtcute e2e ${new Date().toISOString()}` + + const oldSelf = await tg.getFullChat('self') + const res = await tg.updateProfile({ bio }) + const newSelf = await tg.getFullChat('self') + + expect(res.isSelf).to.be.true + expect(oldSelf.bio).to.not.equal(newSelf.bio) + expect(newSelf.bio).to.equal(bio) + }) +}) diff --git a/e2e/ts/tests/03.files.ts b/e2e/ts/tests/03.files.ts new file mode 100644 index 00000000..4ac3deb4 --- /dev/null +++ b/e2e/ts/tests/03.files.ts @@ -0,0 +1,167 @@ +/* eslint-disable no-restricted-imports */ +import { expect } from 'chai' +import { createHash } from 'crypto' +import { describe, it } from 'mocha' + +import { FileDownloadLocation, TelegramClient, Thumbnail } from '@mtcute/client' +import { sleep } from '@mtcute/core/utils.js' + +import { getApiParams } from '../utils.js' + +const CINNAMOROLL_PFP_CHAT = 'test_file_dc2' +const CINNAMOROLL_PFP_THUMB_SHA256 = '3e6f220235a12547c16129f50c19ed3224d39b827414d1d500f79569a3431eae' +const CINNAMOROLL_PFP_SHA256 = '4d9836a71ac039f5656cde55b83525871549bfbff9cfb658c3f8381c5ba89ce8' + +const UWU_MSG = 'https://t.me/test_file_dc2/8' +const UWU_SHA256 = '357b78c9f9d20e813f729a19dd90c6727f30ebd4c8c83557022285f283a705b9' + +const SHREK_MSG = 'https://t.me/test_file_dc2/11' +const SHREK_SHA256 = 'd3e6434e027f3d31dc3e05c6ea2eaf84fdd1fb00774a215f89d9ed8b56f86258' + +const LARGE_MSG = 'https://t.me/test_file_dc2/12' + +async function downloadAsSha256(client: TelegramClient, location: FileDownloadLocation): Promise { + const sha = createHash('sha256') + + for await (const chunk of client.downloadAsIterable(location)) { + sha.update(chunk) + } + + return sha.digest('hex') +} + +describe('3. working with files', function () { + this.timeout(300_000) + // sometimes test dcs are overloaded and we get FILE_REFERENCE_EXPIRED + // because we got multiple -500:No workers running errors in a row + // we currently don't have file references database, so we can just retry the test for now + this.retries(2) + + describe('same-dc', () => { + const tg = new TelegramClient(getApiParams('dc2.session')) + + this.beforeAll(() => tg.connect()) + this.afterAll(() => tg.close()) + + it('should download pfp thumbs', async () => { + const chat = await tg.getChat(CINNAMOROLL_PFP_CHAT) + if (!chat.photo) expect.fail('Chat has no photo') + + expect(await downloadAsSha256(tg, chat.photo.big)).to.equal(CINNAMOROLL_PFP_THUMB_SHA256) + }) + + it('should download animated pfps', async () => { + const chat = await tg.getFullChat(CINNAMOROLL_PFP_CHAT) + const thumb = chat.fullPhoto?.getThumbnail(Thumbnail.THUMB_VIDEO_PROFILE) + if (!thumb) expect.fail('Chat has no animated pfp') + + expect(await downloadAsSha256(tg, thumb)).to.equal(CINNAMOROLL_PFP_SHA256) + }) + + it('should download photos', async () => { + const msg = await tg.getMessageByLink(UWU_MSG) + + if (msg?.media?.type !== 'photo') { + expect.fail('Message not found or not a photo') + } + + expect(await downloadAsSha256(tg, msg.media)).to.equal(UWU_SHA256) + }) + + it('should download documents', async () => { + const msg = await tg.getMessageByLink(SHREK_MSG) + + if (msg?.media?.type !== 'document') { + expect.fail('Message not found or not a document') + } + + expect(await downloadAsSha256(tg, msg.media)).to.equal(SHREK_SHA256) + }) + + it('should cancel downloads', async () => { + const msg = await tg.getMessageByLink(LARGE_MSG) + + if (msg?.media?.type !== 'document') { + expect.fail('Message not found or not a document') + } + + const media = msg.media + + const abort = new AbortController() + + let downloaded = 0 + + async function download() { + const dl = tg.downloadAsIterable(media, { abortSignal: abort.signal }) + + try { + for await (const chunk of dl) { + downloaded += chunk.length + } + } catch (e) { + if (!(e instanceof DOMException && e.name === 'AbortError')) throw e + } + } + + const promise = download() + + // let it download for 10 seconds + await sleep(10000) + abort.abort() + // abort and snap the downloaded amount + const downloadedBefore = downloaded + + const avgSpeed = downloaded / 10 + // eslint-disable-next-line no-console + console.log('Average speed: %d KiB/s', avgSpeed / 1024) + + // wait a bit more to make sure it's aborted + await sleep(2000) + await promise + + expect(downloaded).to.equal(downloadedBefore, 'nothing should be downloaded after abort') + }) + }) + + describe('cross-dc', () => { + const tg = new TelegramClient(getApiParams('dc1.session')) + + this.beforeAll(() => tg.connect()) + this.afterAll(() => tg.close()) + + it('should download pfp thumbs', async () => { + const chat = await tg.getChat(CINNAMOROLL_PFP_CHAT) + if (!chat.photo) expect.fail('Chat has no photo') + + expect(await downloadAsSha256(tg, chat.photo.big)).to.equal(CINNAMOROLL_PFP_THUMB_SHA256) + }) + + it('should download animated pfps', async () => { + const chat = await tg.getFullChat(CINNAMOROLL_PFP_CHAT) + const thumb = chat.fullPhoto?.getThumbnail(Thumbnail.THUMB_VIDEO_PROFILE) + if (!thumb) expect.fail('Chat has no animated pfp') + + expect(await downloadAsSha256(tg, thumb)).to.equal(CINNAMOROLL_PFP_SHA256) + }) + + it('should download photos', async () => { + const msg = await tg.getMessageByLink(UWU_MSG) + + if (msg?.media?.type !== 'photo') { + expect.fail('Message not found or not a photo') + } + + expect(await downloadAsSha256(tg, msg.media)).to.equal(UWU_SHA256) + }) + + it('should download documents', async () => { + const msg = await tg.getMessageByLink(SHREK_MSG) + + if (msg?.media?.type !== 'document') { + expect.fail('Message not found or not a document') + } + + expect(await downloadAsSha256(tg, msg.media)).to.equal(SHREK_SHA256) + }) + }) +}) diff --git a/e2e/ts/tests/04.updates.ts b/e2e/ts/tests/04.updates.ts new file mode 100644 index 00000000..78284610 --- /dev/null +++ b/e2e/ts/tests/04.updates.ts @@ -0,0 +1,49 @@ +import { expect } from 'chai' +import { describe, it } from 'mocha' + +import { Message, TelegramClient } from '@mtcute/client' + +import { getApiParams, waitFor } from '../utils.js' + +describe('4. handling updates', async function () { + this.timeout(300_000) + + const tg1 = new TelegramClient(getApiParams('dc1.session')) + tg1.log.prefix = '[tg1] ' + const tg2 = new TelegramClient(getApiParams('dc2.session')) + tg2.log.prefix = '[tg2] ' + + this.beforeAll(async () => { + await tg1.connect() + await tg1.startUpdatesLoop() + await tg2.connect() + }) + this.afterAll(async () => { + await tg1.close() + await tg2.close() + }) + + it('should send and receive messages', async () => { + const tg1Messages: Message[] = [] + + tg1.on('new_message', (msg) => tg1Messages.push(msg)) + + const [tg1User] = await tg1.getUsers('self') + let username = tg1User!.username + + if (!username) { + username = `mtcute_e2e_${Math.random().toString(36).slice(2)}` + await tg1.setMyUsername(username) + } + + const messageText = `mtcute test message ${Math.random().toString(36).slice(2)}` + const sentMsg = await tg2.sendText(username, messageText) + + expect(sentMsg.text).to.equal(messageText) + expect(sentMsg.chat.id).to.equal(tg1User!.id) + + await waitFor(() => { + expect(tg1Messages.find((msg) => msg.text === messageText)).to.exist + }) + }) +}) diff --git a/e2e/ts/tests/base-client.ts b/e2e/ts/tests/packaging/base-client.ts similarity index 93% rename from e2e/ts/tests/base-client.ts rename to e2e/ts/tests/packaging/base-client.ts index d3acf7e0..7d1aa693 100644 --- a/e2e/ts/tests/base-client.ts +++ b/e2e/ts/tests/packaging/base-client.ts @@ -4,7 +4,7 @@ import { describe, it } from 'mocha' import { BaseTelegramClient } from '@mtcute/core' // @fix-import -import { getApiParams } from '../utils' +import { getApiParams } from '../../utils' describe('@mtcute/core', function () { this.timeout(300_000) diff --git a/e2e/ts/tests/tl-runtime.ts b/e2e/ts/tests/packaging/tl-runtime.ts similarity index 99% rename from e2e/ts/tests/tl-runtime.ts rename to e2e/ts/tests/packaging/tl-runtime.ts index 9e20a1f9..8db7253a 100644 --- a/e2e/ts/tests/tl-runtime.ts +++ b/e2e/ts/tests/packaging/tl-runtime.ts @@ -81,7 +81,7 @@ describe('TlBinaryWriter', () => { w.bytes(obj.pq) w.vector(w.long, obj.serverPublicKeyFingerprints) }, - _staticSize: {} as any + _staticSize: {} as any, } it('should work with Buffers', () => { diff --git a/e2e/ts/tests/tl-schema.ts b/e2e/ts/tests/packaging/tl-schema.ts similarity index 100% rename from e2e/ts/tests/tl-schema.ts rename to e2e/ts/tests/packaging/tl-schema.ts diff --git a/e2e/ts/tests/wasm.ts b/e2e/ts/tests/packaging/wasm.ts similarity index 100% rename from e2e/ts/tests/wasm.ts rename to e2e/ts/tests/packaging/wasm.ts diff --git a/e2e/ts/utils.ts b/e2e/ts/utils.ts index a456568a..ca6fb8c6 100644 --- a/e2e/ts/utils.ts +++ b/e2e/ts/utils.ts @@ -1,8 +1,12 @@ -import { BaseTelegramClientOptions } from '@mtcute/core' -import { MemoryStorage } from '@mtcute/core/storage/memory.js' -import { LogManager } from '@mtcute/core/utils.js' +// eslint-disable-next-line no-restricted-imports +import { join } from 'path' -export const getApiParams = (): BaseTelegramClientOptions => { +import { BaseTelegramClientOptions, MaybeAsync } from '@mtcute/core' +import { MemoryStorage } from '@mtcute/core/storage/memory.js' +import { LogManager, sleep } from '@mtcute/core/utils.js' +import { SqliteStorage } from '@mtcute/sqlite' + +export const getApiParams = (storage?: string): BaseTelegramClientOptions => { if (!process.env.API_ID || !process.env.API_HASH) { throw new Error('API_ID and API_HASH env variables must be set') } @@ -11,7 +15,25 @@ export const getApiParams = (): BaseTelegramClientOptions => { apiId: parseInt(process.env.API_ID), apiHash: process.env.API_HASH, testMode: true, - storage: new MemoryStorage(), - logLevel: LogManager.DEBUG, + storage: storage ? new SqliteStorage(join(__dirname, storage)) : new MemoryStorage(), + logLevel: LogManager.VERBOSE, } } + +export async function waitFor(condition: () => MaybeAsync, timeout = 5000): Promise { + const start = Date.now() + let lastError + + while (Date.now() - start < timeout) { + try { + await condition() + + return + } catch (e) { + lastError = e + await sleep(100) + } + } + + throw lastError +} diff --git a/packages/client/src/methods/messages/send-text.test.ts b/packages/client/src/methods/messages/send-text.test.ts index 2d32ec9e..0fc31431 100644 --- a/packages/client/src/methods/messages/send-text.test.ts +++ b/packages/client/src/methods/messages/send-text.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest' import { Long, toggleChannelIdMark } from '@mtcute/core' import { createStub, StubTelegramClient } from '@mtcute/test' -import { getAuthState } from '../auth/_state.js' +import { getAuthState, setupAuthState } from '../auth/_state.js' import { sendText } from './send-text.js' const stubUser = createStub('user', { @@ -104,6 +104,7 @@ describe('sendText', () => { const client = new StubTelegramClient() await client.registerPeers(stubUser) + setupAuthState(client) getAuthState(client).userId = stubUser.id client.respondWith('messages.sendMessage', () => @@ -127,6 +128,7 @@ describe('sendText', () => { it('should correctly handle updateShortSentMessage without cached peer', async () => { const client = new StubTelegramClient() + setupAuthState(client) getAuthState(client).userId = stubUser.id const getUsersFn = client.respondWith(