From ad9ad041abba5efd2f294043f3ef7c051b7ec459 Mon Sep 17 00:00:00 2001 From: alina sireneva Date: Tue, 30 Apr 2024 05:34:43 +0300 Subject: [PATCH] test(e2e-deno): ported tests from e2e-node --- e2e/deno/.gitignore | 3 +- e2e/deno/tests/01.auth.ts | 83 +++++++++++++ e2e/deno/tests/02.methods.ts | 47 ++++++++ e2e/deno/tests/03.files.ts | 170 +++++++++++++++++++++++++++ e2e/deno/tests/04.updates.ts | 47 ++++++++ e2e/deno/tests/05.worker.ts | 79 +++++++++++++ e2e/deno/tests/_worker.ts | 18 +++ e2e/deno/utils.ts | 8 +- packages/deno/src/utils/exit-hook.ts | 2 +- 9 files changed, 451 insertions(+), 6 deletions(-) create mode 100644 e2e/deno/tests/01.auth.ts create mode 100644 e2e/deno/tests/02.methods.ts create mode 100644 e2e/deno/tests/03.files.ts create mode 100644 e2e/deno/tests/04.updates.ts create mode 100644 e2e/deno/tests/05.worker.ts create mode 100644 e2e/deno/tests/_worker.ts diff --git a/e2e/deno/.gitignore b/e2e/deno/.gitignore index 75a15d22..6771fc36 100644 --- a/e2e/deno/.gitignore +++ b/e2e/deno/.gitignore @@ -1,3 +1,4 @@ /.jsr-data .env -/deno.lock \ No newline at end of file +/deno.lock +/.sessions \ No newline at end of file diff --git a/e2e/deno/tests/01.auth.ts b/e2e/deno/tests/01.auth.ts new file mode 100644 index 00000000..1b739983 --- /dev/null +++ b/e2e/deno/tests/01.auth.ts @@ -0,0 +1,83 @@ +import { assertEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts' + +import { MtcuteError } from '@mtcute/core' +import { BaseTelegramClient, TelegramClient } from '@mtcute/core/client.js' + +import { getApiParams } from '../utils.ts' + +const getAccountId = () => + Math.floor(Math.random() * 10000) + .toString() + .padStart(4, '0') + +Deno.test('1. authorization', { sanitizeResources: false }, async (t) => { + await t.step('should authorize in default dc', async () => { + const base = new BaseTelegramClient(getApiParams('dc2.session')) + const tg = new TelegramClient({ client: base }) + + // reset storage just in case + await base.mt.storage.load() + await base.storage.clear(true) + + while (true) { + const phone = `999662${getAccountId()}` + let user + + try { + user = await tg.start({ + phone, + code: () => '22222', + }) + } catch (e) { + if (e instanceof MtcuteError && e.message.match(/Signup is no longer supported|2FA is enabled/)) { + // retry with another number + continue + } else { + await tg.close() + throw e + } + } + + await tg.close() + + assertEquals(user.isSelf, true) + assertEquals(user.phoneNumber, phone) + break + } + }) + + await t.step('should authorize in dc 1', async () => { + const base = new BaseTelegramClient(getApiParams('dc1.session')) + const tg = new TelegramClient({ client: base }) + + // reset storage just in case + await base.mt.storage.load() + await base.mt.storage.clear(true) + + while (true) { + const phone = `999661${getAccountId()}` + let user + + try { + user = await tg.start({ + phone, + code: () => '11111', + }) + } catch (e) { + if (e instanceof MtcuteError && e.message.match(/Signup is no longer supported|2FA is enabled/)) { + // retry with another number + continue + } else { + await tg.close() + throw e + } + } + + await tg.close() + + assertEquals(user.isSelf, true) + assertEquals(user.phoneNumber, phone) + break + } + }) +}) diff --git a/e2e/deno/tests/02.methods.ts b/e2e/deno/tests/02.methods.ts new file mode 100644 index 00000000..e3e5a140 --- /dev/null +++ b/e2e/deno/tests/02.methods.ts @@ -0,0 +1,47 @@ +import { assertEquals, assertNotEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts' + +import { TelegramClient } from '@mtcute/core/client.js' + +import { getApiParams } from '../utils.ts' + +Deno.test('2. calling methods', { sanitizeResources: false }, async (t) => { + const tg = new TelegramClient(getApiParams('dc2.session')) + + await tg.connect() + + await t.step('getUsers(@BotFather)', async () => { + const [user] = await tg.getUsers('botfather') + + assertEquals(user?.isBot, true) + assertEquals(user?.displayName, 'BotFather') + }) + + await t.step('getUsers(@BotFather) - cached', async () => { + const [user] = await tg.getUsers('botfather') + + assertEquals(user?.isBot, true) + assertEquals(user?.displayName, 'BotFather') + }) + + await t.step('getHistory(777000)', async () => { + const history = await tg.getHistory(777000, { limit: 5 }) + + assertEquals(history[0].chat.chatType, 'private') + assertEquals(history[0].chat.id, 777000) + assertEquals(history[0].chat.firstName, 'Telegram') + }) + + await t.step('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') + + assertEquals(res.isSelf, true) + assertNotEquals(oldSelf.bio, newSelf.bio) + assertEquals(newSelf.bio, bio) + }) + + await tg.close() +}) diff --git a/e2e/deno/tests/03.files.ts b/e2e/deno/tests/03.files.ts new file mode 100644 index 00000000..3cbbbf5a --- /dev/null +++ b/e2e/deno/tests/03.files.ts @@ -0,0 +1,170 @@ +import { assertEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts' +import { createHash } from 'node:crypto' + +import { FileDownloadLocation, Thumbnail } from '@mtcute/core' +import { TelegramClient } from '@mtcute/core/client.js' +import { sleep } from '@mtcute/core/utils.js' + +import { getApiParams } from '../utils.ts' + +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') +} + +Deno.test('3. working with files', { sanitizeResources: false }, async (t) => { + // 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 + // + // ...except we can't under deno because it's not implemented + // https://github.com/denoland/deno/issues/19882 + // this.retries(2) + + await t.step('same-dc', async (t) => { + const tg = new TelegramClient(getApiParams('dc2.session')) + + await tg.connect() + + await t.step('should download pfp thumbs', async () => { + const chat = await tg.getChat(CINNAMOROLL_PFP_CHAT) + if (!chat.photo) throw new Error('Chat has no photo') + + assertEquals(await downloadAsSha256(tg, chat.photo.big), CINNAMOROLL_PFP_THUMB_SHA256) + }) + + await t.step('should download animated pfps', async () => { + const chat = await tg.getFullChat(CINNAMOROLL_PFP_CHAT) + const thumb = chat.fullPhoto?.getThumbnail(Thumbnail.THUMB_VIDEO_PROFILE) + if (!thumb) throw new Error('Chat has no animated pfp') + + assertEquals(await downloadAsSha256(tg, thumb), CINNAMOROLL_PFP_SHA256) + }) + + await t.step('should download photos', async () => { + const msg = await tg.getMessageByLink(UWU_MSG) + + if (msg?.media?.type !== 'photo') { + throw new Error('Message not found or not a photo') + } + + assertEquals(await downloadAsSha256(tg, msg.media), UWU_SHA256) + }) + + await t.step('should download documents', async () => { + const msg = await tg.getMessageByLink(SHREK_MSG) + + if (msg?.media?.type !== 'document') { + throw new Error('Message not found or not a document') + } + + assertEquals(await downloadAsSha256(tg, msg.media), SHREK_SHA256) + }) + + await t.step('should cancel downloads', async () => { + const msg = await tg.getMessageByLink(LARGE_MSG) + + if (msg?.media?.type !== 'document') { + throw new Error('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 + + assertEquals(downloaded, downloadedBefore, 'nothing should be downloaded after abort') + }) + + await tg.close() + }) + + await t.step('cross-dc', async (t) => { + const tg = new TelegramClient(getApiParams('dc1.session')) + + await tg.connect() + + await t.step('should download pfp thumbs', async () => { + const chat = await tg.getChat(CINNAMOROLL_PFP_CHAT) + if (!chat.photo) throw new Error('Chat has no photo') + + assertEquals(await downloadAsSha256(tg, chat.photo.big), CINNAMOROLL_PFP_THUMB_SHA256) + }) + + await t.step('should download animated pfps', async () => { + const chat = await tg.getFullChat(CINNAMOROLL_PFP_CHAT) + const thumb = chat.fullPhoto?.getThumbnail(Thumbnail.THUMB_VIDEO_PROFILE) + if (!thumb) throw new Error('Chat has no animated pfp') + + assertEquals(await downloadAsSha256(tg, thumb), CINNAMOROLL_PFP_SHA256) + }) + + await t.step('should download photos', async () => { + const msg = await tg.getMessageByLink(UWU_MSG) + + if (msg?.media?.type !== 'photo') { + throw new Error('Message not found or not a photo') + } + + assertEquals(await downloadAsSha256(tg, msg.media), UWU_SHA256) + }) + + await t.step('should download documents', async () => { + const msg = await tg.getMessageByLink(SHREK_MSG) + + if (msg?.media?.type !== 'document') { + throw new Error('Message not found or not a document') + } + + assertEquals(await downloadAsSha256(tg, msg.media), SHREK_SHA256) + }) + + await tg.close() + }) +}) diff --git a/e2e/deno/tests/04.updates.ts b/e2e/deno/tests/04.updates.ts new file mode 100644 index 00000000..37bc49e1 --- /dev/null +++ b/e2e/deno/tests/04.updates.ts @@ -0,0 +1,47 @@ +import { assertEquals, assertNotEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts' + +import { Message } from '@mtcute/core' +import { TelegramClient } from '@mtcute/core/client.js' + +import { getApiParams, waitFor } from '../utils.ts' + +Deno.test('4. handling updates', { sanitizeResources: false }, async (t) => { + const tg1 = new TelegramClient(getApiParams('dc1.session')) + tg1.log.prefix = '[tg1] ' + const tg2 = new TelegramClient(getApiParams('dc2.session')) + tg2.log.prefix = '[tg2] ' + + await tg1.connect() + await tg1.startUpdatesLoop() + await tg2.connect() + + await t.step('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) + + assertEquals(sentMsg.text, messageText) + assertEquals(sentMsg.chat.id, tg1User!.id) + + await waitFor(() => { + assertNotEquals( + tg1Messages.find((msg) => msg.text === messageText), + undefined, + ) + }) + }) + + await tg1.close() + await tg2.close() +}) diff --git a/e2e/deno/tests/05.worker.ts b/e2e/deno/tests/05.worker.ts new file mode 100644 index 00000000..fe8ea4ce --- /dev/null +++ b/e2e/deno/tests/05.worker.ts @@ -0,0 +1,79 @@ +import { assertEquals, assertGreater, assertInstanceOf } from 'https://deno.land/std@0.223.0/assert/mod.ts' + +import { TelegramClient } from '@mtcute/core/client.js' +import { Message, TelegramWorkerPort, tl } from '@mtcute/deno' + +import { getApiParams, waitFor } from '../utils.ts' +import type { CustomMethods } from './_worker.ts' + +Deno.test('5. worker', { sanitizeResources: false }, async (t) => { + const worker = new Worker(new URL('_worker.ts', import.meta.url), { + type: 'module', + }) + + const port = new TelegramWorkerPort({ + worker, + }) + const portClient = new TelegramClient({ client: port }) + + await t.step('should make api calls', async function () { + const res = await port.call({ _: 'help.getConfig' }) + + assertEquals(res._, 'config') + }) + + await t.step('should call custom methods', async function () { + const hello = await port.invokeCustom('hello') + assertEquals(hello, 'world') + + const sum = await port.invokeCustom('sum', 2, 3) + assertEquals(sum, 5) + }) + + await t.step('should throw errors', async function () { + try { + await port.call({ _: 'test.useConfigSimple' }) + throw new Error('should have thrown') + } catch (e) { + assertInstanceOf(e, tl.RpcError) + } + }) + + await t.step('should receive updates', async function () { + const client2 = new TelegramClient(getApiParams('dc2.session')) + + try { + await client2.connect() + await port.startUpdatesLoop() + + const me = await portClient.getMe() + let username = me.username + + if (!username) { + username = `mtcute_e2e_${Math.random().toString(36).slice(2, 8)}` + await portClient.setMyUsername(username) + } + + const msgs: Message[] = [] + portClient.on('new_message', (msg) => { + msgs.push(msg) + }) + + const testText = `test ${Math.random()}` + await client2.sendText(username, testText) + + await waitFor(() => { + assertGreater(msgs.length, 0) + assertEquals(msgs[0].text, testText) + }) + } catch (e) { + await client2.close() + throw e + } + + await client2.close() + }) + + await port.close() + worker.terminate() +}) diff --git a/e2e/deno/tests/_worker.ts b/e2e/deno/tests/_worker.ts new file mode 100644 index 00000000..1b5e3938 --- /dev/null +++ b/e2e/deno/tests/_worker.ts @@ -0,0 +1,18 @@ +import { WorkerCustomMethods } from '@mtcute/core/worker.js' +import { BaseTelegramClient, TelegramWorker } from '@mtcute/deno' + +import { getApiParams } from '../utils.ts' + +const customMethods = { + hello: async () => 'world', + sum: async (a: number, b: number) => a + b, +} as const satisfies WorkerCustomMethods +export type CustomMethods = typeof customMethods + +const client = new BaseTelegramClient(getApiParams('dc1.session')) + +// eslint-disable-next-line no-new +new TelegramWorker({ + client, + customMethods, +}) diff --git a/e2e/deno/utils.ts b/e2e/deno/utils.ts index 8714b990..dfba3630 100644 --- a/e2e/deno/utils.ts +++ b/e2e/deno/utils.ts @@ -1,22 +1,22 @@ import { MaybePromise, MemoryStorage } from '@mtcute/core' import { setPlatform } from '@mtcute/core/platform.js' import { LogManager, sleep } from '@mtcute/core/utils.js' -import { DenoCryptoProvider, DenoPlatform, TcpTransport } from '@mtcute/deno' +import { DenoCryptoProvider, DenoPlatform, SqliteStorage, TcpTransport } from '@mtcute/deno' export const getApiParams = (storage?: string) => { - if (storage) throw new Error('unsupported yet') - if (!Deno.env.has('API_ID') || !Deno.env.has('API_HASH')) { throw new Error('API_ID and API_HASH env variables must be set') } + Deno.mkdirSync('.sessions', { recursive: true }) + setPlatform(new DenoPlatform()) return { apiId: parseInt(Deno.env.get('API_ID')!), apiHash: Deno.env.get('API_HASH')!, testMode: true, - storage: new MemoryStorage(), + storage: storage ? new SqliteStorage(`.sessions/${storage}`) : new MemoryStorage(), logLevel: LogManager.VERBOSE, transport: () => new TcpTransport(), crypto: new DenoCryptoProvider(), diff --git a/packages/deno/src/utils/exit-hook.ts b/packages/deno/src/utils/exit-hook.ts index aa43b595..ecaa0e40 100644 --- a/packages/deno/src/utils/exit-hook.ts +++ b/packages/deno/src/utils/exit-hook.ts @@ -6,7 +6,7 @@ export function beforeExit(fn: () => void): () => void { if (!registered) { registered = true - window.addEventListener('unload', () => { + globalThis.addEventListener('unload', () => { for (const callback of callbacks) { callback() }