test(core): improved test coverage
This commit is contained in:
parent
59a4a7553f
commit
e31ecbd3d1
41 changed files with 2262 additions and 173 deletions
15
.eslintrc.js
15
.eslintrc.js
|
@ -215,7 +215,13 @@ module.exports = {
|
|||
'@typescript-eslint/no-dynamic-delete': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'no-restricted-globals': ['error', 'Buffer', '__dirname', 'require'],
|
||||
'no-restricted-imports': ['error', 'buffer', 'crypto', 'fs', 'path', 'stream'],
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: ['buffer', 'crypto', 'fs', 'path', 'stream'],
|
||||
patterns: ['@mtcute/*/dist/**'],
|
||||
},
|
||||
],
|
||||
},
|
||||
reportUnusedDisableDirectives: false,
|
||||
settings: {
|
||||
|
@ -229,7 +235,12 @@ module.exports = {
|
|||
files: ['**/scripts/**', '*.test.ts', 'packages/create-*/**', '**/build.config.cjs'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'no-restricted-imports': 'off',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: ['@mtcute/*/dist/**'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -13,10 +13,10 @@ module.exports = {
|
|||
}
|
||||
|
||||
return [
|
||||
...[...modifiedPackages].map((pkg) => `pnpm -C packages/${pkg} exec tsc --build`),
|
||||
`prettier --write ${filenames.join(' ')}`,
|
||||
`eslint -c ${eslintCiConfig} --fix ${filenames.join(' ')}`,
|
||||
'pnpm run lint:dpdm',
|
||||
...[...modifiedPackages].map((pkg) => `pnpm -C packages/${pkg} run --if-present build --noEmit`)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,8 @@
|
|||
"devDependencies": {
|
||||
"@types/ws": "8.5.4",
|
||||
"node-forge": "1.3.1",
|
||||
"@mtcute/test": "workspace:^",
|
||||
"@mtcute/dispatcher": "workspace:^",
|
||||
"ws": "8.13.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@ import { describe, expect, it } from 'vitest'
|
|||
|
||||
import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime'
|
||||
|
||||
import { IntermediatePacketCodec, TransportError } from '../../index.js'
|
||||
import { IntermediatePacketCodec, PaddedIntermediatePacketCodec, TransportError } from '../../index.js'
|
||||
import { concatBuffers, dataViewFromBuffer } from '../../utils/index.js'
|
||||
|
||||
describe('IntermediatePacketCodec', () => {
|
||||
it('should return correct tag', () => {
|
||||
|
@ -76,4 +77,35 @@ describe('IntermediatePacketCodec', () => {
|
|||
codec.reset()
|
||||
codec.feed(hexDecodeToBuffer('050000000102030405'))
|
||||
}))
|
||||
|
||||
it('should correctly frame packets', () => {
|
||||
const data = hexDecodeToBuffer('6cfeffff')
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
expect(Buffer.from(new IntermediatePacketCodec().encode(data))).toEqual(
|
||||
concatBuffers([new Uint8Array([0x04, 0x00, 0x00, 0x00]), data]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PaddedIntermediatePacketCodec', () => {
|
||||
it('should return correct tag', () => {
|
||||
expect(hexEncode(new PaddedIntermediatePacketCodec().tag())).eq('dddddddd')
|
||||
})
|
||||
|
||||
it('should correctly frame packets', () => {
|
||||
// todo: once we have predictable random, test this properly
|
||||
|
||||
const data = hexDecodeToBuffer('6cfeffff')
|
||||
const encoded = new PaddedIntermediatePacketCodec().encode(data)
|
||||
const dv = dataViewFromBuffer(encoded)
|
||||
|
||||
const packetSize = dv.getUint32(0, true)
|
||||
const paddingSize = packetSize - data.length
|
||||
|
||||
// padding size, 0-15
|
||||
expect(paddingSize).toBeGreaterThanOrEqual(0)
|
||||
expect(paddingSize).toBeLessThanOrEqual(15)
|
||||
expect([...encoded.slice(4, 4 + packetSize - paddingSize)]).toEqual([...data])
|
||||
})
|
||||
})
|
||||
|
|
42
packages/core/src/storage/json.test.ts
Normal file
42
packages/core/src/storage/json.test.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { JsonMemoryStorage } from './json.js'
|
||||
import { stubPeerUser } from './storage.test-utils.js'
|
||||
|
||||
describe('JsonMemoryStorage', () => {
|
||||
class ExtJsonMemoryStorage extends JsonMemoryStorage {
|
||||
loadJson(json: string): void {
|
||||
this._loadJson(json)
|
||||
}
|
||||
|
||||
saveJson(): string {
|
||||
return this._saveJson()
|
||||
}
|
||||
|
||||
getInternalState() {
|
||||
return this._state
|
||||
}
|
||||
}
|
||||
|
||||
it('should allow importing and exporting to json', () => {
|
||||
const s = new ExtJsonMemoryStorage()
|
||||
|
||||
s.setUpdatesPts(123)
|
||||
s.setUpdatesQts(456)
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
s.setAuthKeyFor(1, Buffer.from([1, 2, 3]))
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
s.setTempAuthKeyFor(2, 0, Buffer.from([4, 5, 6]), 1234567890)
|
||||
s.setState('someState', 'someValue')
|
||||
s.updatePeers([{ ...stubPeerUser, updated: 0 }])
|
||||
|
||||
const json = s.saveJson()
|
||||
const s2 = new ExtJsonMemoryStorage()
|
||||
s2.loadJson(json)
|
||||
|
||||
expect(s2.getInternalState()).toEqual({
|
||||
...s.getInternalState(),
|
||||
entities: new Map(), // entities are not saved
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,8 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
import { tl } from '@mtcute/tl'
|
||||
import { base64DecodeToBuffer, base64Encode } from '@mtcute/tl-runtime'
|
||||
|
||||
import { longFromFastString, longToFastString } from '../utils/long-utils.js'
|
||||
import { MemorySessionState, MemoryStorage } from './memory.js'
|
||||
|
||||
/**
|
||||
|
@ -16,25 +14,26 @@ export class JsonMemoryStorage extends MemoryStorage {
|
|||
switch (key) {
|
||||
case 'authKeys':
|
||||
case 'authKeysTemp': {
|
||||
const ret: Record<string, Uint8Array> = {}
|
||||
const ret = new Map<string | number, Uint8Array>()
|
||||
|
||||
;(value as string).split('|').forEach((pair: string) => {
|
||||
const [dcId, b64] = pair.split(',')
|
||||
ret[dcId] = base64DecodeToBuffer(b64)
|
||||
const mapKey = key === 'authKeysTemp' ? dcId : parseInt(dcId)
|
||||
|
||||
ret.set(mapKey, base64DecodeToBuffer(b64))
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
case 'authKeysTempExpiry':
|
||||
case 'entities':
|
||||
case 'phoneIndex':
|
||||
case 'usernameIndex':
|
||||
case 'pts':
|
||||
case 'fsm':
|
||||
case 'rl':
|
||||
return new Map(Object.entries(value as Record<string, string>))
|
||||
case 'accessHash':
|
||||
return longFromFastString(value as string)
|
||||
case 'entities':
|
||||
return new Map()
|
||||
}
|
||||
|
||||
return value
|
||||
|
@ -55,15 +54,14 @@ export class JsonMemoryStorage extends MemoryStorage {
|
|||
.join('|')
|
||||
}
|
||||
case 'authKeysTempExpiry':
|
||||
case 'entities':
|
||||
case 'phoneIndex':
|
||||
case 'usernameIndex':
|
||||
case 'pts':
|
||||
case 'fsm':
|
||||
case 'rl':
|
||||
return Object.fromEntries([...(value as Map<string, string>).entries()])
|
||||
case 'accessHash':
|
||||
return longToFastString(value as tl.Long)
|
||||
case 'entities':
|
||||
return {}
|
||||
}
|
||||
|
||||
return value
|
||||
|
|
26
packages/core/src/storage/localstorage.test.ts
Normal file
26
packages/core/src/storage/localstorage.test.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LocalstorageStorage } from './localstorage.js'
|
||||
|
||||
const localStorageStub = {
|
||||
getItem: vi.fn().mockImplementation(() => null),
|
||||
setItem: vi.fn(),
|
||||
}
|
||||
describe('LocalstorageStorage', () => {
|
||||
beforeAll(() => void vi.stubGlobal('localStorage', localStorageStub))
|
||||
afterAll(() => void vi.unstubAllGlobals())
|
||||
|
||||
it('should load from localstorage', () => {
|
||||
const s = new LocalstorageStorage('test')
|
||||
s.load()
|
||||
|
||||
expect(localStorageStub.getItem).toHaveBeenCalledWith('test')
|
||||
})
|
||||
|
||||
it('should save to localstorage', () => {
|
||||
const s = new LocalstorageStorage('test')
|
||||
s.save()
|
||||
|
||||
expect(localStorageStub.setItem).toHaveBeenCalledWith('test', expect.any(String))
|
||||
})
|
||||
})
|
|
@ -16,11 +16,14 @@ export class LocalstorageStorage extends JsonMemoryStorage {
|
|||
|
||||
load(): void {
|
||||
try {
|
||||
this._loadJson(localStorage[this._key] as string)
|
||||
const val = localStorage.getItem(this._key)
|
||||
if (val === null) return
|
||||
|
||||
this._loadJson(val)
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
save(): void {
|
||||
localStorage[this._key] = this._saveJson()
|
||||
localStorage.setItem(this._key, this._saveJson())
|
||||
}
|
||||
}
|
||||
|
|
52
packages/core/src/storage/memory.test.ts
Normal file
52
packages/core/src/storage/memory.test.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { MemoryStorage } from './memory.js'
|
||||
import { testStateStorage, testStorage } from './storage.test-utils.js'
|
||||
|
||||
describe('MemoryStorage', () => {
|
||||
testStorage(new MemoryStorage())
|
||||
testStateStorage(new MemoryStorage())
|
||||
|
||||
describe('extending', () => {
|
||||
it('should allow populating from an object', () => {
|
||||
class ExtendedMemoryStorage extends MemoryStorage {
|
||||
constructor() {
|
||||
super()
|
||||
this._setStateFrom({
|
||||
$version: 1,
|
||||
defaultDcs: null,
|
||||
authKeys: new Map(),
|
||||
authKeysTemp: new Map(),
|
||||
authKeysTempExpiry: new Map(),
|
||||
entities: new Map(),
|
||||
phoneIndex: new Map(),
|
||||
usernameIndex: new Map(),
|
||||
gpts: [1, 2, 3, 4],
|
||||
pts: new Map(),
|
||||
fsm: new Map(),
|
||||
rl: new Map(),
|
||||
self: null,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const s = new ExtendedMemoryStorage()
|
||||
|
||||
expect(s.getUpdatesState()).toEqual([1, 2, 3, 4])
|
||||
})
|
||||
|
||||
it('should silently fail if version is wrong', () => {
|
||||
class ExtendedMemoryStorage extends MemoryStorage {
|
||||
constructor() {
|
||||
super()
|
||||
// eslint-disable-next-line
|
||||
this._setStateFrom({ $version: 0 } as any)
|
||||
}
|
||||
}
|
||||
|
||||
const s = new ExtendedMemoryStorage()
|
||||
|
||||
expect(s.getUpdatesState()).toEqual(null)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,6 +1,6 @@
|
|||
import { IStateStorage } from '@mtcute/dispatcher'
|
||||
import { tl } from '@mtcute/tl'
|
||||
|
||||
import { MaybeAsync } from '../types/index.js'
|
||||
import { LruMap, toggleChannelIdMark } from '../utils/index.js'
|
||||
import { ITelegramStorage } from './abstract.js'
|
||||
|
||||
|
@ -56,7 +56,7 @@ export interface MemorySessionState {
|
|||
|
||||
const USERNAME_TTL = 86400000 // 24 hours
|
||||
|
||||
export class MemoryStorage implements ITelegramStorage /*, IStateStorage*/ {
|
||||
export class MemoryStorage implements ITelegramStorage, IStateStorage {
|
||||
protected _state!: MemorySessionState
|
||||
private _cachedInputPeers: LruMap<number, tl.TypeInputPeer> = new LruMap(100)
|
||||
|
||||
|
@ -131,11 +131,11 @@ export class MemoryStorage implements ITelegramStorage /*, IStateStorage*/ {
|
|||
// populate indexes if needed
|
||||
let populate = false
|
||||
|
||||
if (!obj.phoneIndex) {
|
||||
if (!obj.phoneIndex?.size) {
|
||||
obj.phoneIndex = new Map()
|
||||
populate = true
|
||||
}
|
||||
if (!obj.usernameIndex) {
|
||||
if (!obj.usernameIndex?.size) {
|
||||
obj.usernameIndex = new Map()
|
||||
populate = true
|
||||
}
|
||||
|
@ -229,7 +229,7 @@ export class MemoryStorage implements ITelegramStorage /*, IStateStorage*/ {
|
|||
}
|
||||
}
|
||||
|
||||
updatePeers(peers: PeerInfoWithUpdated[]): MaybeAsync<void> {
|
||||
updatePeers(peers: PeerInfoWithUpdated[]): void {
|
||||
for (const peer of peers) {
|
||||
this._cachedFull.set(peer.id, peer.full)
|
||||
|
||||
|
@ -326,26 +326,26 @@ export class MemoryStorage implements ITelegramStorage /*, IStateStorage*/ {
|
|||
return this._state.pts.get(entityId) ?? null
|
||||
}
|
||||
|
||||
getUpdatesState(): MaybeAsync<[number, number, number, number] | null> {
|
||||
getUpdatesState(): [number, number, number, number] | null {
|
||||
return this._state.gpts ?? null
|
||||
}
|
||||
|
||||
setUpdatesPts(val: number): MaybeAsync<void> {
|
||||
setUpdatesPts(val: number): void {
|
||||
if (!this._state.gpts) this._state.gpts = [0, 0, 0, 0]
|
||||
this._state.gpts[0] = val
|
||||
}
|
||||
|
||||
setUpdatesQts(val: number): MaybeAsync<void> {
|
||||
setUpdatesQts(val: number): void {
|
||||
if (!this._state.gpts) this._state.gpts = [0, 0, 0, 0]
|
||||
this._state.gpts[1] = val
|
||||
}
|
||||
|
||||
setUpdatesDate(val: number): MaybeAsync<void> {
|
||||
setUpdatesDate(val: number): void {
|
||||
if (!this._state.gpts) this._state.gpts = [0, 0, 0, 0]
|
||||
this._state.gpts[2] = val
|
||||
}
|
||||
|
||||
setUpdatesSeq(val: number): MaybeAsync<void> {
|
||||
setUpdatesSeq(val: number): void {
|
||||
if (!this._state.gpts) this._state.gpts = [0, 0, 0, 0]
|
||||
this._state.gpts[3] = val
|
||||
}
|
||||
|
|
332
packages/core/src/storage/storage.test-utils.ts
Normal file
332
packages/core/src/storage/storage.test-utils.ts
Normal file
|
@ -0,0 +1,332 @@
|
|||
import Long from 'long'
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { IStateStorage } from '@mtcute/dispatcher'
|
||||
import { createStub } from '@mtcute/test'
|
||||
import { tl } from '@mtcute/tl'
|
||||
import { __tlReaderMap } from '@mtcute/tl/binary/reader.js'
|
||||
import { __tlWriterMap } from '@mtcute/tl/binary/writer.js'
|
||||
|
||||
import { defaultProductionDc } from '../utils/default-dcs.js'
|
||||
import { LogManager } from '../utils/index.js'
|
||||
import { ITelegramStorage } from './abstract.js'
|
||||
|
||||
export const stubPeerUser: ITelegramStorage.PeerInfo = {
|
||||
id: 123123,
|
||||
accessHash: Long.fromBits(123, 456),
|
||||
type: 'user',
|
||||
username: 'some_user',
|
||||
phone: '78005553535',
|
||||
full: createStub('user', { id: 123123 }),
|
||||
}
|
||||
const peerUserInput: tl.TypeInputPeer = {
|
||||
_: 'inputPeerUser',
|
||||
userId: 123123,
|
||||
accessHash: Long.fromBits(123, 456),
|
||||
}
|
||||
|
||||
const peerChannel: ITelegramStorage.PeerInfo = {
|
||||
id: -1001183945448,
|
||||
accessHash: Long.fromBits(666, 555),
|
||||
type: 'channel',
|
||||
username: 'some_channel',
|
||||
full: createStub('channel', { id: 123123 }),
|
||||
}
|
||||
|
||||
const peerChannelInput: tl.TypeInputPeer = {
|
||||
_: 'inputPeerChannel',
|
||||
channelId: 1183945448,
|
||||
accessHash: Long.fromBits(666, 555),
|
||||
}
|
||||
|
||||
export function testStorage(s: ITelegramStorage): void {
|
||||
beforeAll(async () => {
|
||||
await s.load?.()
|
||||
|
||||
const logger = new LogManager()
|
||||
logger.level = 0
|
||||
s.setup?.(logger, __tlReaderMap, __tlWriterMap)
|
||||
})
|
||||
|
||||
afterAll(() => s.destroy?.())
|
||||
beforeEach(() => s.reset?.())
|
||||
|
||||
describe('default dc', () => {
|
||||
it('should store', async () => {
|
||||
await s.setDefaultDcs(defaultProductionDc)
|
||||
expect(await s.getDefaultDcs()).toBe(defaultProductionDc)
|
||||
})
|
||||
|
||||
it('should remove', async () => {
|
||||
await s.setDefaultDcs(null)
|
||||
expect(await s.getDefaultDcs()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('auth keys', () => {
|
||||
beforeAll(() => void vi.useFakeTimers())
|
||||
afterAll(() => void vi.useRealTimers())
|
||||
|
||||
const key2 = new Uint8Array(256).fill(0x42)
|
||||
const key3 = new Uint8Array(256).fill(0x43)
|
||||
|
||||
const key2i0 = new Uint8Array(256).fill(0x44)
|
||||
const key2i1 = new Uint8Array(256).fill(0x45)
|
||||
const key3i0 = new Uint8Array(256).fill(0x46)
|
||||
const key3i1 = new Uint8Array(256).fill(0x47)
|
||||
|
||||
it('should store perm auth key', async () => {
|
||||
await s.setAuthKeyFor(2, key2)
|
||||
await s.setAuthKeyFor(3, key3)
|
||||
|
||||
expect(await s.getAuthKeyFor(2)).toEqual(key2)
|
||||
expect(await s.getAuthKeyFor(3)).toEqual(key3)
|
||||
})
|
||||
|
||||
it('should store temp auth keys', async () => {
|
||||
const expire = Date.now() + 1000
|
||||
|
||||
await s.setTempAuthKeyFor(2, 0, key2i0, expire)
|
||||
await s.setTempAuthKeyFor(2, 1, key2i1, expire)
|
||||
await s.setTempAuthKeyFor(3, 0, key3i0, expire)
|
||||
await s.setTempAuthKeyFor(3, 1, key3i1, expire)
|
||||
|
||||
expect(await s.getAuthKeyFor(2, 0)).toEqual(key2i0)
|
||||
expect(await s.getAuthKeyFor(2, 1)).toEqual(key2i1)
|
||||
expect(await s.getAuthKeyFor(3, 0)).toEqual(key3i0)
|
||||
expect(await s.getAuthKeyFor(3, 1)).toEqual(key3i1)
|
||||
})
|
||||
|
||||
it('should expire temp auth keys', async () => {
|
||||
const expire = Date.now() + 1000
|
||||
|
||||
await s.setTempAuthKeyFor(2, 0, key2i0, expire)
|
||||
await s.setTempAuthKeyFor(2, 1, key2i1, expire)
|
||||
await s.setTempAuthKeyFor(3, 0, key3i0, expire)
|
||||
await s.setTempAuthKeyFor(3, 1, key3i1, expire)
|
||||
|
||||
vi.advanceTimersByTime(10000)
|
||||
|
||||
expect(await s.getAuthKeyFor(2, 0)).toBeNull()
|
||||
expect(await s.getAuthKeyFor(2, 1)).toBeNull()
|
||||
expect(await s.getAuthKeyFor(3, 0)).toBeNull()
|
||||
expect(await s.getAuthKeyFor(3, 1)).toBeNull()
|
||||
})
|
||||
|
||||
it('should remove auth keys', async () => {
|
||||
const expire = Date.now() + 1000
|
||||
|
||||
await s.setTempAuthKeyFor(2, 0, key2i0, expire)
|
||||
await s.setTempAuthKeyFor(2, 1, key2i1, expire)
|
||||
await s.setAuthKeyFor(2, key2)
|
||||
await s.setAuthKeyFor(3, key3)
|
||||
|
||||
await s.setAuthKeyFor(2, null)
|
||||
await s.setTempAuthKeyFor(2, 0, null, 0)
|
||||
await s.setTempAuthKeyFor(2, 1, null, 0)
|
||||
|
||||
expect(await s.getAuthKeyFor(2)).toBeNull()
|
||||
expect(await s.getAuthKeyFor(2, 0)).toBeNull()
|
||||
expect(await s.getAuthKeyFor(2, 1)).toBeNull()
|
||||
expect(await s.getAuthKeyFor(3)).toEqual(key3) // should not be removed
|
||||
})
|
||||
|
||||
it('should remove all auth keys with dropAuthKeysFor', async () => {
|
||||
const expire = Date.now() + 1000
|
||||
|
||||
await s.setTempAuthKeyFor(2, 0, key2i0, expire)
|
||||
await s.setTempAuthKeyFor(2, 1, key2i1, expire)
|
||||
await s.setAuthKeyFor(2, key2)
|
||||
await s.setAuthKeyFor(3, key3)
|
||||
|
||||
await s.dropAuthKeysFor(2)
|
||||
|
||||
expect(await s.getAuthKeyFor(2)).toBeNull()
|
||||
expect(await s.getAuthKeyFor(2, 0)).toBeNull()
|
||||
expect(await s.getAuthKeyFor(2, 1)).toBeNull()
|
||||
expect(await s.getAuthKeyFor(3)).toEqual(key3) // should not be removed
|
||||
})
|
||||
})
|
||||
|
||||
describe('peers', () => {
|
||||
it('should cache and return peers', async () => {
|
||||
await s.updatePeers([stubPeerUser, peerChannel])
|
||||
|
||||
expect(await s.getPeerById(stubPeerUser.id)).toEqual(peerUserInput)
|
||||
expect(await s.getPeerById(peerChannel.id)).toEqual(peerChannelInput)
|
||||
})
|
||||
|
||||
it('should cache and return peers by username', async () => {
|
||||
await s.updatePeers([stubPeerUser, peerChannel])
|
||||
|
||||
expect(await s.getPeerByUsername(stubPeerUser.username!)).toEqual(peerUserInput)
|
||||
expect(await s.getPeerByUsername(peerChannel.username!)).toEqual(peerChannelInput)
|
||||
})
|
||||
|
||||
it('should cache and return peers by phone', async () => {
|
||||
await s.updatePeers([stubPeerUser])
|
||||
|
||||
expect(await s.getPeerByPhone(stubPeerUser.phone!)).toEqual(peerUserInput)
|
||||
})
|
||||
|
||||
it('should overwrite existing cached peers', async () => {
|
||||
await s.updatePeers([stubPeerUser])
|
||||
await s.updatePeers([{ ...stubPeerUser, username: 'whatever' }])
|
||||
|
||||
expect(await s.getPeerById(stubPeerUser.id)).toEqual(peerUserInput)
|
||||
expect(await s.getPeerByUsername(stubPeerUser.username!)).toBeNull()
|
||||
expect(await s.getPeerByUsername('whatever')).toEqual(peerUserInput)
|
||||
})
|
||||
|
||||
it('should cache full peer info', async () => {
|
||||
await s.updatePeers([stubPeerUser, peerChannel])
|
||||
|
||||
expect(await s.getFullPeerById(stubPeerUser.id)).toEqual(stubPeerUser.full)
|
||||
expect(await s.getFullPeerById(peerChannel.id)).toEqual(peerChannel.full)
|
||||
})
|
||||
})
|
||||
|
||||
describe('current user', () => {
|
||||
const self: ITelegramStorage.SelfInfo = {
|
||||
userId: 123123,
|
||||
isBot: false,
|
||||
}
|
||||
|
||||
it('should store and return self info', async () => {
|
||||
await s.setSelf(self)
|
||||
expect(await s.getSelf()).toEqual(self)
|
||||
})
|
||||
|
||||
it('should remove self info', async () => {
|
||||
await s.setSelf(self)
|
||||
await s.setSelf(null)
|
||||
expect(await s.getSelf()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('updates state', () => {
|
||||
it('should store and return updates state', async () => {
|
||||
await s.setUpdatesPts(1)
|
||||
await s.setUpdatesQts(2)
|
||||
await s.setUpdatesDate(3)
|
||||
await s.setUpdatesSeq(4)
|
||||
expect(await s.getUpdatesState()).toEqual([1, 2, 3, 4])
|
||||
})
|
||||
|
||||
it('should store and return channel pts', async () => {
|
||||
await s.setManyChannelPts(
|
||||
new Map([
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
]),
|
||||
)
|
||||
|
||||
expect(await s.getChannelPts(1)).toEqual(2)
|
||||
expect(await s.getChannelPts(3)).toEqual(4)
|
||||
expect(await s.getChannelPts(2)).toBeNull()
|
||||
})
|
||||
|
||||
it('should be null after reset', async () => {
|
||||
expect(await s.getUpdatesState()).toBeNull()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function testStateStorage(s: IStateStorage) {
|
||||
describe('key-value state', () => {
|
||||
beforeAll(() => void vi.useFakeTimers())
|
||||
afterAll(() => void vi.useRealTimers())
|
||||
|
||||
it('should store and return state', async () => {
|
||||
await s.setState('a', 'b')
|
||||
await s.setState('c', 'd')
|
||||
await s.setState('e', 'f')
|
||||
|
||||
expect(await s.getState('a')).toEqual('b')
|
||||
expect(await s.getState('c')).toEqual('d')
|
||||
expect(await s.getState('e')).toEqual('f')
|
||||
})
|
||||
|
||||
it('should remove state', async () => {
|
||||
await s.setState('a', 'b')
|
||||
await s.setState('c', 'd')
|
||||
await s.setState('e', 'f')
|
||||
|
||||
await s.deleteState('a')
|
||||
await s.deleteState('c')
|
||||
await s.deleteState('e')
|
||||
|
||||
expect(await s.getState('a')).toBeNull()
|
||||
expect(await s.getState('c')).toBeNull()
|
||||
expect(await s.getState('e')).toBeNull()
|
||||
})
|
||||
|
||||
it('should expire state', async () => {
|
||||
await s.setState('a', 'b', 1)
|
||||
await s.setState('c', 'd', 1)
|
||||
await s.setState('e', 'f', 1)
|
||||
|
||||
vi.advanceTimersByTime(10000)
|
||||
|
||||
expect(await s.getState('a')).toBeNull()
|
||||
expect(await s.getState('c')).toBeNull()
|
||||
expect(await s.getState('e')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('scenes', () => {
|
||||
it('should store and return scenes', async () => {
|
||||
await s.setCurrentScene('a', 'b')
|
||||
await s.setCurrentScene('c', 'd')
|
||||
await s.setCurrentScene('e', 'f')
|
||||
|
||||
expect(await s.getCurrentScene('a')).toEqual('b')
|
||||
expect(await s.getCurrentScene('c')).toEqual('d')
|
||||
expect(await s.getCurrentScene('e')).toEqual('f')
|
||||
})
|
||||
|
||||
it('should remove scenes', async () => {
|
||||
await s.setCurrentScene('a', 'b')
|
||||
await s.setCurrentScene('c', 'd')
|
||||
await s.setCurrentScene('e', 'f')
|
||||
|
||||
await s.deleteCurrentScene('a')
|
||||
await s.deleteCurrentScene('c')
|
||||
await s.deleteCurrentScene('e')
|
||||
|
||||
expect(await s.getCurrentScene('a')).toBeNull()
|
||||
expect(await s.getCurrentScene('c')).toBeNull()
|
||||
expect(await s.getCurrentScene('e')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('rate limit', () => {
|
||||
beforeAll(() => void vi.useFakeTimers())
|
||||
afterAll(() => void vi.useRealTimers())
|
||||
|
||||
const check = () => s.getRateLimit('test', 3, 1)
|
||||
|
||||
it('should implement basic rate limiting', async () => {
|
||||
vi.setSystemTime(0)
|
||||
|
||||
expect(await check()).toEqual([3, 1000])
|
||||
expect(await check()).toEqual([2, 1000])
|
||||
expect(await check()).toEqual([1, 1000])
|
||||
expect(await check()).toEqual([0, 1000])
|
||||
|
||||
vi.setSystemTime(1001)
|
||||
|
||||
expect(await check()).toEqual([3, 2001])
|
||||
})
|
||||
|
||||
it('should allow resetting rate limit', async () => {
|
||||
vi.setSystemTime(0)
|
||||
|
||||
await check()
|
||||
await check()
|
||||
|
||||
await s.resetRateLimit('test')
|
||||
expect(await check()).toEqual([3, 1000])
|
||||
})
|
||||
})
|
||||
}
|
32
packages/core/src/utils/async-lock.test.ts
Normal file
32
packages/core/src/utils/async-lock.test.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { AsyncLock } from './async-lock.js'
|
||||
import { sleep } from './misc-utils.js'
|
||||
|
||||
describe('AsyncLock', () => {
|
||||
it('should correctly lock execution', async () => {
|
||||
const lock = new AsyncLock()
|
||||
|
||||
const log: number[] = []
|
||||
await Promise.all(
|
||||
Array.from({ length: 10 }, (_, idx) =>
|
||||
lock.with(async () => {
|
||||
await sleep(10 - idx)
|
||||
log.push(idx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(log).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||
})
|
||||
|
||||
it('should correctly propagate errors', async () => {
|
||||
const lock = new AsyncLock()
|
||||
|
||||
await expect(async () => {
|
||||
await lock.with(() => {
|
||||
throw new Error('test')
|
||||
})
|
||||
}).rejects.toThrow('test')
|
||||
})
|
||||
})
|
|
@ -2,7 +2,28 @@ import { describe, expect, it } from 'vitest'
|
|||
|
||||
import { hexDecodeToBuffer } from '@mtcute/tl-runtime'
|
||||
|
||||
import { bigIntToBuffer, bufferToBigInt } from './index.js'
|
||||
import {
|
||||
bigIntBitLength,
|
||||
bigIntGcd,
|
||||
bigIntModInv,
|
||||
bigIntModPow,
|
||||
bigIntToBuffer,
|
||||
bufferToBigInt,
|
||||
randomBigInt,
|
||||
randomBigIntBits,
|
||||
randomBigIntInRange,
|
||||
twoMultiplicity,
|
||||
} from './index.js'
|
||||
|
||||
describe('bigIntBitLength', () => {
|
||||
it('should correctly calculate bit length', () => {
|
||||
expect(bigIntBitLength(0n)).eq(0)
|
||||
expect(bigIntBitLength(1n)).eq(1)
|
||||
expect(bigIntBitLength(2n)).eq(2)
|
||||
expect(bigIntBitLength(255n)).eq(8)
|
||||
expect(bigIntBitLength(256n)).eq(9)
|
||||
})
|
||||
})
|
||||
|
||||
describe('bigIntToBuffer', () => {
|
||||
it('should handle writing to BE', () => {
|
||||
|
@ -63,3 +84,111 @@ describe('bufferToBigInt', () => {
|
|||
expect(bufferToBigInt(buf.reverse(), true).toString()).eq(num.toString())
|
||||
})
|
||||
})
|
||||
|
||||
describe('randomBigInt', () => {
|
||||
it('should return a random bigint', () => {
|
||||
const a = randomBigInt(32)
|
||||
const b = randomBigInt(32)
|
||||
|
||||
expect(a).not.toEqual(b)
|
||||
})
|
||||
|
||||
it('should return a random bigint up to specified byte length', () => {
|
||||
const a = randomBigInt(32)
|
||||
const b = randomBigInt(64)
|
||||
|
||||
expect(bigIntBitLength(a)).toBeLessThanOrEqual(32 * 8)
|
||||
expect(bigIntBitLength(b)).toBeLessThanOrEqual(64 * 8)
|
||||
})
|
||||
})
|
||||
|
||||
describe('randomBigIntBits', () => {
|
||||
it('should return a random bigint', () => {
|
||||
const a = randomBigIntBits(32)
|
||||
const b = randomBigIntBits(32)
|
||||
|
||||
expect(a).not.toEqual(b)
|
||||
})
|
||||
|
||||
it('should return a random bigint up to specified bit length', () => {
|
||||
const a = randomBigIntBits(32)
|
||||
const b = randomBigIntBits(64)
|
||||
|
||||
expect(bigIntBitLength(a)).toBeLessThanOrEqual(32)
|
||||
expect(bigIntBitLength(b)).toBeLessThanOrEqual(64)
|
||||
})
|
||||
})
|
||||
|
||||
describe('randomBigIntInRange', () => {
|
||||
it('should return a random bigint', () => {
|
||||
const a = randomBigIntInRange(10000n)
|
||||
const b = randomBigIntInRange(10000n)
|
||||
|
||||
expect(a).not.toEqual(b)
|
||||
})
|
||||
|
||||
it('should return a bigint within a given range', () => {
|
||||
const a = randomBigIntInRange(200n, 100n)
|
||||
|
||||
expect(a).toBeGreaterThanOrEqual(100n)
|
||||
expect(a).toBeLessThan(200n)
|
||||
})
|
||||
})
|
||||
|
||||
describe('twoMultiplicity', () => {
|
||||
it('should return the multiplicity of 2 in the prime factorization of n', () => {
|
||||
expect(twoMultiplicity(0n)).toEqual(0n)
|
||||
expect(twoMultiplicity(1n)).toEqual(0n)
|
||||
expect(twoMultiplicity(2n)).toEqual(1n)
|
||||
expect(twoMultiplicity(4n)).toEqual(2n)
|
||||
expect(twoMultiplicity(65536n)).toEqual(16n)
|
||||
expect(twoMultiplicity(65537n)).toEqual(0n)
|
||||
})
|
||||
})
|
||||
|
||||
describe('bigIntGcd', () => {
|
||||
it('should return the greatest common divisor of a and b', () => {
|
||||
expect(bigIntGcd(123n, 456n)).toEqual(3n)
|
||||
})
|
||||
|
||||
it('should correctly handle zeros', () => {
|
||||
expect(bigIntGcd(0n, 0n)).toEqual(0n)
|
||||
expect(bigIntGcd(0n, 1n)).toEqual(1n)
|
||||
expect(bigIntGcd(1n, 0n)).toEqual(1n)
|
||||
})
|
||||
|
||||
it('should correctly handle equal values', () => {
|
||||
expect(bigIntGcd(1n, 1n)).toEqual(1n)
|
||||
})
|
||||
})
|
||||
|
||||
describe('bigIntModPow', () => {
|
||||
it('should correctly calculate modular exponentiation', () => {
|
||||
expect(bigIntModPow(2n, 3n, 5n)).toEqual(3n)
|
||||
expect(bigIntModPow(2n, 3n, 6n)).toEqual(2n)
|
||||
expect(bigIntModPow(2n, 3n, 7n)).toEqual(1n)
|
||||
expect(bigIntModPow(2n, 3n, 8n)).toEqual(0n)
|
||||
})
|
||||
|
||||
it('should correctly handle very large numbers', () => {
|
||||
// calculating this with BigInt would either take forever or error with "Maximum BigInt size exceeded
|
||||
expect(bigIntModPow(2n, 100000000000n, 100n)).toEqual(76n)
|
||||
})
|
||||
})
|
||||
|
||||
describe('bigIntModInv', () => {
|
||||
it('should correctly calculate modular inverse', () => {
|
||||
expect(bigIntModInv(2n, 5n)).toEqual(3n)
|
||||
expect(bigIntModInv(2n, 7n)).toEqual(4n)
|
||||
})
|
||||
|
||||
it("should error if there's no modular inverse", () => {
|
||||
expect(() => bigIntModInv(2n, 6n)).toThrow(RangeError)
|
||||
expect(() => bigIntModInv(2n, 8n)).toThrow(RangeError)
|
||||
})
|
||||
|
||||
it('should correctly handle very large numbers', () => {
|
||||
// calculating this with BigInt would either take forever or error with "Maximum BigInt size exceeded
|
||||
expect(bigIntModInv(123123123123n, 1829n)).toEqual(318n)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
|
||||
|
||||
|
@ -121,6 +121,19 @@ describe('concatBuffers', () => {
|
|||
buf[0] = 0xff
|
||||
expect(buf1[0]).not.eql(0xff)
|
||||
})
|
||||
|
||||
it('should work without native Buffer', () => {
|
||||
vi.stubGlobal('Buffer', undefined)
|
||||
const buf1 = new Uint8Array([1, 2, 3])
|
||||
const buf2 = new Uint8Array([4, 5, 6])
|
||||
const buf = concatBuffers([buf1, buf2])
|
||||
|
||||
buf1[0] = 0xff
|
||||
|
||||
expect([...buf]).eql([1, 2, 3, 4, 5, 6])
|
||||
})
|
||||
|
||||
afterEach(() => void vi.unstubAllGlobals())
|
||||
})
|
||||
|
||||
describe('bufferToReversed', () => {
|
||||
|
|
36
packages/core/src/utils/condition-variable.test.ts
Normal file
36
packages/core/src/utils/condition-variable.test.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { ConditionVariable } from './condition-variable.js'
|
||||
|
||||
describe('ConditionVariable', () => {
|
||||
it('should correctly unlock execution', async () => {
|
||||
const cv = new ConditionVariable()
|
||||
|
||||
setTimeout(() => cv.notify(), 10)
|
||||
|
||||
await cv.wait()
|
||||
|
||||
expect(true).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should correctly time out', async () => {
|
||||
const cv = new ConditionVariable()
|
||||
|
||||
await cv.wait(10)
|
||||
|
||||
expect(true).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should only unlock once', async () => {
|
||||
const cv = new ConditionVariable()
|
||||
|
||||
setTimeout(() => {
|
||||
cv.notify()
|
||||
cv.notify()
|
||||
}, 10)
|
||||
|
||||
await cv.wait()
|
||||
|
||||
expect(true).toBeTruthy()
|
||||
})
|
||||
})
|
17
packages/core/src/utils/controllable-promise.test.ts
Normal file
17
packages/core/src/utils/controllable-promise.test.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { createControllablePromise } from './controllable-promise.js'
|
||||
|
||||
describe('createControllablePromise', () => {
|
||||
it('should resolve', async () => {
|
||||
const p = createControllablePromise()
|
||||
p.resolve(1)
|
||||
await expect(p).resolves.toBe(1)
|
||||
})
|
||||
|
||||
it('should reject', async () => {
|
||||
const p = createControllablePromise()
|
||||
p.reject(1)
|
||||
await expect(p).rejects.toBe(1)
|
||||
})
|
||||
})
|
|
@ -1,8 +1,10 @@
|
|||
import { beforeAll, expect, it } from 'vitest'
|
||||
import { gzipSync, inflateSync } from 'zlib'
|
||||
|
||||
import { hexDecodeToBuffer, hexEncode, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
|
||||
|
||||
import { ICryptoProvider } from './abstract.js'
|
||||
import { factorizePQSync } from './factorization.js'
|
||||
|
||||
export function testCryptoProvider(c: ICryptoProvider): void {
|
||||
beforeAll(() => c.initialize?.())
|
||||
|
@ -93,4 +95,46 @@ export function testCryptoProvider(c: ICryptoProvider): void {
|
|||
),
|
||||
).to.eq('99706487a1cde613bc6de0b6f24b1c7aa448c8b9c3403e3467a8cad89340f53b')
|
||||
})
|
||||
|
||||
it(
|
||||
'should decompose PQ to prime factors P and Q',
|
||||
() => {
|
||||
const testFactorization = (pq: string, p: string, q: string) => {
|
||||
const [p1, q1] = factorizePQSync(hexDecodeToBuffer(pq))
|
||||
expect(hexEncode(p1)).eq(p.toLowerCase())
|
||||
expect(hexEncode(q1)).eq(q.toLowerCase())
|
||||
}
|
||||
|
||||
// from samples at https://core.telegram.org/mtproto/samples-auth_key
|
||||
testFactorization('17ED48941A08F981', '494C553B', '53911073')
|
||||
// random example
|
||||
testFactorization('14fcab4dfc861f45', '494c5c99', '494c778d')
|
||||
},
|
||||
// since PQ factorization relies on RNG, it may take a while (or may not!)
|
||||
{ timeout: 10000 },
|
||||
)
|
||||
|
||||
it('should correctly gzip', () => {
|
||||
const data = new Uint8Array(1000).fill(0x42)
|
||||
|
||||
const compressed = c.gzip(data, 100)
|
||||
|
||||
expect(compressed).not.toBeNull()
|
||||
|
||||
const decompressed = inflateSync(compressed!)
|
||||
|
||||
expect(compressed!.length).toBeLessThan(data.length)
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
expect(decompressed).toEqual(Buffer.from(data))
|
||||
})
|
||||
|
||||
it('should correctly gunzip', () => {
|
||||
const data = new Uint8Array(1000).fill(0x42)
|
||||
|
||||
const compressed = gzipSync(data)
|
||||
const decompressed = c.gunzip(compressed)
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
expect(Buffer.from(decompressed)).toEqual(Buffer.from(data))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime'
|
||||
|
||||
import { factorizePQSync } from './factorization.js'
|
||||
|
||||
describe(
|
||||
'prime factorization',
|
||||
function () {
|
||||
it('should decompose PQ to prime factors P and Q', () => {
|
||||
const testFactorization = (pq: string, p: string, q: string) => {
|
||||
const [p1, q1] = factorizePQSync(hexDecodeToBuffer(pq))
|
||||
expect(hexEncode(p1)).eq(p.toLowerCase())
|
||||
expect(hexEncode(q1)).eq(q.toLowerCase())
|
||||
}
|
||||
|
||||
// from samples at https://core.telegram.org/mtproto/samples-auth_key
|
||||
testFactorization('17ED48941A08F981', '494C553B', '53911073')
|
||||
// random example
|
||||
testFactorization('14fcab4dfc861f45', '494c5c99', '494c778d')
|
||||
})
|
||||
},
|
||||
{ timeout: 10000 },
|
||||
) // since PQ factorization relies on RNG, it may take a while (or may not!)
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { parsePublicKey } from '../index.js'
|
||||
import { findKeyByFingerprints, parsePublicKey } from '../index.js'
|
||||
import { NodeCryptoProvider } from './node.js'
|
||||
|
||||
const crypto = new NodeCryptoProvider()
|
||||
|
@ -27,4 +27,23 @@ XGF710w9lwCGNbmNxNYhtIkdqfsEcwR5JwIDAQAB
|
|||
old: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should be able to find a key by its fingerprint', () => {
|
||||
expect(findKeyByFingerprints(['b25898df208d2603'])).toMatchInlineSnapshot(`
|
||||
{
|
||||
"exponent": "010001",
|
||||
"fingerprint": "b25898df208d2603",
|
||||
"modulus": "c8c11d635691fac091dd9489aedced2932aa8a0bcefef05fa800892d9b52ed03200865c9e97211cb2ee6c7ae96d3fb0e15aeffd66019b44a08a240cfdd2868a85e1f54d6fa5deaa041f6941ddf302690d61dc476385c2fa655142353cb4e4b59f6e5b6584db76fe8b1370263246c010c93d011014113ebdf987d093f9d37c2be48352d69a1683f8f6e6c2167983c761e3ab169fde5daaa12123fa1beab621e4da5935e9c198f82f35eae583a99386d8110ea6bd1abb0f568759f62694419ea5f69847c43462abef858b4cb5edc84e7b9226cd7bd7e183aa974a712c079dde85b9dc063b8a5c08e8f859c0ee5dcd824c7807f20153361a7f63cfd2a433a1be7f5",
|
||||
"old": false,
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should prefer new keys over old', () => {
|
||||
expect(findKeyByFingerprints(['71e025b6c76033e3', 'b25898df208d2603'])?.fingerprint).toEqual('b25898df208d2603')
|
||||
})
|
||||
|
||||
it('should return null if not found', () => {
|
||||
expect(findKeyByFingerprints(['deadbeefdeadbeef'])).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
76
packages/core/src/utils/crypto/password.test.ts
Normal file
76
packages/core/src/utils/crypto/password.test.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { tl } from '@mtcute/tl'
|
||||
import { hexDecodeToBuffer, hexEncode, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
|
||||
|
||||
import { computePasswordHash, defaultCryptoProviderFactory } from './index.js'
|
||||
|
||||
// a real-world request from an account with "qwe123" password
|
||||
const fakeAlgo: tl.RawPasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow = {
|
||||
_: 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow',
|
||||
salt1: hexDecodeToBuffer('9b3accc457c0d5288e8cff31eb21094048bc11902f6614dbb9afb839ee7641c37619537d8ebe749e'),
|
||||
salt2: hexDecodeToBuffer('6c619bb0786dc4ed1bf211d23f6e4065'),
|
||||
g: 3,
|
||||
p: hexDecodeToBuffer(
|
||||
'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f' +
|
||||
'48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c37' +
|
||||
'20fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f64' +
|
||||
'2477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4' +
|
||||
'a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754' +
|
||||
'fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4' +
|
||||
'e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f' +
|
||||
'0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b',
|
||||
),
|
||||
}
|
||||
// const fakeRequest: tl.account.RawPassword = {
|
||||
// _: 'account.password',
|
||||
// hasRecovery: false,
|
||||
// hasSecureValues: false,
|
||||
// hasPassword: true,
|
||||
// currentAlgo: fakeAlgo,
|
||||
// srpB: hexDecodeToBuffer(
|
||||
// '1476a7b5991d7f028bbee33b3455cad3f2cd0eb3737409fcce92fa7d4cd5c733' +
|
||||
// 'ec6d2cb3454e587d4c17eda2fd7ef9a57327215f38292cc8bd5dc77d3e1d31cd' +
|
||||
// 'dae2652f8347c4b0093f7c78242f70e6cc13137ee7acc257a49855a63113db8f' +
|
||||
// '163992b9101551f3b6f7eb5d196cee3647c359553b1bcbe82ba8933c0fb1ac35' +
|
||||
// '0243c535b8e634613e1f626ba8a6d141ef957c859e71a117b557c0298bfbb107' +
|
||||
// 'c91f71f5b4275fded58289aa1e87c612f44b7aa0b5e0de7def4458f58db80019' +
|
||||
// 'd2e7b181eb66dc270374af2d160dd0c53edd677b2701694d71ea8718c49df6a9' +
|
||||
// 'dbe2cbae051ffc1986336cd26f11a8ab426dfe0813d7b3f4eedf4e34182ccc3a',
|
||||
// ),
|
||||
// srpId: Long.fromBits(-2046015018, 875006452),
|
||||
// newAlgo: {
|
||||
// _: 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow',
|
||||
// salt1: hexDecodeToBuffer('9b3accc457c0d528'),
|
||||
// salt2: hexDecodeToBuffer('6c619bb0786dc4ed1bf211d23f6e4065'),
|
||||
// g: 3,
|
||||
// p: hexDecodeToBuffer(
|
||||
// 'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f' +
|
||||
// '48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c37' +
|
||||
// '20fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f64' +
|
||||
// '2477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4' +
|
||||
// 'a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754' +
|
||||
// 'fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4' +
|
||||
// 'e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f' +
|
||||
// '0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b',
|
||||
// ),
|
||||
// },
|
||||
// newSecureAlgo: {
|
||||
// _: 'securePasswordKdfAlgoPBKDF2HMACSHA512iter100000',
|
||||
// salt: hexDecodeToBuffer('fdd59abc0bffb24d'),
|
||||
// },
|
||||
// secureRandom: new Uint8Array(), // unused
|
||||
// }
|
||||
const password = utf8EncodeToBuffer('qwe123')
|
||||
|
||||
describe('computePasswordHash', () => {
|
||||
const crypto = defaultCryptoProviderFactory()
|
||||
|
||||
it('should correctly compute password hash as defined by MTProto', async () => {
|
||||
const actual = await computePasswordHash(crypto, password, fakeAlgo.salt1, fakeAlgo.salt2)
|
||||
|
||||
expect(hexEncode(actual)).toEqual('750f1fe282965e63ce17b98427b35549fb864465211840f6a7c1f2fb657cc33b')
|
||||
})
|
||||
|
||||
// todo: computeNewPasswordHash and computeSrpParams both require predictable random
|
||||
})
|
173
packages/core/src/utils/deque.test.ts
Normal file
173
packages/core/src/utils/deque.test.ts
Normal file
|
@ -0,0 +1,173 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { Deque } from './deque.js'
|
||||
|
||||
describe('Deque', () => {
|
||||
function setupWrapping() {
|
||||
const d = new Deque<number>(Infinity, /* capacity: */ 8)
|
||||
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
d.pushBack(i)
|
||||
}
|
||||
|
||||
// [1, 2, 3, 4, 5, 6, 7, _]
|
||||
|
||||
d.popFront()
|
||||
d.popFront()
|
||||
d.popFront()
|
||||
|
||||
// [_, _, _, 4, 5, 6, 7, _]
|
||||
|
||||
d.pushBack(8)
|
||||
d.pushBack(9)
|
||||
d.pushBack(10)
|
||||
|
||||
// [9, 10, _, 4, 5, 6, 7, 8]
|
||||
return d
|
||||
}
|
||||
|
||||
it('should push items to the end correctly', () => {
|
||||
const d = new Deque<number>()
|
||||
|
||||
d.pushBack(1)
|
||||
d.pushBack(2)
|
||||
d.pushBack(3)
|
||||
|
||||
expect(d.toArray()).eql([1, 2, 3])
|
||||
expect([...d.iter()]).eql([1, 2, 3])
|
||||
})
|
||||
|
||||
it('should push items to the start correctly', () => {
|
||||
const d = new Deque<number>()
|
||||
|
||||
d.pushFront(1)
|
||||
d.pushFront(2)
|
||||
d.pushFront(3)
|
||||
|
||||
expect(d.toArray()).eql([3, 2, 1])
|
||||
})
|
||||
|
||||
it('should pop items from the end correctly', () => {
|
||||
const d = new Deque<number>()
|
||||
|
||||
d.pushBack(1)
|
||||
d.pushBack(2)
|
||||
d.pushBack(3)
|
||||
|
||||
expect(d.popBack()).eql(3)
|
||||
expect(d.popBack()).eql(2)
|
||||
expect(d.popBack()).eql(1)
|
||||
})
|
||||
|
||||
it('should pop items from the start correctly', () => {
|
||||
const d = new Deque<number>()
|
||||
|
||||
d.pushFront(1)
|
||||
d.pushFront(2)
|
||||
d.pushFront(3)
|
||||
|
||||
expect(d.popFront()).eql(3)
|
||||
expect(d.popFront()).eql(2)
|
||||
expect(d.popFront()).eql(1)
|
||||
})
|
||||
|
||||
it('should return the correct length', () => {
|
||||
const d = new Deque<number>()
|
||||
|
||||
d.pushBack(1)
|
||||
d.pushBack(2)
|
||||
d.pushBack(3)
|
||||
|
||||
expect(d.length).eql(3)
|
||||
})
|
||||
|
||||
it('accessors should correctly wrap around', () => {
|
||||
const d = setupWrapping()
|
||||
|
||||
expect(d.toArray()).eql([4, 5, 6, 7, 8, 9, 10])
|
||||
expect([...d.iter()]).eql([4, 5, 6, 7, 8, 9, 10])
|
||||
expect(d.at(6)).eql(10)
|
||||
})
|
||||
|
||||
it('should handle maxLength by removing items from the other end', () => {
|
||||
const d = new Deque<number>(4)
|
||||
|
||||
d.pushBack(1)
|
||||
d.pushBack(2)
|
||||
d.pushBack(3)
|
||||
d.pushBack(4)
|
||||
|
||||
d.pushBack(5)
|
||||
expect(d.toArray()).eql([2, 3, 4, 5])
|
||||
|
||||
d.pushFront(6)
|
||||
expect(d.toArray()).eql([6, 2, 3, 4])
|
||||
})
|
||||
|
||||
it('should correctly resize', () => {
|
||||
const d = new Deque<number>(Infinity, /* capacity: */ 8)
|
||||
|
||||
for (let i = 0; i <= 16; i++) {
|
||||
d.pushBack(i)
|
||||
}
|
||||
|
||||
const expected = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||||
|
||||
expect(d.length).eql(17)
|
||||
expect(d.toArray()).eql(expected)
|
||||
expect([...d.iter()]).eql(expected)
|
||||
expect(d.at(16)).eql(16)
|
||||
})
|
||||
|
||||
it('should correctly remove items', () => {
|
||||
const d = new Deque<number>(Infinity, /* capacity: */ 16)
|
||||
|
||||
d.pushBack(1)
|
||||
d.pushBack(2)
|
||||
d.pushBack(3)
|
||||
d.pushBack(4)
|
||||
d.pushBack(5)
|
||||
|
||||
d.remove(1)
|
||||
d.remove(4)
|
||||
|
||||
expect(d.toArray()).eql([2, 3, 5])
|
||||
})
|
||||
|
||||
it('should correctly remove items by predicate (the first matched)', () => {
|
||||
const d = new Deque<number>(Infinity, /* capacity: */ 4)
|
||||
|
||||
d.pushBack(1)
|
||||
d.pushBack(2)
|
||||
d.pushBack(3)
|
||||
d.pushBack(4)
|
||||
d.pushBack(5)
|
||||
|
||||
d.removeBy((it) => it >= 2)
|
||||
|
||||
expect(d.toArray()).eql([1, 3, 4, 5])
|
||||
})
|
||||
|
||||
it('should correctly peek at edges', () => {
|
||||
const d = new Deque<number>()
|
||||
|
||||
d.pushBack(1)
|
||||
d.pushBack(2)
|
||||
|
||||
expect(d.peekFront()).eql(1)
|
||||
expect(d.peekBack()).eql(2)
|
||||
})
|
||||
|
||||
it('should correctly clear', () => {
|
||||
const d = new Deque<number>()
|
||||
|
||||
d.pushBack(1)
|
||||
d.pushBack(2)
|
||||
|
||||
d.clear()
|
||||
|
||||
expect(d.length).eql(0)
|
||||
expect(d.toArray()).eql([])
|
||||
expect([...d.iter()]).eql([])
|
||||
})
|
||||
})
|
|
@ -24,14 +24,16 @@ export class Deque<T> {
|
|||
protected _capacity: number
|
||||
|
||||
constructor(
|
||||
minCapacity = MIN_INITIAL_CAPACITY,
|
||||
readonly maxLength = Infinity,
|
||||
minCapacity = maxLength === Infinity ? MIN_INITIAL_CAPACITY : maxLength,
|
||||
) {
|
||||
let capacity = minCapacity
|
||||
|
||||
if (capacity >= MIN_INITIAL_CAPACITY) {
|
||||
if (capacity < MIN_INITIAL_CAPACITY) {
|
||||
capacity = MIN_INITIAL_CAPACITY
|
||||
}
|
||||
if (capacity !== MIN_INITIAL_CAPACITY) {
|
||||
// Find the best power of two to hold elements.
|
||||
// >= because array can't be full
|
||||
capacity |= capacity >>> 1
|
||||
capacity |= capacity >>> 2
|
||||
capacity |= capacity >>> 4
|
||||
|
@ -135,6 +137,8 @@ export class Deque<T> {
|
|||
|
||||
toArray(): T[] {
|
||||
const sz = this.length
|
||||
if (sz === 0) return []
|
||||
|
||||
const arr = new Array(sz)
|
||||
|
||||
if (this._head < this._tail) {
|
||||
|
@ -220,6 +224,7 @@ export class Deque<T> {
|
|||
|
||||
*iter(): IterableIterator<T> {
|
||||
const sz = this.length
|
||||
if (sz === 0) return
|
||||
|
||||
if (this._head < this._tail) {
|
||||
// head to tail
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
// based on https://github.dev/tdlib/td/blob/master/tdutils/td/utils/FloodControlStrict.h
|
||||
|
||||
interface FloodControlLimit {
|
||||
duration: number
|
||||
count: number
|
||||
pos: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Flood limiter, based on TDlib
|
||||
*/
|
||||
export class FloodControl {
|
||||
private _wakeupAt = 1
|
||||
private _withoutUpdate = 0
|
||||
private _events: number[] = []
|
||||
// pair: duration, count
|
||||
private _limits: FloodControlLimit[] = []
|
||||
|
||||
// no more than count in each duration
|
||||
addLimit(duration: number, count: number): void {
|
||||
this._limits.push({ duration, count, pos: 0 })
|
||||
this._withoutUpdate = 0
|
||||
}
|
||||
|
||||
addEvent(now: number): void {
|
||||
this._events.push(now)
|
||||
|
||||
if (this._withoutUpdate > 0) {
|
||||
this._withoutUpdate -= 1
|
||||
} else {
|
||||
this._update(now)
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._events.length = 0
|
||||
this._withoutUpdate = 0
|
||||
this._wakeupAt = 1
|
||||
this._limits.forEach((limit) => (limit.pos = 0))
|
||||
}
|
||||
|
||||
get wakeupAt(): number {
|
||||
return this._wakeupAt
|
||||
}
|
||||
|
||||
private _update(now: number): void {
|
||||
let minPos = this._events.length
|
||||
this._withoutUpdate = Infinity
|
||||
|
||||
this._limits.forEach((limit) => {
|
||||
if (limit.count < this._events.length - limit.pos) {
|
||||
limit.pos = this._events.length - limit.count
|
||||
}
|
||||
|
||||
while (limit.pos < this._events.length && this._events[limit.pos] + limit.duration < now) {
|
||||
limit.pos += 1
|
||||
}
|
||||
|
||||
if (limit.count + limit.pos < this._events.length) {
|
||||
this._wakeupAt = Math.max(this._wakeupAt, this._events[limit.pos] + limit.duration)
|
||||
this._withoutUpdate = 0
|
||||
} else {
|
||||
this._withoutUpdate = Math.min(this._withoutUpdate, limit.count + limit.pos - this._events.length - 1)
|
||||
}
|
||||
|
||||
minPos = Math.min(minPos, limit.pos)
|
||||
})
|
||||
|
||||
if (minPos * 2 > this._events.length) {
|
||||
this._limits.forEach((limit) => (limit.pos -= minPos))
|
||||
this._events.splice(0, minPos)
|
||||
}
|
||||
}
|
||||
}
|
27
packages/core/src/utils/function-utils.test.ts
Normal file
27
packages/core/src/utils/function-utils.test.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { ConditionVariable } from './condition-variable.js'
|
||||
import { throttle } from './function-utils.js'
|
||||
|
||||
describe('throttle', () => {
|
||||
it('should throttle', async () => {
|
||||
const cv = new ConditionVariable()
|
||||
let count = 0
|
||||
|
||||
const func = () => {
|
||||
count++
|
||||
cv.notify()
|
||||
}
|
||||
|
||||
const throttled = throttle(func, 10)
|
||||
|
||||
throttled()
|
||||
throttled()
|
||||
throttled()
|
||||
throttled()
|
||||
|
||||
await cv.wait()
|
||||
|
||||
expect(count).eq(1)
|
||||
})
|
||||
})
|
|
@ -7,7 +7,6 @@ export * from './crypto/index.js'
|
|||
export * from './default-dcs.js'
|
||||
export * from './deque.js'
|
||||
export * from './early-timer.js'
|
||||
export * from './flood-control.js'
|
||||
export * from './function-utils.js'
|
||||
export * from './linked-list.js'
|
||||
export * from './links/index.js'
|
||||
|
|
47
packages/core/src/utils/linked-list.test.ts
Normal file
47
packages/core/src/utils/linked-list.test.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { SortedLinkedList } from './linked-list.js'
|
||||
|
||||
describe('SortedLinkedList', () => {
|
||||
const ascendingComparator = (a: number, b: number) => a - b
|
||||
|
||||
it('should add items in the correct order', () => {
|
||||
const list = new SortedLinkedList(ascendingComparator)
|
||||
|
||||
list.add(3)
|
||||
list.add(1)
|
||||
list.add(2)
|
||||
|
||||
expect(list.popFront()).eq(1)
|
||||
expect(list.popFront()).eq(2)
|
||||
expect(list.popFront()).eq(3)
|
||||
})
|
||||
|
||||
it('should allow deleting nodes using _remove', () => {
|
||||
const list = new SortedLinkedList(ascendingComparator)
|
||||
|
||||
list.add(3)
|
||||
list.add(1)
|
||||
list.add(2)
|
||||
|
||||
const node = list._first!.n!
|
||||
|
||||
list._remove(node)
|
||||
|
||||
expect(list.popFront()).eq(1)
|
||||
expect(list.popFront()).eq(3)
|
||||
})
|
||||
|
||||
it('should clear', () => {
|
||||
const list = new SortedLinkedList(ascendingComparator)
|
||||
|
||||
list.add(3)
|
||||
list.add(1)
|
||||
list.add(2)
|
||||
|
||||
list.clear()
|
||||
|
||||
expect(list.length).eq(0)
|
||||
expect(list.popFront()).eq(undefined)
|
||||
})
|
||||
})
|
173
packages/core/src/utils/logger.test.ts
Normal file
173
packages/core/src/utils/logger.test.ts
Normal file
|
@ -0,0 +1,173 @@
|
|||
import Long from 'long'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LogManager } from './logger.js'
|
||||
|
||||
describe('logger', () => {
|
||||
const createManager = () => {
|
||||
const mgr = new LogManager()
|
||||
|
||||
const spy = vi.fn<Parameters<typeof mgr.handler>>()
|
||||
mgr.handler = spy
|
||||
|
||||
return [mgr, spy] as const
|
||||
}
|
||||
|
||||
it('should only log messages below the set level', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
mgr.level = LogManager.INFO
|
||||
mgr.error('test error')
|
||||
mgr.warn('test warn')
|
||||
mgr.info('test info')
|
||||
mgr.debug('test debug')
|
||||
mgr.verbose('test verbose')
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(3)
|
||||
expect(spy).toHaveBeenCalledWith(3, 1, 'base', 'test error', [])
|
||||
expect(spy).toHaveBeenCalledWith(3, 2, 'base', 'test warn', [])
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', 'test info', [])
|
||||
})
|
||||
|
||||
it('should create child loggers', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
mgr.create('child').info('test info')
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(0, 3, 'child', 'test info', [])
|
||||
})
|
||||
|
||||
it('should apply filtering by tags', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
const test1 = mgr.create('test1')
|
||||
const test2 = mgr.create('test2')
|
||||
|
||||
mgr.filter((tag) => tag === 'test1')
|
||||
|
||||
test1.info('test1 info')
|
||||
test2.info('test2 info')
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
expect(spy).toHaveBeenCalledWith(5, 3, 'test1', 'test1 info', [])
|
||||
})
|
||||
|
||||
it('should recursively add prefixes', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
const test = mgr.create('test1')
|
||||
const test2 = test.create('test2')
|
||||
|
||||
test.prefix = '[TEST] '
|
||||
test2.prefix = '[TEST2] '
|
||||
|
||||
test2.create('test3').info('test info')
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(1, 3, 'test3', '[TEST] [TEST2] test info', [])
|
||||
})
|
||||
|
||||
describe('formatting', () => {
|
||||
it('should pass generic format strings through', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
mgr.info('test %s', 'info')
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', 'test %s', ['info'])
|
||||
})
|
||||
|
||||
describe('%h', () => {
|
||||
it('should format buffers as hex strings', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
mgr.info('test %h', new Uint8Array([0x01, 0x02, 0x03]))
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', 'test 010203', [])
|
||||
})
|
||||
|
||||
it('should format numbers as hex', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
mgr.info('test %h', 0x010203)
|
||||
mgr.info('test bigint %h', 0x010203n)
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', 'test 10203', [])
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', 'test bigint 10203', [])
|
||||
})
|
||||
|
||||
it('should format everything else as normal strings', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
mgr.info('test %h', {})
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', 'test [object Object]', [])
|
||||
})
|
||||
})
|
||||
|
||||
describe('%b', () => {
|
||||
it('should format booleans as strings', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
mgr.info('test %b', true)
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', 'test true', [])
|
||||
})
|
||||
|
||||
it('should coerce everything to boolean', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
mgr.info('test %b', 123)
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', 'test true', [])
|
||||
})
|
||||
})
|
||||
|
||||
describe('%j', () => {
|
||||
it('should format objects as JSON', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
mgr.info('test %j', { a: 1 })
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', 'test {"a":1}', [])
|
||||
})
|
||||
|
||||
it('should format buffers inside as hex strings', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
mgr.info('test %j', { a: Buffer.from([1, 2, 3]) })
|
||||
mgr.info('test Uint8Array %j', { a: new Uint8Array([1, 2, 3]) })
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', 'test {"a":"010203"}', [])
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', 'test Uint8Array {"a":"010203"}', [])
|
||||
})
|
||||
|
||||
it('should trim long buffers', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
const _150hexZeros = Array(150).fill('00').join('')
|
||||
|
||||
mgr.info('test %j', { a: new Uint8Array(300) })
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', `test {"a":"${_150hexZeros}..."}`, [])
|
||||
})
|
||||
|
||||
it('should have %J variation accepting iterators', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
mgr.info('test %J', new Set([1, 2, 3]))
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', 'test [1,2,3]', [])
|
||||
})
|
||||
})
|
||||
|
||||
describe('%l', () => {
|
||||
it('should format Longs as strings', () => {
|
||||
const [mgr, spy] = createManager()
|
||||
|
||||
mgr.info('test %l', Long.fromInt(123))
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(3, 3, 'base', 'test 123', [])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -4,6 +4,7 @@ import { _defaultLoggingHandler } from './platform/logging.js'
|
|||
|
||||
let defaultLogLevel = 3
|
||||
|
||||
/* c8 ignore start */
|
||||
if (typeof process !== 'undefined') {
|
||||
const envLogLevel = parseInt(process.env.MTCUTE_LOG_LEVEL ?? '')
|
||||
|
||||
|
@ -17,6 +18,7 @@ if (typeof process !== 'undefined') {
|
|||
defaultLogLevel = localLogLevel
|
||||
}
|
||||
}
|
||||
/* c8 ignore end */
|
||||
|
||||
const FORMATTER_RE = /%[a-zA-Z]/g
|
||||
|
||||
|
@ -79,7 +81,7 @@ export class Logger {
|
|||
|
||||
if (m === '%h') {
|
||||
if (ArrayBuffer.isView(val)) return hexEncode(val as Uint8Array)
|
||||
if (typeof val === 'number') return val.toString(16)
|
||||
if (typeof val === 'number' || typeof val === 'bigint') return val.toString(16)
|
||||
|
||||
return String(val)
|
||||
}
|
||||
|
@ -165,10 +167,6 @@ export class LogManager extends Logger {
|
|||
level = defaultLogLevel
|
||||
handler = _defaultLoggingHandler
|
||||
|
||||
disable(): void {
|
||||
this.level = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link Logger} with the given tag
|
||||
*
|
||||
|
|
221
packages/core/src/utils/long-utils.test.ts
Normal file
221
packages/core/src/utils/long-utils.test.ts
Normal file
|
@ -0,0 +1,221 @@
|
|||
import Long from 'long'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
longFromBuffer,
|
||||
longFromFastString,
|
||||
LongMap,
|
||||
LongSet,
|
||||
longToFastString,
|
||||
randomLong,
|
||||
removeFromLongArray,
|
||||
} from './long-utils.js'
|
||||
|
||||
describe('randomLong', () => {
|
||||
it('should return a random Long', () => {
|
||||
const long = randomLong()
|
||||
const long2 = randomLong()
|
||||
|
||||
expect(long).toBeInstanceOf(Long)
|
||||
expect(long.eq(long2)).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('longFromBuffer', () => {
|
||||
it('should correctly read LE longs', () => {
|
||||
expect(longFromBuffer(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]))).toEqual(Long.fromInt(0))
|
||||
expect(longFromBuffer(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]))).toEqual(Long.fromBits(0x04030201, 0x08070605))
|
||||
})
|
||||
|
||||
it('should correctly read BE longs', () => {
|
||||
expect(longFromBuffer(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]), false, false)).toEqual(Long.fromInt(0))
|
||||
expect(longFromBuffer(new Uint8Array([8, 7, 6, 5, 4, 3, 2, 1]), false, false)).toEqual(
|
||||
Long.fromBits(0x04030201, 0x08070605),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeFromLongArray', () => {
|
||||
it('should remove a Long from an array', () => {
|
||||
const arr = [Long.fromInt(1), Long.fromInt(2), Long.fromInt(3)]
|
||||
|
||||
expect(removeFromLongArray(arr, Long.fromInt(2))).toBeTruthy()
|
||||
expect(arr).toEqual([Long.fromInt(1), Long.fromInt(3)])
|
||||
})
|
||||
|
||||
it('should return false if the Long was not found', () => {
|
||||
const arr = [Long.fromInt(1), Long.fromInt(2), Long.fromInt(3)]
|
||||
|
||||
expect(removeFromLongArray(arr, Long.fromInt(4))).toBeFalsy()
|
||||
expect(arr).toEqual([Long.fromInt(1), Long.fromInt(2), Long.fromInt(3)])
|
||||
})
|
||||
|
||||
it('should only remove one matching element', () => {
|
||||
const arr = [Long.fromInt(1), Long.fromInt(2), Long.fromInt(2), Long.fromInt(3)]
|
||||
|
||||
expect(removeFromLongArray(arr, Long.fromInt(2))).toBeTruthy()
|
||||
expect(arr).toEqual([Long.fromInt(1), Long.fromInt(2), Long.fromInt(3)])
|
||||
})
|
||||
})
|
||||
|
||||
describe('longToFastString', () => {
|
||||
it('should correctly serialize a Long', () => {
|
||||
expect(longToFastString(Long.fromInt(0))).toEqual('0|0')
|
||||
expect(longToFastString(Long.fromBits(123, 456))).toEqual('123|456')
|
||||
})
|
||||
|
||||
it('should work with negative numbers', () => {
|
||||
expect(longToFastString(Long.fromInt(-1))).toEqual('-1|-1')
|
||||
expect(longToFastString(Long.fromBits(-123, -456))).toEqual('-123|-456')
|
||||
})
|
||||
})
|
||||
|
||||
describe('longFromFastString', () => {
|
||||
it('should correctly deserialize a Long', () => {
|
||||
expect(longFromFastString('0|0')).toEqual(Long.fromInt(0))
|
||||
expect(longFromFastString('123|456')).toEqual(Long.fromBits(123, 456))
|
||||
})
|
||||
|
||||
it('should work with negative numbers', () => {
|
||||
expect(longFromFastString('-1|-1')).toEqual(Long.fromInt(-1))
|
||||
expect(longFromFastString('-123|-456')).toEqual(Long.fromBits(-123, -456))
|
||||
})
|
||||
|
||||
it('should throw on invalid strings', () => {
|
||||
expect(() => longFromFastString('0')).toThrow()
|
||||
expect(() => longFromFastString('0|0|0')).toThrow()
|
||||
expect(() => longFromFastString('abc|def')).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('LongMap', () => {
|
||||
it('should set and get values', () => {
|
||||
const map = new LongMap<string>()
|
||||
|
||||
map.set(Long.fromInt(123), 'test')
|
||||
|
||||
expect(map.get(Long.fromInt(123))).toEqual('test')
|
||||
})
|
||||
|
||||
it('should return undefined for non-existing keys', () => {
|
||||
const map = new LongMap<string>()
|
||||
|
||||
expect(map.get(Long.fromInt(123))).toEqual(undefined)
|
||||
})
|
||||
|
||||
it('should check for existing keys', () => {
|
||||
const map = new LongMap<string>()
|
||||
|
||||
map.set(Long.fromInt(123), 'test')
|
||||
|
||||
expect(map.has(Long.fromInt(123))).toBeTruthy()
|
||||
expect(map.has(Long.fromInt(456))).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should delete keys', () => {
|
||||
const map = new LongMap<string>()
|
||||
|
||||
map.set(Long.fromInt(123), 'test')
|
||||
map.delete(Long.fromInt(123))
|
||||
|
||||
expect(map.has(Long.fromInt(123))).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should iterate over keys', () => {
|
||||
const map = new LongMap<string>()
|
||||
|
||||
map.set(Long.fromInt(123), 'test')
|
||||
map.set(Long.fromInt(456), 'test2')
|
||||
|
||||
expect([...map.keys()]).toEqual([Long.fromInt(123), Long.fromInt(456)])
|
||||
})
|
||||
|
||||
it('should iterate over values', () => {
|
||||
const map = new LongMap<string>()
|
||||
|
||||
map.set(Long.fromInt(123), 'test')
|
||||
map.set(Long.fromInt(456), 'test2')
|
||||
|
||||
expect([...map.values()]).toEqual(['test', 'test2'])
|
||||
})
|
||||
|
||||
it('should clear', () => {
|
||||
const map = new LongMap<string>()
|
||||
|
||||
map.set(Long.fromInt(123), 'test')
|
||||
map.set(Long.fromInt(456), 'test2')
|
||||
|
||||
map.clear()
|
||||
|
||||
expect(map.has(Long.fromInt(123))).toBeFalsy()
|
||||
expect(map.has(Long.fromInt(456))).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should return the size', () => {
|
||||
const map = new LongMap<string>()
|
||||
|
||||
map.set(Long.fromInt(123), 'test')
|
||||
map.set(Long.fromInt(456), 'test2')
|
||||
|
||||
expect(map.size()).toEqual(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LongSet', () => {
|
||||
it('should add and check for values', () => {
|
||||
const set = new LongSet()
|
||||
|
||||
set.add(Long.fromInt(123))
|
||||
|
||||
expect(set.has(Long.fromInt(123))).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should remove values', () => {
|
||||
const set = new LongSet()
|
||||
|
||||
set.add(Long.fromInt(123))
|
||||
set.add(Long.fromInt(456))
|
||||
set.delete(Long.fromInt(123))
|
||||
|
||||
expect(set.has(Long.fromInt(123))).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should return the size', () => {
|
||||
const set = new LongSet()
|
||||
|
||||
set.add(Long.fromInt(123))
|
||||
set.add(Long.fromInt(456))
|
||||
|
||||
expect(set.size).toEqual(2)
|
||||
})
|
||||
|
||||
it('should clear', () => {
|
||||
const set = new LongSet()
|
||||
|
||||
set.add(Long.fromInt(123))
|
||||
set.add(Long.fromInt(456))
|
||||
|
||||
set.clear()
|
||||
|
||||
expect(set.has(Long.fromInt(123))).toBeFalsy()
|
||||
expect(set.has(Long.fromInt(456))).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should return the size', () => {
|
||||
const set = new LongSet()
|
||||
|
||||
set.add(Long.fromInt(123))
|
||||
set.add(Long.fromInt(456))
|
||||
|
||||
expect(set.size).toEqual(2)
|
||||
})
|
||||
|
||||
it('should convert to array', () => {
|
||||
const set = new LongSet()
|
||||
|
||||
set.add(Long.fromInt(123))
|
||||
set.add(Long.fromInt(456))
|
||||
|
||||
expect(set.toArray()).toEqual([Long.fromInt(123), Long.fromInt(456)])
|
||||
})
|
||||
})
|
|
@ -3,42 +3,111 @@ import { describe, expect, it } from 'vitest'
|
|||
import { LruMap } from './lru-map.js'
|
||||
|
||||
describe('LruMap', () => {
|
||||
it('Map backend', () => {
|
||||
it('should maintain maximum size by removing oldest added', () => {
|
||||
const lru = new LruMap<string, number>(2)
|
||||
|
||||
lru.set('first', 1)
|
||||
expect(lru.has('first')).true
|
||||
expect(lru.has('second')).false
|
||||
expect(lru.get('first')).eq(1)
|
||||
|
||||
lru.set('first', 42)
|
||||
expect(lru.has('first')).true
|
||||
expect(lru.has('second')).false
|
||||
expect(lru.get('first')).eq(42)
|
||||
|
||||
lru.set('second', 2)
|
||||
expect(lru.has('first')).true
|
||||
expect(lru.has('second')).true
|
||||
expect(lru.get('first')).eq(42)
|
||||
expect(lru.get('second')).eq(2)
|
||||
|
||||
lru.set('third', 3)
|
||||
expect(lru.has('first')).false
|
||||
expect(lru.has('second')).true
|
||||
expect(lru.has('third')).true
|
||||
expect(lru.get('first')).eq(undefined)
|
||||
expect(lru.get('second')).eq(2)
|
||||
expect(lru.get('third')).eq(3)
|
||||
|
||||
lru.get('second') // update lru so that last = third
|
||||
lru.set('fourth', 4)
|
||||
expect(lru.has('first')).false
|
||||
expect(lru.has('second')).true
|
||||
expect(lru.has('third')).false
|
||||
expect(lru.has('fourth')).true
|
||||
expect(lru.get('first')).eq(undefined)
|
||||
expect(lru.get('second')).eq(2)
|
||||
expect(lru.get('third')).eq(undefined)
|
||||
expect(lru.get('fourth')).eq(4)
|
||||
expect(lru.has('first')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should update the last added item', () => {
|
||||
const lru = new LruMap<string, number>(2)
|
||||
|
||||
lru.set('first', 1)
|
||||
lru.set('second', 2)
|
||||
lru.set('first', 42)
|
||||
lru.set('third', 3)
|
||||
|
||||
expect(lru.get('first')).toEqual(42)
|
||||
expect(lru.has('second')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should update the last used item', () => {
|
||||
const lru = new LruMap<string, number>(2)
|
||||
|
||||
lru.set('first', 1)
|
||||
lru.set('second', 2)
|
||||
lru.get('first')
|
||||
lru.set('third', 3)
|
||||
lru.get('third')
|
||||
|
||||
expect(lru.get('first')).toEqual(1)
|
||||
expect(lru.has('second')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should allow deleting items', () => {
|
||||
const lru = new LruMap<string, number>(2)
|
||||
|
||||
lru.set('first', 1)
|
||||
lru.set('second', 2)
|
||||
lru.set('third', 3) // first is now deleted
|
||||
lru.delete('second')
|
||||
|
||||
expect(lru.has('first')).toBeFalsy()
|
||||
expect(lru.has('second')).toBeFalsy()
|
||||
expect(lru.has('third')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should handle deleting all items', () => {
|
||||
const lru = new LruMap<string, number>(2)
|
||||
|
||||
lru.set('first', 1)
|
||||
lru.set('second', 2)
|
||||
lru.delete('second')
|
||||
lru.delete('first')
|
||||
|
||||
expect(lru.has('first')).toBeFalsy()
|
||||
expect(lru.has('second')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should return undefined for non-existing items', () => {
|
||||
const lru = new LruMap<string, number>(2)
|
||||
|
||||
lru.set('first', 1)
|
||||
lru.set('second', 2)
|
||||
|
||||
expect(lru.get('third')).toEqual(undefined)
|
||||
})
|
||||
|
||||
// it('Map backend', () => {
|
||||
// const lru = new LruMap<string, number>(2)
|
||||
//
|
||||
// lru.set('first', 1)
|
||||
// expect(lru.has('first')).true
|
||||
// expect(lru.has('second')).false
|
||||
// expect(lru.get('first')).eq(1)
|
||||
//
|
||||
// lru.set('first', 42)
|
||||
// expect(lru.has('first')).true
|
||||
// expect(lru.has('second')).false
|
||||
// expect(lru.get('first')).eq(42)
|
||||
//
|
||||
// lru.set('second', 2)
|
||||
// expect(lru.has('first')).true
|
||||
// expect(lru.has('second')).true
|
||||
// expect(lru.get('first')).eq(42)
|
||||
// expect(lru.get('second')).eq(2)
|
||||
//
|
||||
// lru.set('third', 3)
|
||||
// expect(lru.has('first')).false
|
||||
// expect(lru.has('second')).true
|
||||
// expect(lru.has('third')).true
|
||||
// expect(lru.get('first')).eq(undefined)
|
||||
// expect(lru.get('second')).eq(2)
|
||||
// expect(lru.get('third')).eq(3)
|
||||
//
|
||||
// lru.get('second') // update lru so that last = third
|
||||
// lru.set('fourth', 4)
|
||||
// expect(lru.has('first')).false
|
||||
// expect(lru.has('second')).true
|
||||
// expect(lru.has('third')).false
|
||||
// expect(lru.has('fourth')).true
|
||||
// expect(lru.get('first')).eq(undefined)
|
||||
// expect(lru.get('second')).eq(2)
|
||||
// expect(lru.get('third')).eq(undefined)
|
||||
// expect(lru.get('fourth')).eq(4)
|
||||
// })
|
||||
})
|
||||
|
|
153
packages/core/src/utils/peer-utils.test.ts
Normal file
153
packages/core/src/utils/peer-utils.test.ts
Normal file
|
@ -0,0 +1,153 @@
|
|||
import Long from 'long'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { createStub } from '@mtcute/test'
|
||||
|
||||
import {
|
||||
getAllPeersFrom,
|
||||
getBarePeerId,
|
||||
getBasicPeerType,
|
||||
getMarkedPeerId,
|
||||
markedPeerIdToBare,
|
||||
toggleChannelIdMark,
|
||||
} from './peer-utils.js'
|
||||
|
||||
const SOME_CHANNEL_ID = 1183945448
|
||||
const SOME_CHANNEL_ID_MARKED = -1001183945448
|
||||
|
||||
describe('toggleChannelIdMark', () => {
|
||||
it('should turn marked channel id into bare', () => {
|
||||
expect(toggleChannelIdMark(SOME_CHANNEL_ID_MARKED)).toEqual(SOME_CHANNEL_ID)
|
||||
})
|
||||
|
||||
it('should turn bare channel id into marked', () => {
|
||||
expect(toggleChannelIdMark(SOME_CHANNEL_ID)).toEqual(SOME_CHANNEL_ID_MARKED)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getBarePeerId', () => {
|
||||
it('should return bare peer id from Peer', () => {
|
||||
expect(getBarePeerId({ _: 'peerUser', userId: 123 })).toEqual(123)
|
||||
expect(getBarePeerId({ _: 'peerChat', chatId: 456 })).toEqual(456)
|
||||
expect(getBarePeerId({ _: 'peerChannel', channelId: 789 })).toEqual(789)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMarkedPeerId', () => {
|
||||
it('should return marked peer id from bare and type', () => {
|
||||
expect(getMarkedPeerId(123, 'user')).toEqual(123)
|
||||
expect(getMarkedPeerId(456, 'chat')).toEqual(-456)
|
||||
expect(getMarkedPeerId(SOME_CHANNEL_ID, 'channel')).toEqual(SOME_CHANNEL_ID_MARKED)
|
||||
})
|
||||
|
||||
it('should throw on invalid type', () => {
|
||||
// eslint-disable-next-line
|
||||
expect(() => getMarkedPeerId(123, 'invalid' as any)).toThrow()
|
||||
})
|
||||
|
||||
it('should return marked peer id from Peer', () => {
|
||||
expect(getMarkedPeerId({ _: 'peerUser', userId: 123 })).toEqual(123)
|
||||
expect(getMarkedPeerId({ _: 'peerChat', chatId: 456 })).toEqual(-456)
|
||||
expect(getMarkedPeerId({ _: 'peerChannel', channelId: SOME_CHANNEL_ID })).toEqual(SOME_CHANNEL_ID_MARKED)
|
||||
})
|
||||
|
||||
it('should return marked peer id from Input*', () => {
|
||||
expect(getMarkedPeerId({ _: 'inputPeerUser', userId: 123, accessHash: Long.ZERO })).toEqual(123)
|
||||
expect(getMarkedPeerId({ _: 'inputUser', userId: 123, accessHash: Long.ZERO })).toEqual(123)
|
||||
expect(getMarkedPeerId({ _: 'inputPeerChat', chatId: 456 })).toEqual(-456)
|
||||
expect(getMarkedPeerId({ _: 'inputPeerChannel', channelId: SOME_CHANNEL_ID, accessHash: Long.ZERO })).toEqual(
|
||||
SOME_CHANNEL_ID_MARKED,
|
||||
)
|
||||
expect(getMarkedPeerId({ _: 'inputChannel', channelId: SOME_CHANNEL_ID, accessHash: Long.ZERO })).toEqual(
|
||||
SOME_CHANNEL_ID_MARKED,
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw if peer does not have id', () => {
|
||||
expect(() => getMarkedPeerId({ _: 'inputPeerSelf' })).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getBasicPeerType', () => {
|
||||
it('should return basic peer type from Peer', () => {
|
||||
expect(getBasicPeerType({ _: 'peerUser', userId: 123 })).toEqual('user')
|
||||
expect(getBasicPeerType({ _: 'peerChat', chatId: 456 })).toEqual('chat')
|
||||
expect(getBasicPeerType({ _: 'peerChannel', channelId: SOME_CHANNEL_ID })).toEqual('channel')
|
||||
})
|
||||
|
||||
it('should return basic peer type from marked id', () => {
|
||||
expect(getBasicPeerType(123)).toEqual('user')
|
||||
expect(getBasicPeerType(-456)).toEqual('chat')
|
||||
expect(getBasicPeerType(SOME_CHANNEL_ID_MARKED)).toEqual('channel')
|
||||
})
|
||||
|
||||
it('should throw for invalid marked ids', () => {
|
||||
expect(() => getBasicPeerType(0)).toThrow('Invalid marked peer id')
|
||||
|
||||
// secret chats are not supported yet
|
||||
expect(() => getBasicPeerType(-1997852516400)).toThrow('Secret chats are not supported')
|
||||
})
|
||||
})
|
||||
|
||||
describe('markedPeerIdToBare', () => {
|
||||
it('should return bare peer id from marked id', () => {
|
||||
expect(markedPeerIdToBare(123)).toEqual(123)
|
||||
expect(markedPeerIdToBare(-456)).toEqual(456)
|
||||
expect(markedPeerIdToBare(SOME_CHANNEL_ID_MARKED)).toEqual(SOME_CHANNEL_ID)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAllPeersFrom', () => {
|
||||
const stubUser1 = createStub('user', { id: 123 })
|
||||
const stubUser2 = createStub('user', { id: 456 })
|
||||
const stubChat = createStub('chat', { id: 789 })
|
||||
const stubChannel = createStub('channel', { id: 101112 })
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */
|
||||
it('should find all peers from objects containing users/chats fields', () => {
|
||||
expect([...getAllPeersFrom({ users: [], chats: [] } as any)]).toEqual([])
|
||||
expect([
|
||||
...getAllPeersFrom({
|
||||
users: [stubUser1, stubUser2],
|
||||
chats: [stubChat, stubChannel],
|
||||
} as any),
|
||||
]).toEqual([stubUser1, stubUser2, stubChat, stubChannel])
|
||||
})
|
||||
|
||||
it('should extract peers from objects containing user/chat fields', () => {
|
||||
expect([
|
||||
...getAllPeersFrom({
|
||||
user: stubUser2,
|
||||
chat: stubChat,
|
||||
channel: stubChannel,
|
||||
} as any),
|
||||
]).toEqual([stubUser2, stubChat, stubChannel])
|
||||
})
|
||||
|
||||
it('should work for arrays', () => {
|
||||
expect([...getAllPeersFrom([{ user: stubUser1 }, { user: stubUser2 }] as any)]).toEqual([stubUser1, stubUser2])
|
||||
})
|
||||
|
||||
it('should work for peer objects directly', () => {
|
||||
expect([...getAllPeersFrom(stubUser1)]).toEqual([stubUser1])
|
||||
})
|
||||
|
||||
it('should ignore *Empty', () => {
|
||||
expect([
|
||||
...getAllPeersFrom({
|
||||
users: [createStub('userEmpty')],
|
||||
chats: [createStub('chatEmpty')],
|
||||
} as any),
|
||||
]).toEqual([])
|
||||
expect([...getAllPeersFrom({ user: createStub('userEmpty'), chat: createStub('chatEmpty') } as any)]).toEqual(
|
||||
[],
|
||||
)
|
||||
expect([...getAllPeersFrom([createStub('userEmpty'), createStub('chatEmpty')])]).toEqual([])
|
||||
})
|
||||
|
||||
it('should correctly handle users/chats fields of type number[]', () => {
|
||||
expect([...getAllPeersFrom({ users: [123, 456], chats: [123, 456] } as any)]).toEqual([])
|
||||
})
|
||||
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument */
|
||||
})
|
|
@ -72,14 +72,18 @@ export function getMarkedPeerId(
|
|||
switch (peer._) {
|
||||
case 'peerUser':
|
||||
case 'inputPeerUser':
|
||||
case 'inputPeerUserFromMessage':
|
||||
case 'inputUser':
|
||||
case 'inputUserFromMessage':
|
||||
return peer.userId
|
||||
case 'peerChat':
|
||||
case 'inputPeerChat':
|
||||
return -peer.chatId
|
||||
case 'peerChannel':
|
||||
case 'inputPeerChannel':
|
||||
case 'inputPeerChannelFromMessage':
|
||||
case 'inputChannel':
|
||||
case 'inputChannelFromMessage':
|
||||
return ZERO_CHANNEL_ID - peer.channelId
|
||||
}
|
||||
|
||||
|
@ -110,7 +114,7 @@ export function getBasicPeerType(peer: tl.TypePeer | number): BasicPeerType {
|
|||
return 'channel'
|
||||
}
|
||||
|
||||
if (MAX_SECRET_CHAT_ID <= peer && peer !== ZERO_SECRET_CHAT_ID) {
|
||||
if (MAX_SECRET_CHAT_ID >= peer && peer !== ZERO_SECRET_CHAT_ID) {
|
||||
// return 'secret'
|
||||
throw new MtUnsupportedError('Secret chats are not supported')
|
||||
}
|
||||
|
|
86
packages/core/src/utils/sorted-array.test.ts
Normal file
86
packages/core/src/utils/sorted-array.test.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { SortedArray } from './sorted-array.js'
|
||||
|
||||
describe('SortedArray', () => {
|
||||
const ascendingComparator = (a: number, b: number) => a - b
|
||||
|
||||
it('should insert items in accordance with comparator', () => {
|
||||
const arr = new SortedArray<number>([], ascendingComparator)
|
||||
|
||||
arr.insert(1)
|
||||
arr.insert(2)
|
||||
arr.insert(5)
|
||||
arr.insert(3)
|
||||
arr.insert(4)
|
||||
|
||||
expect(arr.raw).toEqual([1, 2, 3, 4, 5])
|
||||
})
|
||||
|
||||
it('should insert arrays', () => {
|
||||
const arr = new SortedArray<number>([], ascendingComparator)
|
||||
|
||||
arr.insert([1, 2, 5, 3, 4])
|
||||
|
||||
expect(arr.raw).toEqual([1, 2, 3, 4, 5])
|
||||
})
|
||||
|
||||
it('should return length', () => {
|
||||
const arr = new SortedArray<number>([], ascendingComparator)
|
||||
|
||||
arr.insert(1)
|
||||
arr.insert(2)
|
||||
arr.insert(5)
|
||||
|
||||
expect(arr.length).toEqual(3)
|
||||
})
|
||||
|
||||
it('should return index of the item', () => {
|
||||
const arr = new SortedArray<number>([], ascendingComparator)
|
||||
|
||||
arr.insert(1)
|
||||
arr.insert(2)
|
||||
arr.insert(5)
|
||||
|
||||
expect(arr.index(1)).toEqual(0)
|
||||
expect(arr.index(2)).toEqual(1)
|
||||
expect(arr.index(5)).toEqual(2)
|
||||
expect(arr.index(10)).toEqual(-1)
|
||||
})
|
||||
|
||||
it('should return closest index for an item not in the array', () => {
|
||||
const arr = new SortedArray<number>([], ascendingComparator)
|
||||
|
||||
arr.insert(1)
|
||||
arr.insert(2)
|
||||
arr.insert(5)
|
||||
|
||||
expect(arr.index(3, true)).toEqual(2)
|
||||
expect(arr.index(4, true)).toEqual(2) // still 2, becuse left-hand side
|
||||
expect(arr.index(6, true)).toEqual(3)
|
||||
})
|
||||
|
||||
it('should remove items by value', () => {
|
||||
const arr = new SortedArray<number>([], ascendingComparator)
|
||||
|
||||
arr.insert(1)
|
||||
arr.insert(2)
|
||||
arr.insert(5)
|
||||
arr.remove(2)
|
||||
|
||||
expect(arr.raw).toEqual([1, 5])
|
||||
})
|
||||
|
||||
it('should check if item is in array', () => {
|
||||
const arr = new SortedArray<number>([], ascendingComparator)
|
||||
|
||||
arr.insert(1)
|
||||
arr.insert(2)
|
||||
arr.insert(5)
|
||||
|
||||
expect(arr.includes(1)).toBeTruthy()
|
||||
expect(arr.includes(2)).toBeTruthy()
|
||||
expect(arr.includes(5)).toBeTruthy()
|
||||
expect(arr.includes(10)).toBeFalsy()
|
||||
})
|
||||
})
|
155
packages/core/src/utils/string-session.test.ts
Normal file
155
packages/core/src/utils/string-session.test.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { createStub } from '@mtcute/test'
|
||||
import { __tlReaderMap } from '@mtcute/tl/binary/reader.js'
|
||||
import { __tlWriterMap } from '@mtcute/tl/binary/writer.js'
|
||||
|
||||
import { defaultProductionDc } from './default-dcs.js'
|
||||
import { readStringSession, writeStringSession } from './string-session.js'
|
||||
|
||||
const stubAuthKey = new Uint8Array(32)
|
||||
const stubDcs = {
|
||||
main: createStub('dcOption', defaultProductionDc.main),
|
||||
media: createStub('dcOption', defaultProductionDc.media),
|
||||
}
|
||||
const stubDcsSameMedia = {
|
||||
main: stubDcs.main,
|
||||
media: stubDcs.main,
|
||||
}
|
||||
|
||||
describe('writeStringSession', () => {
|
||||
it('should write production string session without user', () => {
|
||||
expect(
|
||||
writeStringSession(__tlWriterMap, {
|
||||
version: 2,
|
||||
testMode: false,
|
||||
primaryDcs: stubDcs,
|
||||
authKey: stubAuthKey,
|
||||
}),
|
||||
).toMatchInlineSnapshot(
|
||||
'"AgQAAAANobcYAAAAAAIAAAAOMTQ5LjE1NC4xNjcuNTAAuwEAAA2htxgCAAAAAgAAAA8xNDkuMTU0LjE2Ny4yMjK7AQAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"',
|
||||
)
|
||||
})
|
||||
it('should write production string session without user with same dc for media', () => {
|
||||
expect(
|
||||
writeStringSession(__tlWriterMap, {
|
||||
version: 2,
|
||||
testMode: false,
|
||||
primaryDcs: stubDcsSameMedia,
|
||||
authKey: stubAuthKey,
|
||||
}),
|
||||
).toMatchInlineSnapshot(
|
||||
'"AgAAAAANobcYAAAAAAIAAAAOMTQ5LjE1NC4xNjcuNTAAuwEAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"',
|
||||
)
|
||||
})
|
||||
|
||||
it('should write production string session with user', () => {
|
||||
expect(
|
||||
writeStringSession(__tlWriterMap, {
|
||||
version: 2,
|
||||
testMode: false,
|
||||
primaryDcs: stubDcs,
|
||||
authKey: stubAuthKey,
|
||||
self: { userId: 12345, isBot: false },
|
||||
}),
|
||||
).toMatchInlineSnapshot(
|
||||
'"AgUAAAANobcYAAAAAAIAAAAOMTQ5LjE1NC4xNjcuNTAAuwEAAA2htxgCAAAAAgAAAA8xNDkuMTU0LjE2Ny4yMjK7AQAAOTAAAAAAAAA3l3m8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"',
|
||||
)
|
||||
})
|
||||
|
||||
it('should write test dc string session with user', () => {
|
||||
expect(
|
||||
writeStringSession(__tlWriterMap, {
|
||||
version: 2,
|
||||
testMode: true,
|
||||
primaryDcs: stubDcs,
|
||||
authKey: stubAuthKey,
|
||||
self: { userId: 12345, isBot: false },
|
||||
}),
|
||||
).toMatchInlineSnapshot(
|
||||
'"AgcAAAANobcYAAAAAAIAAAAOMTQ5LjE1NC4xNjcuNTAAuwEAAA2htxgCAAAAAgAAAA8xNDkuMTU0LjE2Ny4yMjK7AQAAOTAAAAAAAAA3l3m8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('readStringSession', () => {
|
||||
describe('v2', () => {
|
||||
it('should read production string session without user', () => {
|
||||
expect(
|
||||
readStringSession(
|
||||
__tlReaderMap,
|
||||
'AgQAAAANobcYAAAAAAIAAAAOMTQ5LjE1NC4xNjcuNTAAuwEAAA2htxgCAAAAAgAAAA8xNDkuMTU0LjE2Ny4yMjK7AQAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
),
|
||||
).toEqual({
|
||||
version: 2,
|
||||
testMode: false,
|
||||
primaryDcs: stubDcs,
|
||||
authKey: stubAuthKey,
|
||||
self: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('should read production string session without user with same dc for media', () => {
|
||||
expect(
|
||||
readStringSession(
|
||||
__tlReaderMap,
|
||||
'AgAAAAANobcYAAAAAAIAAAAOMTQ5LjE1NC4xNjcuNTAAuwEAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
),
|
||||
).toEqual({
|
||||
version: 2,
|
||||
testMode: false,
|
||||
primaryDcs: stubDcsSameMedia,
|
||||
authKey: stubAuthKey,
|
||||
self: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('should read production string session with user', () => {
|
||||
expect(
|
||||
readStringSession(
|
||||
__tlReaderMap,
|
||||
'AgUAAAANobcYAAAAAAIAAAAOMTQ5LjE1NC4xNjcuNTAAuwEAAA2htxgCAAAAAgAAAA8xNDkuMTU0LjE2Ny4yMjK7AQAAOTAAAAAAAAA3l3m8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
),
|
||||
).toEqual({
|
||||
version: 2,
|
||||
testMode: false,
|
||||
primaryDcs: stubDcs,
|
||||
authKey: stubAuthKey,
|
||||
self: { userId: 12345, isBot: false },
|
||||
})
|
||||
})
|
||||
|
||||
it('should read test dc string session with user', () => {
|
||||
expect(
|
||||
readStringSession(
|
||||
__tlReaderMap,
|
||||
'AgcAAAANobcYAAAAAAIAAAAOMTQ5LjE1NC4xNjcuNTAAuwEAAA2htxgCAAAAAgAAAA8xNDkuMTU0LjE2Ny4yMjK7AQAAOTAAAAAAAAA3l3m8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
),
|
||||
).toEqual({
|
||||
version: 2,
|
||||
testMode: true,
|
||||
primaryDcs: stubDcs,
|
||||
authKey: stubAuthKey,
|
||||
self: { userId: 12345, isBot: false },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('v1', () => {
|
||||
it('should read string session with user', () => {
|
||||
expect(
|
||||
readStringSession(
|
||||
__tlReaderMap,
|
||||
'AQEAAAANobcYAAAAAAIAAAAOMTQ5LjE1NC4xNjcuNTAAuwEAADkwAAAAAAAAN5d5vCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
),
|
||||
).toEqual({
|
||||
version: 1,
|
||||
testMode: false,
|
||||
// v1 didn't have separate media dc
|
||||
primaryDcs: stubDcsSameMedia,
|
||||
authKey: stubAuthKey,
|
||||
self: { userId: 12345, isBot: false },
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -41,11 +41,14 @@ export function writeStringSession(writerMap: TlWriterMap, data: StringSessionDa
|
|||
writer.uint8View[0] = version
|
||||
writer.pos += 1
|
||||
|
||||
if (version >= 2 && data.primaryDcs.media !== data.primaryDcs.main) {
|
||||
flags |= 4
|
||||
}
|
||||
|
||||
writer.int(flags)
|
||||
writer.object(data.primaryDcs.main)
|
||||
|
||||
if (version >= 2 && data.primaryDcs.media !== data.primaryDcs.main) {
|
||||
flags |= 4
|
||||
writer.object(data.primaryDcs.media)
|
||||
}
|
||||
|
||||
|
@ -97,7 +100,7 @@ export function readStringSession(readerMap: TlReaderMap, data: string): StringS
|
|||
const key = reader.bytes()
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
version,
|
||||
testMode,
|
||||
primaryDcs: {
|
||||
main: primaryDc,
|
||||
|
|
98
packages/core/src/utils/tl-json.test.ts
Normal file
98
packages/core/src/utils/tl-json.test.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { jsonToTlJson, tlJsonToJson } from './tl-json.js'
|
||||
|
||||
describe('jsonToTlJson', () => {
|
||||
it('should correctly handle null/undefined', () => {
|
||||
expect(jsonToTlJson(null)).toEqual({ _: 'jsonNull' })
|
||||
expect(jsonToTlJson(undefined)).toEqual({ _: 'jsonNull' })
|
||||
})
|
||||
|
||||
it('should correctly handle booleans', () => {
|
||||
expect(jsonToTlJson(true)).toEqual({ _: 'jsonBool', value: true })
|
||||
expect(jsonToTlJson(false)).toEqual({ _: 'jsonBool', value: false })
|
||||
})
|
||||
|
||||
it('should correctly handle numbers', () => {
|
||||
expect(jsonToTlJson(123)).toEqual({ _: 'jsonNumber', value: 123 })
|
||||
expect(jsonToTlJson(0)).toEqual({ _: 'jsonNumber', value: 0 })
|
||||
expect(jsonToTlJson(-123)).toEqual({ _: 'jsonNumber', value: -123 })
|
||||
})
|
||||
|
||||
it('should correctly handle strings', () => {
|
||||
expect(jsonToTlJson('hello')).toEqual({ _: 'jsonString', value: 'hello' })
|
||||
expect(jsonToTlJson('')).toEqual({ _: 'jsonString', value: '' })
|
||||
})
|
||||
|
||||
it('should correcly handle arrays', () => {
|
||||
expect(jsonToTlJson([1, 2, 3])).toEqual({
|
||||
_: 'jsonArray',
|
||||
value: [
|
||||
{ _: 'jsonNumber', value: 1 },
|
||||
{ _: 'jsonNumber', value: 2 },
|
||||
{ _: 'jsonNumber', value: 3 },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should correctly handle objects', () => {
|
||||
expect(jsonToTlJson({ a: 1, b: 2 })).toEqual({
|
||||
_: 'jsonObject',
|
||||
value: [
|
||||
{ _: 'jsonObjectValue', key: 'a', value: { _: 'jsonNumber', value: 1 } },
|
||||
{ _: 'jsonObjectValue', key: 'b', value: { _: 'jsonNumber', value: 2 } },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should error on unsupported types', () => {
|
||||
expect(() => jsonToTlJson(Symbol('test'))).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('tlJsonToJson', () => {
|
||||
it('should correctly handle null/undefined', () => {
|
||||
expect(tlJsonToJson({ _: 'jsonNull' })).toEqual(null)
|
||||
})
|
||||
|
||||
it('should correctly handle booleans', () => {
|
||||
expect(tlJsonToJson({ _: 'jsonBool', value: true })).toEqual(true)
|
||||
expect(tlJsonToJson({ _: 'jsonBool', value: false })).toEqual(false)
|
||||
})
|
||||
|
||||
it('should correctly handle numbers', () => {
|
||||
expect(tlJsonToJson({ _: 'jsonNumber', value: 123 })).toEqual(123)
|
||||
expect(tlJsonToJson({ _: 'jsonNumber', value: 0 })).toEqual(0)
|
||||
expect(tlJsonToJson({ _: 'jsonNumber', value: -123 })).toEqual(-123)
|
||||
})
|
||||
|
||||
it('should correctly handle strings', () => {
|
||||
expect(tlJsonToJson({ _: 'jsonString', value: 'hello' })).toEqual('hello')
|
||||
expect(tlJsonToJson({ _: 'jsonString', value: '' })).toEqual('')
|
||||
})
|
||||
|
||||
it('should correcly handle arrays', () => {
|
||||
expect(
|
||||
tlJsonToJson({
|
||||
_: 'jsonArray',
|
||||
value: [
|
||||
{ _: 'jsonNumber', value: 1 },
|
||||
{ _: 'jsonNumber', value: 2 },
|
||||
{ _: 'jsonNumber', value: 3 },
|
||||
],
|
||||
}),
|
||||
).toEqual([1, 2, 3])
|
||||
})
|
||||
|
||||
it('should correctly handle objects', () => {
|
||||
expect(
|
||||
tlJsonToJson({
|
||||
_: 'jsonObject',
|
||||
value: [
|
||||
{ _: 'jsonObjectValue', key: 'a', value: { _: 'jsonNumber', value: 1 } },
|
||||
{ _: 'jsonObjectValue', key: 'b', value: { _: 'jsonNumber', value: 2 } },
|
||||
],
|
||||
}),
|
||||
).toEqual({ a: 1, b: 2 })
|
||||
})
|
||||
})
|
84
packages/core/src/utils/type-assertions.test.ts
Normal file
84
packages/core/src/utils/type-assertions.test.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
assertTypeIs,
|
||||
assertTypeIsNot,
|
||||
hasPresentKey,
|
||||
hasValueAtKey,
|
||||
isPresent,
|
||||
mtpAssertTypeIs,
|
||||
} from './type-assertions.js'
|
||||
|
||||
describe('isPresent', () => {
|
||||
it('should return true for non-null values', () => {
|
||||
expect(isPresent(1)).toBe(true)
|
||||
expect(isPresent('')).toBe(true)
|
||||
expect(isPresent({})).toBe(true)
|
||||
expect(isPresent([])).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for null/undefined', () => {
|
||||
expect(isPresent(null)).toBe(false)
|
||||
expect(isPresent(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasPresentKey', () => {
|
||||
it('should return true for objects with present keys', () => {
|
||||
expect(hasPresentKey('a')({ a: 1 })).toBe(true)
|
||||
expect(hasPresentKey('a')({ a: 1, b: 2 })).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for objects with undefined/null keys', () => {
|
||||
expect(hasPresentKey('a')({ a: undefined })).toBe(false)
|
||||
expect(hasPresentKey('a')({ a: null })).toBe(false)
|
||||
expect(hasPresentKey('a')({ a: undefined, b: 2 })).toBe(false)
|
||||
expect(hasPresentKey('a')({ a: null, b: 2 })).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for objects without the key', () => {
|
||||
expect(hasPresentKey('a')({ b: 2 })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasValueAtKey', () => {
|
||||
it('should return true for objects with the correct value', () => {
|
||||
expect(hasValueAtKey('a', 1)({ a: 1 })).toBe(true)
|
||||
expect(hasValueAtKey('a', 1)({ a: 1, b: 2 })).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for objects with the wrong value', () => {
|
||||
expect(hasValueAtKey('a', 1)({ a: 2 })).toBe(false)
|
||||
expect(hasValueAtKey('a', 1)({ a: 2, b: 2 })).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for objects without the key', () => {
|
||||
// @ts-expect-error - we want to make sure the key is not present at runtime
|
||||
expect(hasValueAtKey('a', 1)({ b: 2 })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('assertTypeIs', () => {
|
||||
it('should not throw for correct types', () => {
|
||||
assertTypeIs('peerUser', { _: 'peerUser', userId: 1 }, 'peerUser')
|
||||
mtpAssertTypeIs('peerUser', { _: 'mt_rpc_answer_unknown' }, 'mt_rpc_answer_unknown')
|
||||
})
|
||||
|
||||
it('should throw for incorrect types', () => {
|
||||
// eslint-disable-next-line
|
||||
expect(() => assertTypeIs('peerUser', { _: 'peerChannel', channelId: 1 } as any, 'peerUser')).toThrow()
|
||||
// eslint-disable-next-line
|
||||
expect(() => mtpAssertTypeIs('peerUser', { _: 'mt_rpc_answer_unknown' } as any, 'peerUser')).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('assertTypeIsNot', () => {
|
||||
it('should not throw for correct types', () => {
|
||||
// eslint-disable-next-line
|
||||
assertTypeIsNot('peerUser', { _: 'peerChannel', channelId: 1 } as any, 'peerUser')
|
||||
})
|
||||
|
||||
it('should throw for incorrect types', () => {
|
||||
expect(() => assertTypeIsNot('peerUser', { _: 'peerUser', userId: 1 }, 'peerUser')).toThrow()
|
||||
})
|
||||
})
|
|
@ -3,9 +3,6 @@
|
|||
"compilerOptions": {
|
||||
"outDir": "./dist/esm",
|
||||
"rootDir": "./src",
|
||||
"paths": {
|
||||
"@mtcute/dispatcher": ["../dispatcher/src/state/storage.ts"],
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./src",
|
||||
|
|
|
@ -34,6 +34,14 @@ describe('stub', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('should correctly generate stubs for optional vectors', () => {
|
||||
expect(createStub('updateChannelPinnedTopics')).toEqual({
|
||||
_: 'updateChannelPinnedTopics',
|
||||
channelId: 0,
|
||||
order: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('should correctly generate stubs for nested types', () => {
|
||||
expect(createStub('messageActionGroupCallScheduled', { scheduleDate: 123 })).toEqual({
|
||||
_: 'messageActionGroupCallScheduled',
|
||||
|
|
17
packages/test/src/utils.test.ts
Normal file
17
packages/test/src/utils.test.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { markedIdToPeer } from './utils.js'
|
||||
|
||||
describe('markedIdToPeer', () => {
|
||||
it('should correctly convert user ids', () => {
|
||||
expect(markedIdToPeer(12345)).toEqual({ _: 'peerUser', userId: 12345 })
|
||||
})
|
||||
|
||||
it('should correctly convert chat ids', () => {
|
||||
expect(markedIdToPeer(-12345)).toEqual({ _: 'peerChat', chatId: 12345 })
|
||||
})
|
||||
|
||||
it('should correctly convert channel ids', () => {
|
||||
expect(markedIdToPeer(-1000000012345)).toEqual({ _: 'peerChannel', channelId: 12345 })
|
||||
})
|
||||
})
|
|
@ -127,6 +127,12 @@ importers:
|
|||
specifier: 5.2.3
|
||||
version: 5.2.3
|
||||
devDependencies:
|
||||
'@mtcute/dispatcher':
|
||||
specifier: workspace:^
|
||||
version: link:../dispatcher
|
||||
'@mtcute/test':
|
||||
specifier: workspace:^
|
||||
version: link:../test
|
||||
'@types/ws':
|
||||
specifier: 8.5.4
|
||||
version: 8.5.4
|
||||
|
|
Loading…
Reference in a new issue