test(client): test coverage for client utils
This commit is contained in:
parent
dbe5ff190e
commit
4a027d5498
14 changed files with 567 additions and 47 deletions
|
@ -37,5 +37,8 @@
|
|||
"dependencies": {
|
||||
"@mtcute/core": "workspace:^",
|
||||
"@mtcute/file-id": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mtcute/test": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
|
64
packages/client/src/utils/file-type.test.ts
Normal file
64
packages/client/src/utils/file-type.test.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { guessFileMime } from './file-type.js'
|
||||
import { hexDecodeToBuffer } from './index.js'
|
||||
|
||||
describe('guessFileMime', () => {
|
||||
it.each([
|
||||
['424d', 'image/bmp'],
|
||||
['4d5a', 'application/x-msdownload'],
|
||||
['1f9d', 'application/x-compress'],
|
||||
['1fa0', 'application/x-compress'],
|
||||
['1f8b', 'application/gzip'],
|
||||
['425a68', 'application/x-bzip2'],
|
||||
['494433', 'audio/mpeg'],
|
||||
['fffb', 'audio/mpeg'],
|
||||
['fff3', 'audio/mpeg'],
|
||||
['fff2', 'audio/mpeg'],
|
||||
['504b0304', 'application/zip'],
|
||||
['38425053', 'image/vnd.adobe.photoshop'],
|
||||
['7f454c46', 'application/x-elf'],
|
||||
['feedfacf', 'application/x-mach-binary'],
|
||||
['28b52ffd', 'application/zstd'],
|
||||
['664c6143', 'audio/x-flac'],
|
||||
['ffd8ffdb', 'image/jpeg'],
|
||||
['ffd8ffe0', 'image/jpeg'],
|
||||
['ffd8ffee', 'image/jpeg'],
|
||||
['ffd8ffe1', 'image/jpeg'],
|
||||
['4f676753', 'application/ogg'],
|
||||
['4f6767530000000000000000000000000000000000000000000000004f70757348656164', 'audio/ogg'],
|
||||
['4f67675300000000000000000000000000000000000000000000000001766964656f', 'video/ogg'],
|
||||
['4f6767530000000000000000000000000000000000000000000000007f464c4143', 'audio/ogg'],
|
||||
['4f67675300000000000000000000000000000000000000000000000001766f72626973', 'audio/ogg'],
|
||||
['255044462d', 'application/pdf'],
|
||||
['474946383761', 'image/gif'],
|
||||
['474946383961', 'image/gif'],
|
||||
['377abcaf271c', 'application/x-7z-compressed'],
|
||||
['89504e470d0a1a0a', 'image/png'],
|
||||
['526172211a0700', 'application/x-rar-compressed'],
|
||||
['526172211a0701', 'application/x-rar-compressed'],
|
||||
['000000006674797061766966', 'image/avif'],
|
||||
['000000006674797061766973', 'image/avif'],
|
||||
['00000000667479706d696631', 'image/heif'],
|
||||
['000000006674797068656963', 'image/heic'],
|
||||
['000000006674797068656978', 'image/heic'],
|
||||
['000000006674797068657663', 'image/heic-sequence'],
|
||||
['000000006674797068657678', 'image/heic-sequence'],
|
||||
['000000006674797071740000', 'video/quicktime'],
|
||||
['00000000667479704d345600', 'video/x-m4v'],
|
||||
['00000000667479704d345648', 'video/x-m4v'],
|
||||
['00000000667479704d345650', 'video/x-m4v'],
|
||||
['00000000667479704d345000', 'video/mp4'],
|
||||
['00000000667479704d344100', 'audio/x-m4a'],
|
||||
['00000000667479704d344200', 'audio/mp4'],
|
||||
['000000006674797046344100', 'audio/mp4'],
|
||||
['000000006674797046344200', 'audio/mp4'],
|
||||
['000000006674797063727800', 'image/x-canon-cr3'],
|
||||
['000000006674797033673200', 'video/3gpp2'],
|
||||
['000000006674797033670000', 'video/3gpp'],
|
||||
])('should detect %s as %s', (header, mime) => {
|
||||
header += '00'.repeat(16)
|
||||
|
||||
expect(guessFileMime(hexDecodeToBuffer(header))).toEqual(mime)
|
||||
})
|
||||
})
|
|
@ -76,13 +76,13 @@ export function guessFileMime(chunk: Uint8Array): string | null {
|
|||
}
|
||||
|
||||
if (b0 === 0x52 && b1 === 0x61 && b2 === 0x72 && b3 === 0x21 && b4 === 0x1a && b5 === 0x07) {
|
||||
if (chunk[6] === 0x00 || chunk[6] === 0x01) return 'application/x-rar-compressed'
|
||||
if (b6 === 0x00 || b6 === 0x01) return 'application/x-rar-compressed'
|
||||
}
|
||||
|
||||
// ftyp - iso container
|
||||
if (b4 === 0x66 && b5 === 0x74 && b6 === 0x79 && b7 === 0x70 && chunk[8] & 0x60) {
|
||||
const brandMajor = String.fromCharCode(...chunk.subarray(8, 12))
|
||||
.replace('\0', ' ')
|
||||
.replace(/\0/g, ' ')
|
||||
.trim()
|
||||
|
||||
switch (brandMajor) {
|
||||
|
@ -105,18 +105,10 @@ export function guessFileMime(chunk: Uint8Array): string | null {
|
|||
case 'M4VH':
|
||||
case 'M4VP':
|
||||
return 'video/x-m4v'
|
||||
case 'M4P':
|
||||
return 'video/mp4'
|
||||
case 'M4B':
|
||||
return 'audio/mp4'
|
||||
case 'M4A':
|
||||
return 'audio/x-m4a'
|
||||
case 'F4V':
|
||||
return 'video/mp4'
|
||||
case 'F4P':
|
||||
return 'video/mp4'
|
||||
case 'M4B':
|
||||
case 'F4A':
|
||||
return 'audio/mp4'
|
||||
case 'F4B':
|
||||
return 'audio/mp4'
|
||||
case 'crx':
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { hexDecodeToBuffer, utf8EncodeToBuffer } from '@mtcute/core/utils.js'
|
||||
import { hexDecodeToBuffer, hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/core/utils.js'
|
||||
|
||||
import { isProbablyPlainText } from './file-utils.js'
|
||||
import {
|
||||
extractFileName,
|
||||
inflateSvgPath,
|
||||
isProbablyPlainText,
|
||||
strippedPhotoToJpg,
|
||||
svgPathToFile,
|
||||
} from './file-utils.js'
|
||||
|
||||
describe('isProbablyPlainText', () => {
|
||||
it('should return true for buffers only containing printable ascii', () => {
|
||||
|
@ -28,3 +34,125 @@ describe('isProbablyPlainText', () => {
|
|||
expect(isProbablyPlainText(hexDecodeToBuffer('20e8e218e54254c813b261432b0330d7'))).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractFileName', () => {
|
||||
it('should extract file name from a path', () => {
|
||||
expect(extractFileName('file.txt')).toEqual('file.txt')
|
||||
expect(extractFileName('/home/user/file.txt')).toEqual('file.txt')
|
||||
expect(extractFileName('C:\\Users\\user\\file.txt')).toEqual('file.txt')
|
||||
})
|
||||
|
||||
it('should skip file: prefix', () => {
|
||||
expect(extractFileName('file:file.txt')).toEqual('file.txt')
|
||||
expect(extractFileName('file:/home/user/file.txt')).toEqual('file.txt')
|
||||
expect(extractFileName('file:C:\\Users\\user\\file.txt')).toEqual('file.txt')
|
||||
})
|
||||
})
|
||||
|
||||
describe('svgPathToFile', () => {
|
||||
it('should convert SVG path to a file', () => {
|
||||
const path = 'M 0 0 L 100 0 L 100 100 L 0 100 L 0 0 Z'
|
||||
|
||||
expect(utf8Decode(svgPathToFile(path))).toMatchInlineSnapshot(
|
||||
'"<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?><svg version=\\"1.1\\" xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\"viewBox=\\"0 0 512 512\\" xml:space=\\"preserve\\"><path d=\\"M 0 0 L 100 0 L 100 100 L 0 100 L 0 0 Z\\"/></svg>"',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('inflateSvgPath', () => {
|
||||
const data = hexDecodeToBuffer(
|
||||
'1a05b302dc5f4446068649064247424a6a4c704550535b5e665e5e4c044a024c' +
|
||||
'074e06414d80588863935fad74be4704854684518b528581904695498b488b56' +
|
||||
'965c85438d8191818543894a8f4d834188818a4284498454895d9a6f86074708' +
|
||||
'8d0146089283a281b48bbfa386078a04880390098490949997ab828a4b984a9d' +
|
||||
'8490b395aa9949845c485d4a42434b4a4a46848a8792859d4186468c48938182' +
|
||||
'91a293ac859781af848701818989928b9c849086a785b880816c8071817c814c' +
|
||||
'0080520081',
|
||||
)
|
||||
const path =
|
||||
'M265,512c-31-4-66,6-96-2-7-2-10-42-12-48-5-16-19-27-30' +
|
||||
'-38-30-30-124-102-127-146-1-13,0-24,8-35,19-31,45-52,62-74,5-6,4-17,11-18,' +
|
||||
'5,1,16-6,21-9,11-8,11-22,22-28,5-3,13,1,17,1,5-3,9-10,15-13,3-1,8,1,10-2,4' +
|
||||
'-9,4-20,9-29,26-47,67-78,131-68,18,3,34,1,52,11,63,35,67,104,83,169,4,16,20,' +
|
||||
'25,23,43,2,10-11,24-10,29,4,16,51,21,42,25-9,4-28-8-29-10-2-3-11-10-10-6,4,10,' +
|
||||
'7,18,5,29-1,6-6,12-8,19,1,2,17,34,19,44,5,23,1,47,4,71,1,9,9,18,11,28,4,16,6,39,' +
|
||||
'5,56,0,1-44,0-49,1-60,1-120,0-180,1z'
|
||||
|
||||
it('should correctly inflate svg path', () => {
|
||||
expect(inflateSvgPath(data)).toEqual(path)
|
||||
})
|
||||
})
|
||||
|
||||
describe('strippedPhotoToJpg', () => {
|
||||
// strippedThumb of @Channel_Bot
|
||||
const dataPfp = hexDecodeToBuffer('010808b1f2f95fed673451457033ad1f')
|
||||
// photoStrippedSize of a random image
|
||||
const dataPicture = hexDecodeToBuffer(
|
||||
'012728b532aacce4b302d8c1099c74a634718675cb6381f73d3ffd557667d9b5' +
|
||||
'816f4c28ce69aa58a863238cf62a334590f999042234cbe1986d03eefe14c68e' +
|
||||
'32847cc00ce709ea7ffad577773f78fe54d6c927f78c3db14ac1ccca91a2ef4f' +
|
||||
'9d89dd9e53e9455c456072646618e840a28b20bb223275e463b55769b07e4cf1' +
|
||||
'c52cedfbb03d38aab9e718356909b2733c839cf5a72dc3646ee6a4bb882c2ac0' +
|
||||
'70a31c554c1d81f0403eb4d598b5350b8680b03c628aab09ff0044707b1a2a5a' +
|
||||
'012e016420753cd25b491ac603a723bd14517ba28b46788c5e613f27d2a9cb32' +
|
||||
'ceca2353807bd14525b831e6175deccde991eb45145508',
|
||||
)
|
||||
|
||||
it('should inflate stripped jpeg (from profile picture)', () => {
|
||||
expect(hexEncode(strippedPhotoToJpg(dataPfp))).toMatchInlineSnapshot(
|
||||
'"ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e192' +
|
||||
'82321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a' +
|
||||
'0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2' +
|
||||
'd2d3c353c76414176f8a58ca5f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f' +
|
||||
'8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8ffc' +
|
||||
'00011080008000803012200021101031101ffc4001f000001050101010101010' +
|
||||
'0000000000000000102030405060708090a0bffc400b51000020103030204030' +
|
||||
'50504040000017d01020300041105122131410613516107227114328191a1082' +
|
||||
'342b1c11552d1f02433627282090a161718191a25262728292a3435363738393' +
|
||||
'a434445464748494a535455565758595a636465666768696a737475767778797' +
|
||||
'a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b' +
|
||||
'7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf' +
|
||||
'1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000' +
|
||||
'102030405060708090a0bffc400b511000201020404030407050404000102770' +
|
||||
'00102031104052131061241510761711322328108144291a1b1c109233352f01' +
|
||||
'56272d10a162434e125f11718191a262728292a35363738393a4344454647484' +
|
||||
'94a535455565758595a636465666768696a737475767778797a8283848586878' +
|
||||
'8898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c' +
|
||||
'4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f' +
|
||||
'9faffda000c03010002110311003f00b1f2f95fed673451457033ad1fffd9"',
|
||||
)
|
||||
})
|
||||
|
||||
it('should inflate stripped jpeg (from a picture)', () => {
|
||||
expect(hexEncode(strippedPhotoToJpg(dataPicture))).toMatchInlineSnapshot(
|
||||
'"ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e192' +
|
||||
'82321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a' +
|
||||
'0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2' +
|
||||
'd2d3c353c76414176f8a58ca5f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f' +
|
||||
'8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8ffc' +
|
||||
'00011080027002803012200021101031101ffc4001f000001050101010101010' +
|
||||
'0000000000000000102030405060708090a0bffc400b51000020103030204030' +
|
||||
'50504040000017d01020300041105122131410613516107227114328191a1082' +
|
||||
'342b1c11552d1f02433627282090a161718191a25262728292a3435363738393' +
|
||||
'a434445464748494a535455565758595a636465666768696a737475767778797' +
|
||||
'a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b' +
|
||||
'7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf' +
|
||||
'1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000' +
|
||||
'102030405060708090a0bffc400b511000201020404030407050404000102770' +
|
||||
'00102031104052131061241510761711322328108144291a1b1c109233352f01' +
|
||||
'56272d10a162434e125f11718191a262728292a35363738393a4344454647484' +
|
||||
'94a535455565758595a636465666768696a737475767778797a8283848586878' +
|
||||
'8898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c' +
|
||||
'4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f' +
|
||||
'9faffda000c03010002110311003f00b532aacce4b302d8c1099c74a63471867' +
|
||||
'5cb6381f73d3ffd557667d9b5816f4c28ce69aa58a863238cf62a334590f9990' +
|
||||
'42234cbe1986d03eefe14c68e32847cc00ce709ea7ffad577773f78fe54d6c92' +
|
||||
'7f78c3db14ac1ccca91a2ef4f9d89dd9e53e9455c456072646618e840a28b20b' +
|
||||
'b223275e463b55769b07e4cf1c52cedfbb03d38aab9e718356909b2733c839cf' +
|
||||
'5a72dc3646ee6a4bb882c2ac070a31c554c1d81f0403eb4d598b5350b8680b03' +
|
||||
'c628aab09ff0044707b1a2a5a012e016420753cd25b491ac603a723bd14517ba' +
|
||||
'28b46788c5e613f27d2a9cb32ceca2353807bd14525b831e6175deccde991eb4' +
|
||||
'5145508ffd9"',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -59,7 +59,7 @@ const JPEG_FOOTER = new Uint8Array([0xff, 0xd9])
|
|||
*/
|
||||
export function strippedPhotoToJpg(stripped: Uint8Array): Uint8Array {
|
||||
if (stripped.length < 3 || stripped[0] !== 1) {
|
||||
return stripped
|
||||
throw new MtArgumentError('Invalid stripped JPEG')
|
||||
}
|
||||
|
||||
const result = concatBuffers([JPEG_HEADER, stripped.slice(3), JPEG_FOOTER])
|
||||
|
@ -96,6 +96,8 @@ export function inflateSvgPath(encoded: Uint8Array): string {
|
|||
}
|
||||
}
|
||||
|
||||
path += 'z'
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
|
|
49
packages/client/src/utils/inline-utils.test.ts
Normal file
49
packages/client/src/utils/inline-utils.test.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { Long, tl } from '@mtcute/core'
|
||||
|
||||
import { encodeInlineMessageId, normalizeInlineId, parseInlineMessageId } from './inline-utils.js'
|
||||
|
||||
describe('inline message id', () => {
|
||||
it('should encode and decode legacy inline message id', () => {
|
||||
const id: tl.RawInputBotInlineMessageID = {
|
||||
_: 'inputBotInlineMessageID',
|
||||
dcId: 1,
|
||||
id: Long.fromBits(123, 456),
|
||||
accessHash: Long.fromBits(789, 999),
|
||||
}
|
||||
const encoded = encodeInlineMessageId(id)
|
||||
const parsed = parseInlineMessageId(encoded)
|
||||
|
||||
expect(encoded).toEqual('AQAAAHsAAADIAQAAFQMAAOcDAAA')
|
||||
expect(parsed).toEqual(id)
|
||||
})
|
||||
|
||||
it('should encode and decode 64-bit inline message id', () => {
|
||||
const id: tl.RawInputBotInlineMessageID64 = {
|
||||
_: 'inputBotInlineMessageID64',
|
||||
dcId: 1,
|
||||
ownerId: Long.fromBits(123, 456),
|
||||
id: 666,
|
||||
accessHash: Long.fromBits(789, 999),
|
||||
}
|
||||
const encoded = encodeInlineMessageId(id)
|
||||
const parsed = parseInlineMessageId(encoded)
|
||||
|
||||
expect(encoded).toEqual('AQAAAHsAAADIAQAAmgIAABUDAADnAwAA')
|
||||
expect(parsed).toEqual(id)
|
||||
})
|
||||
|
||||
it('should normalize to tl object', () => {
|
||||
const id: tl.RawInputBotInlineMessageID64 = {
|
||||
_: 'inputBotInlineMessageID64',
|
||||
dcId: 1,
|
||||
ownerId: Long.fromBits(123, 456),
|
||||
id: 666,
|
||||
accessHash: Long.fromBits(789, 999),
|
||||
}
|
||||
|
||||
expect(normalizeInlineId('AQAAAHsAAADIAQAAmgIAABUDAADnAwAA')).toEqual(id)
|
||||
expect(normalizeInlineId(id)).toBe(id)
|
||||
})
|
||||
})
|
112
packages/client/src/utils/inspectable.test.ts
Normal file
112
packages/client/src/utils/inspectable.test.ts
Normal file
|
@ -0,0 +1,112 @@
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { makeInspectable } from './inspectable.js'
|
||||
|
||||
describe('makeInspectable', () => {
|
||||
// eslint-disable-next-line
|
||||
const inspect = (obj: any) => obj.toJSON()
|
||||
|
||||
it('should make all getters inspectable', () => {
|
||||
class Foo {
|
||||
get foo() {
|
||||
return 1
|
||||
}
|
||||
get bar() {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
makeInspectable(Foo)
|
||||
|
||||
expect(inspect(new Foo())).toEqual({ foo: 1, bar: 2 })
|
||||
})
|
||||
|
||||
it('should use nested classes toJSON', () => {
|
||||
class Inner {
|
||||
toJSON = vi.fn().mockReturnValue(42)
|
||||
}
|
||||
const inner = new Inner()
|
||||
|
||||
class Foo {
|
||||
get foo() {
|
||||
return inner
|
||||
}
|
||||
}
|
||||
|
||||
makeInspectable(Foo)
|
||||
|
||||
expect(inspect(new Foo())).toEqual({ foo: 42 })
|
||||
expect(inner.toJSON).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not inspect fields', () => {
|
||||
class Foo {
|
||||
get foo() {
|
||||
return 1
|
||||
}
|
||||
bar = 2
|
||||
}
|
||||
|
||||
makeInspectable(Foo)
|
||||
|
||||
expect(inspect(new Foo())).toEqual({ foo: 1 })
|
||||
})
|
||||
|
||||
it('should inspect fields if specified', () => {
|
||||
class Foo {
|
||||
bar = 1
|
||||
baz = 2
|
||||
}
|
||||
|
||||
makeInspectable(Foo, ['bar'])
|
||||
|
||||
expect(inspect(new Foo())).toEqual({ bar: 1 })
|
||||
})
|
||||
|
||||
it('should hide getters if specified', () => {
|
||||
class Foo {
|
||||
get foo() {
|
||||
return 1
|
||||
}
|
||||
get bar() {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
makeInspectable(Foo, undefined, ['foo'])
|
||||
|
||||
expect(inspect(new Foo())).toEqual({ bar: 2 })
|
||||
})
|
||||
|
||||
it('should handle errors', () => {
|
||||
class Foo {
|
||||
get foo() {
|
||||
return 1
|
||||
}
|
||||
get bar() {
|
||||
throw new Error('whatever')
|
||||
}
|
||||
}
|
||||
|
||||
makeInspectable(Foo)
|
||||
|
||||
expect(inspect(new Foo())).toEqual({
|
||||
foo: 1,
|
||||
bar: 'Error: whatever',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle Uint8Arrays', () => {
|
||||
class Foo {
|
||||
get foo() {
|
||||
return new Uint8Array([1, 2, 3])
|
||||
}
|
||||
}
|
||||
|
||||
makeInspectable(Foo)
|
||||
|
||||
expect(inspect(new Foo())).toEqual({
|
||||
foo: 'AQID',
|
||||
})
|
||||
})
|
||||
})
|
|
@ -25,10 +25,6 @@ function getAllGettersNames<T>(obj: T): (keyof T)[] {
|
|||
return getters
|
||||
}
|
||||
|
||||
const bufferToJsonInspect = function (this: Uint8Array) {
|
||||
return base64Encode(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Small helper function that adds `toJSON` and `util.custom.inspect`
|
||||
* methods to a given class based on its getters
|
||||
|
@ -48,18 +44,18 @@ export function makeInspectable<T>(obj: new (...args: any[]) => T, props?: (keyo
|
|||
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
||||
const proto = new Function(`return function ${obj.name}(){}`)().prototype
|
||||
|
||||
obj.prototype.toJSON = function (nested = false) {
|
||||
if (!nested) {
|
||||
(Uint8Array as any).toJSON = bufferToJsonInspect
|
||||
}
|
||||
|
||||
obj.prototype.toJSON = function () {
|
||||
const ret: any = Object.create(proto)
|
||||
getters.forEach((it) => {
|
||||
try {
|
||||
let val = this[it]
|
||||
|
||||
if (val && typeof val === 'object' && typeof val.toJSON === 'function') {
|
||||
val = val.toJSON(true)
|
||||
if (val && typeof val === 'object') {
|
||||
if (val instanceof Uint8Array) {
|
||||
val = base64Encode(val)
|
||||
} else if (typeof val.toJSON === 'function') {
|
||||
val = val.toJSON(true)
|
||||
}
|
||||
}
|
||||
ret[it] = val
|
||||
} catch (e: any) {
|
||||
|
@ -67,10 +63,6 @@ export function makeInspectable<T>(obj: new (...args: any[]) => T, props?: (keyo
|
|||
}
|
||||
})
|
||||
|
||||
if (!nested) {
|
||||
delete (Uint8Array as any).prototype.toJSON
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return ret
|
||||
}
|
||||
|
|
146
packages/client/src/utils/peer-utils.test.ts
Normal file
146
packages/client/src/utils/peer-utils.test.ts
Normal file
|
@ -0,0 +1,146 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { createStub } from '@mtcute/test'
|
||||
|
||||
import { MtInvalidPeerTypeError } from '../types/index.js'
|
||||
import {
|
||||
inputPeerToPeer,
|
||||
isInputPeerChannel,
|
||||
isInputPeerChat,
|
||||
isInputPeerUser,
|
||||
normalizeToInputChannel,
|
||||
normalizeToInputPeer,
|
||||
normalizeToInputUser,
|
||||
} from './peer-utils.js'
|
||||
|
||||
describe('normalizeToInputPeer', () => {
|
||||
it.each([
|
||||
['inputChannelEmpty', 'inputPeerEmpty'],
|
||||
['inputUserEmpty', 'inputPeerEmpty'],
|
||||
['inputUser', 'inputPeerUser'],
|
||||
['inputUserSelf', 'inputPeerSelf'],
|
||||
['inputUserSelf', 'inputPeerSelf'],
|
||||
['inputChannel', 'inputPeerChannel'],
|
||||
['inputChannelFromMessage', 'inputPeerChannelFromMessage'],
|
||||
['inputUserFromMessage', 'inputPeerUserFromMessage'],
|
||||
] as const)('should convert %s to %s', (fromType, toType) => {
|
||||
const from = createStub(fromType)
|
||||
const to = createStub(toType)
|
||||
|
||||
expect(normalizeToInputPeer(from)).toEqual(to)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['inputPeerEmpty'],
|
||||
['inputPeerSelf'],
|
||||
['inputPeerUser'],
|
||||
['inputPeerChannel'],
|
||||
['inputPeerChannelFromMessage'],
|
||||
['inputPeerUserFromMessage'],
|
||||
] as const)('should keep %s as is', (type) => {
|
||||
const obj = createStub(type)
|
||||
|
||||
expect(normalizeToInputPeer(obj)).toBe(obj)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeToInputUser', () => {
|
||||
it.each([
|
||||
['inputPeerSelf', 'inputUserSelf'],
|
||||
['inputPeerUser', 'inputUser'],
|
||||
['inputPeerUserFromMessage', 'inputUserFromMessage'],
|
||||
] as const)('should convert %s to %s', (fromType, toType) => {
|
||||
const from = createStub(fromType)
|
||||
const to = createStub(toType)
|
||||
|
||||
expect(normalizeToInputUser(from)).toEqual(to)
|
||||
})
|
||||
|
||||
it('should throw for other types', () => {
|
||||
expect(() => normalizeToInputUser(createStub('inputPeerChannel'), 'some_channel')).toThrow(
|
||||
MtInvalidPeerTypeError,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeToInputChannel', () => {
|
||||
it.each([
|
||||
['inputPeerChannel', 'inputChannel'],
|
||||
['inputPeerChannelFromMessage', 'inputChannelFromMessage'],
|
||||
] as const)('should convert %s to %s', (fromType, toType) => {
|
||||
const from = createStub(fromType)
|
||||
const to = createStub(toType)
|
||||
|
||||
expect(normalizeToInputChannel(from)).toEqual(to)
|
||||
})
|
||||
|
||||
it('should throw for other types', () => {
|
||||
expect(() => normalizeToInputChannel(createStub('inputPeerUser'), 'some_user')).toThrow(MtInvalidPeerTypeError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isInputPeerUser', () => {
|
||||
it.each([['inputPeerSelf'], ['inputPeerUser'], ['inputPeerUserFromMessage']] as const)(
|
||||
'should return true for %s',
|
||||
(type) => {
|
||||
expect(isInputPeerUser(createStub(type))).toBe(true)
|
||||
},
|
||||
)
|
||||
|
||||
it.each([['inputPeerEmpty'], ['inputPeerChannel'], ['inputPeerChannelFromMessage']] as const)(
|
||||
'should return false for %s',
|
||||
(type) => {
|
||||
expect(isInputPeerUser(createStub(type))).toBe(false)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('isInputPeerChannel', () => {
|
||||
it.each([['inputPeerChannel'], ['inputPeerChannelFromMessage']] as const)('should return true for %s', (type) => {
|
||||
expect(isInputPeerChannel(createStub(type))).toBe(true)
|
||||
})
|
||||
|
||||
it.each([['inputPeerEmpty'], ['inputPeerSelf'], ['inputPeerUser'], ['inputPeerUserFromMessage']] as const)(
|
||||
'should return false for %s',
|
||||
(type) => {
|
||||
expect(isInputPeerChannel(createStub(type))).toBe(false)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('isInputPeerChat', () => {
|
||||
it('should return true for inputPeerChat', () => {
|
||||
expect(isInputPeerChat(createStub('inputPeerChat'))).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['inputPeerChannel'],
|
||||
['inputPeerChannelFromMessage'],
|
||||
['inputPeerEmpty'],
|
||||
['inputPeerSelf'],
|
||||
['inputPeerUser'],
|
||||
['inputPeerUserFromMessage'],
|
||||
] as const)('should return false for %s', (type) => {
|
||||
expect(isInputPeerChat(createStub(type))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('inputPeerToPeer', () => {
|
||||
it.each([
|
||||
['inputPeerUser', 'peerUser'],
|
||||
['inputPeerUserFromMessage', 'peerUser'],
|
||||
['inputPeerChat', 'peerChat'],
|
||||
['inputPeerChannel', 'peerChannel'],
|
||||
['inputPeerChannelFromMessage', 'peerChannel'],
|
||||
] as const)('should convert %s to %s', (fromType, toType) => {
|
||||
const from = createStub(fromType)
|
||||
const to = createStub(toType)
|
||||
|
||||
expect(inputPeerToPeer(from)).toEqual(to)
|
||||
})
|
||||
|
||||
it('should throw for other types', () => {
|
||||
expect(() => inputPeerToPeer(createStub('inputPeerEmpty'))).toThrow(MtInvalidPeerTypeError)
|
||||
expect(() => inputPeerToPeer(createStub('inputPeerSelf'))).toThrow(MtInvalidPeerTypeError)
|
||||
})
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
import { assertNever, Long, tl } from '@mtcute/core'
|
||||
import { assertNever, tl } from '@mtcute/core'
|
||||
|
||||
import { MtInvalidPeerTypeError } from '../types/errors.js'
|
||||
import { InputPeerLike } from '../types/peers/index.js'
|
||||
|
@ -140,21 +140,6 @@ export function inputPeerToPeer(inp: tl.TypeInputPeer): tl.TypePeer {
|
|||
case 'inputPeerChat':
|
||||
return { _: 'peerChat', chatId: inp.chatId }
|
||||
default:
|
||||
throw new Error(`Cannot convert ${inp._} to peer`)
|
||||
}
|
||||
}
|
||||
|
||||
export function peerToInputPeer(peer: tl.TypePeer, accessHash = Long.ZERO): tl.TypeInputPeer {
|
||||
switch (peer._) {
|
||||
case 'peerUser':
|
||||
return { _: 'inputPeerUser', userId: peer.userId, accessHash }
|
||||
case 'peerChannel':
|
||||
return {
|
||||
_: 'inputPeerChannel',
|
||||
channelId: peer.channelId,
|
||||
accessHash,
|
||||
}
|
||||
case 'peerChat':
|
||||
return { _: 'inputPeerChat', chatId: peer.chatId }
|
||||
throw new MtInvalidPeerTypeError(inp, `Cannot convert ${inp._} to peer`)
|
||||
}
|
||||
}
|
||||
|
|
41
packages/client/src/utils/voice-utils.test.ts
Normal file
41
packages/client/src/utils/voice-utils.test.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { hexDecodeToBuffer, hexEncode } from '@mtcute/core/utils.js'
|
||||
|
||||
import { decodeWaveform, encodeWaveform } from './voice-utils.js'
|
||||
|
||||
describe('decodeWaveform', () => {
|
||||
it('should correctly decode telegram-encoded waveform', () => {
|
||||
expect(
|
||||
decodeWaveform(
|
||||
hexDecodeToBuffer(
|
||||
'0000104210428c310821a51463cc39072184524a4aa9b51663acb5e69c7bef41' +
|
||||
'08618c514a39e7a494d65aadb5f75e8c31ce396badf7de9cf3debbf7feff0f',
|
||||
),
|
||||
),
|
||||
).toEqual([
|
||||
0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 10,
|
||||
11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19,
|
||||
19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 26, 26, 26, 27, 27, 27, 28,
|
||||
28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31,
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('encodeWaveform', () => {
|
||||
it('should correctly decode telegram-encoded waveform', () => {
|
||||
expect(
|
||||
hexEncode(
|
||||
encodeWaveform([
|
||||
0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10,
|
||||
10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 18,
|
||||
18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 26,
|
||||
26, 26, 27, 27, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31,
|
||||
]),
|
||||
),
|
||||
).toEqual(
|
||||
'0000104210428c310821a51463cc39072184524a4aa9b51663acb5e69c7bef41' +
|
||||
'08618c514a39e7a494d65aadb5f75e8c31ce396badf7de9cf3debbf7feff0f',
|
||||
)
|
||||
})
|
||||
})
|
|
@ -8,6 +8,7 @@
|
|||
"./src",
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../core" }
|
||||
{ "path": "../core" },
|
||||
{ "path": "../test" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@
|
|||
"references": [
|
||||
{ "path": "../tl" },
|
||||
{ "path": "../tl-runtime" },
|
||||
{ "path": "../test" },
|
||||
]
|
||||
}
|
||||
|
|
|
@ -108,6 +108,10 @@ importers:
|
|||
'@mtcute/file-id':
|
||||
specifier: workspace:^
|
||||
version: link:../file-id
|
||||
devDependencies:
|
||||
'@mtcute/test':
|
||||
specifier: workspace:^
|
||||
version: link:../test
|
||||
|
||||
packages/core:
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in a new issue