From a948e0d44da64b7f5fb6a92675172f66f801e36b Mon Sep 17 00:00:00 2001 From: Wheat Carrier Date: Mon, 22 Apr 2024 05:04:30 +0800 Subject: [PATCH] refactor test cases --- src/api/client/file-api.ts | 4 +- src/api/client/message-api/file-uploader.ts | 13 +- src/api/client/metadata-api.ts | 2 + src/api/ops/list.ts | 37 ++-- src/api/types.ts | 14 +- src/server/webdav/tgfs-filesystem.ts | 34 ++- src/utils/logger.ts | 2 +- test/api/model/operations.spec.ts | 30 ++- test/cmd/cmd.spec.ts | 14 +- test/server/webdav.spec.ts | 112 +++++----- test/utils/mock-gramjs-api.ts | 87 ++++++++ test/utils/mock-messages.spec.ts | 58 ++++++ test/utils/mock-messages.ts | 47 +++-- test/utils/mock-telegraf-api.ts | 37 ++++ test/utils/mock-tg-client.ts | 217 ++++---------------- 15 files changed, 389 insertions(+), 319 deletions(-) create mode 100644 test/utils/mock-gramjs-api.ts create mode 100644 test/utils/mock-messages.spec.ts create mode 100644 test/utils/mock-telegraf-api.ts diff --git a/src/api/client/file-api.ts b/src/api/client/file-api.ts index 6881aa4..8ad1ab3 100644 --- a/src/api/client/file-api.ts +++ b/src/api/client/file-api.ts @@ -9,8 +9,8 @@ export class FileApi extends DirectoryApi { private async createFile(where: TGFSDirectory, fileMsg: GeneralFileMessage) { validateName(fileMsg.name); - const id = await this.createFileDesc(fileMsg); - const fr = where.createFileRef(fileMsg.name, id); + const messageId = await this.createFileDesc(fileMsg); + const fr = where.createFileRef(fileMsg.name, messageId); await this.syncMetadata(); diff --git a/src/api/client/message-api/file-uploader.ts b/src/api/client/message-api/file-uploader.ts index c64e6c9..f4e9cc6 100644 --- a/src/api/client/message-api/file-uploader.ts +++ b/src/api/client/message-api/file-uploader.ts @@ -64,15 +64,14 @@ export abstract class FileUploader { this.uploaded + this.chunkSize > this.fileSize ? this.fileSize - this.uploaded : this.chunkSize; + this.uploaded += chunkLength; + this.partCnt += 1; - const chunk = await this.read(chunkLength); - - if (chunk === null) { + if (chunkLength === 0) { return 0; } - this.uploaded += chunkLength; - this.partCnt += 1; + const chunk = await this.read(chunkLength); let retry = 3; while (retry) { @@ -133,8 +132,8 @@ export abstract class FileUploader { const createWorker = async (workerId: number): Promise => { try { while (!this.done()) { - await this.uploadNextPart(workerId); - if (callback) { + const partSize = await this.uploadNextPart(workerId); + if (partSize && callback) { Logger.info( `[worker ${workerId}] ${ (this.uploaded * 100) / this.fileSize diff --git a/src/api/client/metadata-api.ts b/src/api/client/metadata-api.ts index b79fec5..eccdd15 100644 --- a/src/api/client/metadata-api.ts +++ b/src/api/client/metadata-api.ts @@ -50,6 +50,7 @@ export class MetaDataApi extends FileDescApi { protected async updateMetadata(): Promise { const buffer = Buffer.from(JSON.stringify(this.metadata.toObject())); if (this.metadata.msgId) { + // update current metadata await this.editMessageMedia( this.metadata.msgId, buffer, @@ -57,6 +58,7 @@ export class MetaDataApi extends FileDescApi { '', ); } else { + // doesn't exist, create new metadata and pin const messageId = await this.sendFile({ buffer, name: 'metadata.json' }); this.metadata.msgId = messageId; await this.pinMessage(messageId); diff --git a/src/api/ops/list.ts b/src/api/ops/list.ts index a22080d..148f26d 100644 --- a/src/api/ops/list.ts +++ b/src/api/ops/list.ts @@ -2,27 +2,32 @@ import { PathLike } from 'fs'; import { Client } from 'src/api'; import { FileOrDirectoryDoesNotExistError } from 'src/errors/path'; +import { TGFSDirectory, TGFSFileRef } from 'src/model/directory'; import { navigateToDir } from './navigate-to-dir'; import { splitPath } from './utils'; -export const list = (client: Client) => async (path: PathLike) => { - const [basePath, name] = splitPath(path); - const dir = await navigateToDir(client)(basePath); +export const list = + (client: Client) => + async ( + path: PathLike, + ): Promise> => { + const [basePath, name] = splitPath(path); + const dir = await navigateToDir(client)(basePath); - let nextDir = dir; + let nextDir = dir; - if (name) { - nextDir = dir.findChildren([name])[0]; - } - if (nextDir) { - return [...nextDir.findChildren(), ...nextDir.findFiles()]; - } else { - const nextFile = dir.findFiles([name])[0]; - if (nextFile) { - return nextFile; + if (name) { + nextDir = dir.findChildren([name])[0]; + } + if (nextDir) { + return [...nextDir.findChildren(), ...nextDir.findFiles()]; } else { - throw new FileOrDirectoryDoesNotExistError(path.toString()); + const nextFile = dir.findFiles([name])[0]; + if (nextFile) { + return nextFile; + } else { + throw new FileOrDirectoryDoesNotExistError(path.toString()); + } } - } -}; + }; diff --git a/src/api/types.ts b/src/api/types.ts index 02dc01b..d40cbc9 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -12,14 +12,16 @@ export type GetMessagesReq = Chat & { messageIds: number[]; }; +export type Document = { + size: BigInteger; + id: BigInteger; + accessHash: BigInteger; + fileReference: Buffer; +}; + export type MessageResp = Message & { text?: string; - document?: { - size: BigInteger; - id: BigInteger; - accessHash: BigInteger; - fileReference: Buffer; - }; + document?: Document; }; export type GetMessagesResp = MessageResp[]; diff --git a/src/server/webdav/tgfs-filesystem.ts b/src/server/webdav/tgfs-filesystem.ts index 922bdc5..2006531 100644 --- a/src/server/webdav/tgfs-filesystem.ts +++ b/src/server/webdav/tgfs-filesystem.ts @@ -29,11 +29,7 @@ import { Client, createClient } from 'src/api'; import { createDir, list, removeDir, removeFile } from 'src/api/ops'; import { createEmptyFile } from 'src/api/ops/create-empty-file'; import { uploadFromStream } from 'src/api/ops/upload'; -import { - FileOrDirectoryAlreadyExistsError, - FileOrDirectoryDoesNotExistError, - InvalidNameError, -} from 'src/errors'; +import { BusinessError } from 'src/errors/base'; import { TGFSDirectory, TGFSFileRef } from 'src/model/directory'; import { Logger } from 'src/utils/logger'; @@ -57,12 +53,13 @@ export class TGFSSerializer implements FileSystemSerializer { } } -const handleError = (callback: (e) => any) => (err) => { - if (err instanceof FileOrDirectoryAlreadyExistsError) { +const handleError = (callback: (e: Error) => any) => (err: Error) => { + const castedError = err as BusinessError; + if (castedError.code == 'FILE_OR_DIR_ALREADY_EXISTS') { callback(Errors.ResourceAlreadyExists); - } else if (err instanceof FileOrDirectoryDoesNotExistError) { + } else if (castedError.code == 'FILE_OR_DIR_DOES_NOT_EXIST') { callback(Errors.ResourceNotFound); - } else if (err instanceof InvalidNameError) { + } else if (castedError.code == 'INVALID_NAME') { callback(Errors.IllegalArguments); } else { callback(Errors.InvalidOperation); @@ -273,16 +270,15 @@ export class TGFSFileSystem extends FileSystem { ): void { (async () => { try { - const fileRef = await list(this.tgClient)(path.toString()); - if (fileRef instanceof TGFSFileRef) { - const chunks = this.tgClient.downloadLatestVersion( - fileRef, - fileRef.name, - ); - callback(null, Readable.from(chunks)); - } else { - callback(Errors.InvalidOperation); - } + const fileRef = (await list(this.tgClient)( + path.toString(), + )) as TGFSFileRef; + const chunks = this.tgClient.downloadLatestVersion( + fileRef, + fileRef.name, + ); + + callback(null, Readable.from(chunks)); } catch (err) { handleError(callback)(err); Logger.error(err); diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 1e601eb..8c7c945 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -22,7 +22,7 @@ export class Logger { err.errors.forEach((e) => this.error(e)); } else if (err instanceof BusinessError) { console.error( - `[${this.getTime()}] [ERROR] ${err.code} ${err.name} ${err.message}`, + `[${this.getTime()}] [ERROR] ${err.code} ${err.name} ${err.message} \n${err.stack}`, ); } else if (err instanceof TechnicalError) { console.error( diff --git a/test/api/model/operations.spec.ts b/test/api/model/operations.spec.ts index 75d4e71..f9fca0f 100644 --- a/test/api/model/operations.spec.ts +++ b/test/api/model/operations.spec.ts @@ -12,12 +12,6 @@ describe('file and directory operations', () => { }); describe('create / remove directories', () => { - var client: Client; - - beforeEach(async () => { - client = await createMockClient(); - }); - it('should create a directory', async () => { const client = await createMockClient(); const root = client.getRootDirectory(); @@ -31,10 +25,10 @@ describe('file and directory operations', () => { await expect( client.createDirectory({ name: '-d1', under: root }), - ).rejects.toThrowError(); + ).rejects.toThrow(); await expect( client.createDirectory({ name: 'd/1', under: root }), - ).rejects.toThrowError(); + ).rejects.toThrow(); }); it('should remove a directory', async () => { @@ -60,7 +54,7 @@ describe('file and directory operations', () => { }); describe('create / remove files', () => { - var client: Client; + let client: Client; beforeEach(async () => { client = await createMockClient(); @@ -76,16 +70,17 @@ describe('file and directory operations', () => { }); it('should create a small file from path', async () => { - fs.writeFileSync('./mock-file.txt', 'mock-file-content'); + const fileName = `${Math.random()}.txt`; + fs.writeFileSync(fileName, 'mock-file-content'); const root = client.getRootDirectory(); const f1 = await client.uploadFile( { under: root }, - { name: 'f1', path: './mock-file.txt' }, + { name: 'f1', path: fileName }, ); expect(root.findFiles(['f1'])[0]).toEqual(f1); - fs.rmSync('./mock-file.txt'); + fs.rmSync(fileName); }); it('should create a big file from buffer', async () => { @@ -101,18 +96,19 @@ describe('file and directory operations', () => { }); it('should create a big file from path', async () => { + const fileName = `${Math.random()}.txt`; const content = Buffer.alloc(1024 * 1024 * 10, 'a'); - fs.writeFileSync('./mock-file.txt', content); + fs.writeFileSync(fileName, content); const root = client.getRootDirectory(); const f1 = await client.uploadFile( { under: root }, - { name: 'f1', path: './mock-file.txt' }, + { name: 'f1', path: fileName }, ); expect(root.findFiles(['f1'])[0]).toEqual(f1); - fs.rmSync('./mock-file.txt'); + fs.rmSync(fileName); }); it('should add a file version', async () => { @@ -122,7 +118,7 @@ describe('file and directory operations', () => { { name: 'f1', buffer: Buffer.from('mock-file-content') }, ); - await sleep(300); + await sleep(300); // wait for the timestamp to change to ensure the order of versions const content2 = 'mock-file-content-edited'; await client.uploadFile( { under: root }, @@ -224,7 +220,7 @@ describe('file and directory operations', () => { ); const fr = root.findFiles(['f1'])[0]; - const localFileName = 'test-download-file-content'; + const localFileName = `${Math.random()}.txt`; await saveToFile(client.downloadLatestVersion(fr, 'f1'), localFileName); diff --git a/test/cmd/cmd.spec.ts b/test/cmd/cmd.spec.ts index 5c0d219..ca33c1d 100644 --- a/test/cmd/cmd.spec.ts +++ b/test/cmd/cmd.spec.ts @@ -57,7 +57,7 @@ describe('commands', () => { it('should throw an error if path does not exist', () => { jest.replaceProperty(process, 'argv', ['ls', '/not-exist']); - expect(executor.execute(parse())).rejects.toThrowError(); + expect(executor.execute(parse())).rejects.toThrow(); }); }); @@ -100,12 +100,12 @@ describe('commands', () => { await executor.execute(parse()); jest.replaceProperty(process, 'argv', ['mkdir', '/d1']); - await expect(executor.execute(parse())).rejects.toThrowError(); + await expect(executor.execute(parse())).rejects.toThrow(); }); it('should throw an error if the path does not start with /', async () => { jest.replaceProperty(process, 'argv', ['mkdir', 'd1']); - await expect(executor.execute(parse())).rejects.toThrowError(); + await expect(executor.execute(parse())).rejects.toThrow(); }); }); @@ -114,8 +114,8 @@ describe('commands', () => { await removeDir(client)('/', true); }); - it('should upload a file', async () => { - const fileName = 'mock-file.txt'; + it('should upload a file from local', async () => { + const fileName = `${Math.random()}.txt`; fs.writeFileSync(fileName, 'mock-file-content'); jest.replaceProperty(process, 'argv', ['cp', fileName, '/f1']); @@ -124,12 +124,12 @@ describe('commands', () => { const f1 = client.getRootDirectory().findFiles(['f1'])[0]; expect(f1.name).toEqual('f1'); - fs.unlinkSync('./mock-file.txt'); + fs.rmSync(fileName); }); it('should throw an error if file does not exist', () => { jest.replaceProperty(process, 'argv', ['cp', 'not-exist', '/f1']); - expect(executor.execute(parse())).rejects.toThrowError(); + expect(executor.execute(parse())).rejects.toThrow(); }); }); diff --git a/test/server/webdav.spec.ts b/test/server/webdav.spec.ts index 51c1570..900de0d 100644 --- a/test/server/webdav.spec.ts +++ b/test/server/webdav.spec.ts @@ -1,6 +1,6 @@ -import request from 'supertest'; +import express from 'express'; +import supertest from 'supertest'; -import http from 'http'; import { v2 as webdav } from 'webdav-server'; import { TGFSFileSystem } from 'src/server/webdav/tgfs-filesystem'; @@ -8,124 +8,140 @@ import { TGFSFileSystem } from 'src/server/webdav/tgfs-filesystem'; import { createMockClient } from '../utils/mock-tg-client'; describe('TGFSFileSystem', () => { - let server: http.Server; - - beforeAll(async () => { + const getServer = async () => { const mockClient = await createMockClient(); const webDAVServer = new webdav.WebDAVServer(); + webDAVServer.setFileSystemSync('/', new TGFSFileSystem(mockClient)); - webDAVServer.start((httpServer) => { - server = httpServer; - }); + const app = express(); + return supertest(app.use(webdav.extensions.express('/', webDAVServer))); + }; + + beforeAll(async () => { console.info = jest.fn(); }); describe('list directory', () => { it('should list root directory', async () => { - const rsp = await request(server).propfind('/'); + const server = await getServer(); + const rsp = await server.propfind('/'); expect(rsp.statusCode).toEqual(207); }); it('should report 404 for non-exist directory', async () => { - const rsp = await request(server).propfind('/non-exist'); + const server = await getServer(); + const rsp = await server.propfind('/non-exist'); expect(rsp.statusCode).toEqual(404); }); }); describe('create directory', () => { it('should create a directory', async () => { - const rsp = await request(server).mkcol('/d1'); + const server = await getServer(); + const rsp = await server.mkcol('/d1'); expect(rsp.statusCode).toEqual(201); - const rsp2 = await request(server).propfind('/d1'); + const rsp2 = await server.propfind('/d1'); expect(rsp2.statusCode).toEqual(207); }); it('should report 405 for directory that already exists', async () => { - const rsp = await request(server).mkcol('/d2'); + const server = await getServer(); + const rsp = await server.mkcol('/d2'); expect(rsp.statusCode).toEqual(201); - const rsp2 = await request(server).mkcol('/d2'); + const rsp2 = await server.mkcol('/d2'); expect(rsp2.statusCode).toEqual(405); }); it('should report 500 if the directory name is illegal', async () => { - const rsp = await request(server).mkcol('/-d1'); + const server = await getServer(); + const rsp = await server.mkcol('/-d1'); expect(rsp.statusCode).toEqual(500); }); }); describe('delete directory', () => { it('should delete a directory', async () => { - await request(server).delete('/d1'); - const rsp = await request(server).propfind('/d1'); + const server = await getServer(); + await server.delete('/d1'); + const rsp = await server.propfind('/d1'); expect(rsp.statusCode).toEqual(404); }); it('should report 404 for non-exist directory', async () => { - const rsp = await request(server).delete('/non-exist'); + const server = await getServer(); + const rsp = await server.delete('/non-exist'); expect(rsp.statusCode).toEqual(404); }); }); - const uploadFile = (path: string, content: string) => { - return request(server) + const uploadFile = ( + server: supertest.SuperTest, + path: string, + content: string, + ) => { + return server .put(path) .set('Content-Type', 'text/plain') - .send(content); - }; - - const propfind = (path: string) => { - return request(server).propfind(path); + .send(Buffer.from(content)); }; describe('upload file', () => { - it('should upload a file', async () => { - await uploadFile('/f1', 'mock-file-content').expect(201); - const rsp = await propfind('/f1'); + it('should upload a file and show the file', async () => { + const server = await getServer(); + await uploadFile(server, '/f1', 'mock-file-content').expect(201); + const rsp = await server.propfind('/f1'); expect(rsp.statusCode).toEqual(207); - }); - - it('should show the file', async () => { - const rsp = await propfind('/'); expect(rsp.text).toEqual(expect.stringContaining('f1')); }); it('should upload a file with overwrite', async () => { - await uploadFile('/f1', 'mock-file-content'); - const rsp = await propfind('/f1'); + const server = await getServer(); + await uploadFile(server, '/f1', 'mock-file-content').expect(201); + await uploadFile(server, '/f1', 'edited-mock-file-content'); + const rsp = await server.propfind('/f1'); expect(rsp.statusCode).toEqual(207); }); it('should create an empty file', async () => { - await uploadFile('/f2', '').expect(201); - const rsp = await propfind('/f2'); + const server = await getServer(); + await uploadFile(server, '/f2', '').expect(201); + const rsp = await server.propfind('/f2'); expect(rsp.statusCode).toEqual(207); }); it('should report 409 for non-exist directory', async () => { - const rsp = await uploadFile('/non-exist/f1', 'mock-file-content'); + const server = await getServer(); + const rsp = await uploadFile( + server, + '/non-exist/f1', + 'mock-file-content', + ); expect(rsp.statusCode).toEqual(409); }); }); - // describe('download file', () => { - // it('should download a file', async () => { - // await uploadFile('/f1', 'mock-file-content'); - // const rsp = await request(server).get('/f1'); - // expect(rsp.statusCode).toEqual(200); - // expect(rsp.body.toString()).toEqual('mock-file-content'); - // }); - // }); + describe('download file', () => { + it('should download a file', async () => { + const server = await getServer(); + await uploadFile(server, '/f1', 'mock-file-content').expect(201); + await server.get('/f1').expect(200); + + // TODO: I don't know how to receive the data + }); + }); describe('delete file', () => { it('should delete a file', async () => { - await request(server).delete('/f1'); - const rsp = await request(server).propfind('/f1'); + const server = await getServer(); + await server.delete('/f1'); + const rsp = await server.propfind('/f1'); expect(rsp.statusCode).toEqual(404); }); it('should report 404 for non-exist file', async () => { - const rsp = await request(server).delete('/non-exist'); + const server = await getServer(); + const rsp = await server.delete('/non-exist'); expect(rsp.statusCode).toEqual(404); }); }); diff --git a/test/utils/mock-gramjs-api.ts b/test/utils/mock-gramjs-api.ts new file mode 100644 index 0000000..5c22e57 --- /dev/null +++ b/test/utils/mock-gramjs-api.ts @@ -0,0 +1,87 @@ +import { ITDLibClient } from 'src/api/interface'; +import * as types from 'src/api/types'; + +import { MockMessages } from './mock-messages'; + +export class MockGramJSApi implements ITDLibClient { + constructor(private messages: MockMessages) {} + + public async getMessages( + req: types.GetMessagesReq, + ): Promise { + return req.messageIds.map((messageId) => { + const message = this.messages.getMessage(messageId); + + return { + messageId, + text: message.text, + document: message.document, + }; + }); + } + + public async searchMessages( + req: types.SearchMessagesReq, + ): Promise { + const messages = this.messages.search(req.search); + return messages.map((message) => { + return { + messageId: message.id, + text: message.text, + }; + }); + } + + public async getPinnedMessages( + req: types.GetPinnedMessagesReq, + ): Promise { + if (!this.messages.pinnedMessageId) { + return []; + } + const message = this.messages.getMessage(this.messages.pinnedMessageId); + return [ + { + messageId: message.id, + text: message.text, + }, + ]; + } + + public async saveBigFilePart( + req: types.SaveBigFilePartReq, + ): Promise { + return this.messages.saveFilePart(req.fileId, req.filePart, req.bytes); + } + + public async saveFilePart( + req: types.SaveFilePartReq, + ): Promise { + return this.messages.saveFilePart(req.fileId, req.filePart, req.bytes); + } + + public async sendBigFile( + req: types.SendFileReq, + ): Promise { + const messageId = this.messages.sendMessage({ file: req.file.id }); + return { messageId }; + } + + public async sendSmallFile( + req: types.SendFileReq, + ): Promise { + const messageId = this.messages.sendMessage({ file: req.file.id }); + return { messageId }; + } + + public downloadFile(req: types.DownloadFileReq): types.DownloadFileResp { + const message = this.messages.getMessage(req.messageId); + const fileId = message.document.id; + const parts = this.messages.getFile(fileId); + return (async function* () { + const len = Object.keys(parts).length; + for (let i = 0; i < len; i++) { + yield parts[i]; + } + })(); + } +} diff --git a/test/utils/mock-messages.spec.ts b/test/utils/mock-messages.spec.ts new file mode 100644 index 0000000..f6bddfd --- /dev/null +++ b/test/utils/mock-messages.spec.ts @@ -0,0 +1,58 @@ +import bigInt from 'big-integer'; + +import { MockMessages } from './mock-messages'; + +describe('test mock messages', () => { + it('should send and get message', () => { + const mockMessages = new MockMessages(); + const messageId = mockMessages.sendMessage({ message: 'hello' }); + const message = mockMessages.getMessage(messageId); + expect(message.text).toEqual('hello'); + }); + + it('should increase message id after sending message', () => { + const mockMessages = new MockMessages(); + const messageId1 = mockMessages.sendMessage({ message: 'hello' }); + const messageId2 = mockMessages.sendMessage({ message: 'world' }); + expect(messageId2).toBeGreaterThan(messageId1); + }); + + it('should send and get message with file', () => { + const mockMessages = new MockMessages(); + const fileId = bigInt(123); + const messageId = mockMessages.sendMessage({ + message: 'hello', + file: fileId, + }); + const message = mockMessages.getMessage(messageId); + expect(message.document.id).toEqual(fileId); + }); + + it('should save and get file part', () => { + const mockMessages = new MockMessages(); + const fileId = bigInt(123); + const data = Buffer.from('hello'); + mockMessages.saveFilePart(fileId, 0, data); + const parts = mockMessages.getFile(fileId); + expect(Object.keys(parts).length).toEqual(1); + expect(parts[0]).toEqual(data); + }); + + it('should edit a message', () => { + const mockMessages = new MockMessages(); + const messageId = mockMessages.sendMessage({ message: 'hello' }); + mockMessages.editMessage(messageId, { message: 'world' }); + const message = mockMessages.getMessage(messageId); + expect(message.text).toEqual('world'); + }); + + it('should edit a message with file', () => { + const mockMessages = new MockMessages(); + const fileId = bigInt(123); + const messageId = mockMessages.sendMessage({ file: fileId }); + const fileId2 = bigInt(456); + mockMessages.editMessage(messageId, { file: fileId2 }); + const message = mockMessages.getMessage(messageId); + expect(message.document.id).toEqual(fileId2); + }); +}); diff --git a/test/utils/mock-messages.ts b/test/utils/mock-messages.ts index c83b2bc..9ddde30 100644 --- a/test/utils/mock-messages.ts +++ b/test/utils/mock-messages.ts @@ -1,21 +1,23 @@ -import { BigInteger } from 'big-integer'; +import bigInt from 'big-integer'; -type InputMessage = { file?: BigInteger; message?: string }; +import * as types from 'src/api/types'; + +type InputMessage = { file?: bigInt.BigInteger; message?: string }; type Message = { id: number; text?: string; - document?: { id: BigInteger }; + document?: types.Document; }; export class MockMessages { - messages: { - [key: number]: Message; - } = {}; - messageId: number = 1; - pinnedMessageId: number; - fileParts: { [fileId: string]: { [part: number]: Buffer } } = {}; + constructor( + public readonly messages: { [key: number]: Message } = {}, + public messageId: number = 1, + public pinnedMessageId?: number, + public fileParts: { [fileId: string]: { [part: number]: Buffer } } = {}, + ) {} - saveFilePart(fileId: BigInteger, part: number, data: Buffer) { + saveFilePart(fileId: bigInt.BigInteger, part: number, data: Buffer) { if (!this.fileParts[String(fileId)]) { this.fileParts[String(fileId)] = {}; } @@ -23,6 +25,19 @@ export class MockMessages { return { success: true }; } + private fileIdToDocument(fileId: bigInt.BigInteger): types.Document { + const fileParts = this.fileParts[String(fileId)]; + const size = fileParts + ? Object.values(fileParts).reduce((acc, part) => acc + part.length, 0) + : 0; + return { + id: fileId, + size: bigInt(size), + accessHash: bigInt(0), + fileReference: Buffer.from(''), + }; + } + sendMessage(msg: InputMessage) { const { file, message } = msg; @@ -31,9 +46,7 @@ export class MockMessages { this.messages[messageId] = { id: messageId, text: message, - document: { - id: file, - }, + document: file ? this.fileIdToDocument(file) : undefined, }; return messageId; } @@ -47,7 +60,7 @@ export class MockMessages { }; } - getFile(fileId: BigInteger) { + getFile(fileId: bigInt.BigInteger) { return this.fileParts[String(fileId)]; } @@ -60,13 +73,13 @@ export class MockMessages { }; } if (file) { - this.messages[messageId].document = { id: file }; + this.messages[messageId].document = this.fileIdToDocument(file); } } search(kw: string) { - return Object.values(this.messages).filter( - (msg: any) => msg.text?.includes(kw), + return Object.values(this.messages).filter((msg: any) => + msg.text?.includes(kw), ); } } diff --git a/test/utils/mock-telegraf-api.ts b/test/utils/mock-telegraf-api.ts new file mode 100644 index 0000000..ad44d6b --- /dev/null +++ b/test/utils/mock-telegraf-api.ts @@ -0,0 +1,37 @@ +import { IBot } from 'src/api/interface'; +import * as types from 'src/api/types'; +import { generateFileId } from 'src/api/utils'; + +import { MockMessages } from './mock-messages'; + +export class MockTelegrafApi implements IBot { + constructor(private messages: MockMessages) {} + + public async sendText(req: types.SendTextReq): Promise { + const messageId = this.messages.sendMessage({ message: req.text }); + return { messageId }; + } + + public async editMessageText( + req: types.EditMessageTextReq, + ): Promise { + this.messages.editMessage(req.messageId, { message: req.text }); + return { messageId: req.messageId }; + } + + public async editMessageMedia( + req: types.EditMessageMediaReq, + ): Promise { + const fileId = generateFileId(); + this.messages.saveFilePart(fileId, 0, req.buffer); + + this.messages.editMessage(req.messageId, { + file: fileId, + }); + return { messageId: req.messageId }; + } + + public async pinMessage(req: types.PinMessageReq): Promise { + this.messages.pinnedMessageId = req.messageId; + } +} diff --git a/test/utils/mock-tg-client.ts b/test/utils/mock-tg-client.ts index 24cbbc8..9895ffe 100644 --- a/test/utils/mock-tg-client.ts +++ b/test/utils/mock-tg-client.ts @@ -1,186 +1,45 @@ -import { Api, TelegramClient } from 'telegram'; -import { IterDownloadFunction } from 'telegram/client/downloads'; -import { EntityLike } from 'telegram/define'; - -import { Telegram } from 'telegraf'; - -import { createClient } from 'src/api'; -import { generateFileId } from 'src/api/utils'; -import { Config } from 'src/config'; - +import { MockGramJSApi } from './mock-gramjs-api'; import { MockMessages } from './mock-messages'; +import { MockTelegrafApi } from './mock-telegraf-api'; -let mockMessages: MockMessages = null; - -jest.mock('src/config', () => { - return { - ...jest.requireActual('src/config'), - config: { - telegram: { - private_file_channel: 'mock-private-file-channel', - }, - tgfs: { - download: { - chunk_size_kb: 1024, - }, - }, - } as Partial, - }; -}); - -jest.mock('src/api/impl/gramjs', () => { - return { - ...jest.requireActual('src/api/impl/gramjs'), - loginAsAccount: jest.fn().mockImplementation(async () => { - return new TelegramClient('mock-session', 0, 'mock-api-hash', {}); - }), - loginAsBot: jest.fn().mockImplementation(async () => { - return new TelegramClient('mock-session', 0, 'mock-api-hash', {}); - }), - }; -}); - -jest.mock('src/api/impl/telegraf', () => { - return { - ...jest.requireActual('src/api/impl/telegraf'), - createBot: jest.fn().mockImplementation(() => { - return new Telegram('mock-token'); - }), - }; -}); - -jest.mock('telegram', () => { - return { - ...jest.requireActual('telegram'), - TelegramClient: jest - .fn() - .mockImplementation( - (session: string, apiId: number, apiHash: string, options: any) => { - return { - getMessages: jest - .fn() - .mockImplementation((channelId: string, options: any) => { - const { filter, ids, search } = options; - if (filter instanceof Api.InputMessagesFilterPinned) { - return mockMessages.pinnedMessageId - ? [mockMessages.getMessage(mockMessages.pinnedMessageId)] - : []; - } - if (search) { - return mockMessages.search(search); - } - return ids.map((id: number) => mockMessages.getMessage(id)); - }), - iterDownload: jest - .fn() - .mockImplementation((iterFileParams: IterDownloadFunction) => { - const { file: fileLoc } = iterFileParams; - const fileParts = mockMessages.getFile( - (fileLoc as Api.InputDocumentFileLocation).id, - ); - mockMessages; - let done = false; - let i = 0; - return { - [Symbol.asyncIterator]() { - return { - next() { - const res = Promise.resolve({ - value: fileParts[i++], - done, - }); - done = !done; - return res; - }, - return() { - return { done: true }; - }, - }; - }, - }; - }), +export const createMockClient = async () => { + jest.resetModules(); - sendFile: jest - .fn() - .mockImplementation( - (entity: EntityLike, { file: { id: fileId } }) => { - const id = mockMessages.sendMessage({ - file: fileId, - }); - return { id }; - }, - ), + const mockMessages = new MockMessages(); - invoke: jest.fn().mockImplementation((req: any) => { - if ( - req.className === 'upload.SaveFilePart' || - req.className === 'upload.SaveBigFilePart' - ) { - return mockMessages.saveFilePart( - req.fileId, - req.filePart, - req.bytes, - ); - } - }), - }; + jest.mock('src/config', () => { + return { + config: { + telegram: { + private_file_channel: '114514', }, - ), - }; -}); -jest.mock('telegraf', () => { - return { - Telegram: jest.fn().mockImplementation((botToken: string) => { - return { - sendMessage: jest - .fn() - .mockImplementation((chatId: string, text: string) => { - const messageId = mockMessages.sendMessage({ message: text }); - return { message_id: messageId }; - }), - editMessageText: jest - .fn() - .mockImplementation( - ( - chatId: string, - messageId: number, - inlineMessageId, - text: string, - ) => { - mockMessages.editMessage(messageId, { message: text }); - }, - ), - editMessageMedia: jest.fn().mockImplementation( - ( - chatId: string, - messageId: number, - inlineMessageId: undefined | string, - media: { - type: 'document'; - media: { - source: Buffer; - }; - }, - ) => { - const fileId = generateFileId(); - mockMessages.saveFilePart(fileId, 0, media.media.source); - mockMessages.editMessage(messageId, { - file: fileId, - }); + tgfs: { + download: { + chunk_size_kb: 1024, }, - ), - pinChatMessage: jest - .fn() - .mockImplementation((chatId: string, messageId: number) => { - mockMessages.pinnedMessageId = messageId; - }), - }; - }), - }; -}); - -export const createMockClient = async () => { - mockMessages = new MockMessages(); - const client = await createClient(); - return client; + }, + }, + }; + }); + jest.mock('src/api/impl/gramjs', () => { + return { + GramJSApi: jest + .fn() + .mockImplementation(() => new MockGramJSApi(mockMessages)), + loginAsAccount: jest.fn(), + loginAsBot: jest.fn(), + }; + }); + jest.mock('src/api/impl/telegraf', () => { + return { + TelegrafApi: jest + .fn() + .mockImplementation(() => new MockTelegrafApi(mockMessages)), + createBot: jest.fn().mockImplementation(() => null), + }; + }); + + const { createClient } = require('src/api'); + + return await createClient(); };