test(core): more tests!
This commit is contained in:
parent
13be8482e0
commit
484149eae9
3 changed files with 441 additions and 1 deletions
218
packages/core/src/network/config-manager.test.ts
Normal file
218
packages/core/src/network/config-manager.test.ts
Normal file
|
@ -0,0 +1,218 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createStub } from '@mtcute/test'
|
||||
import { tl } from '@mtcute/tl'
|
||||
|
||||
import { ConfigManager } from './config-manager.js'
|
||||
|
||||
describe('ConfigManager', () => {
|
||||
const config = createStub('config', {
|
||||
expires: 300,
|
||||
})
|
||||
const getConfig = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(0)
|
||||
getConfig.mockClear().mockImplementation(() => Promise.resolve(config))
|
||||
})
|
||||
afterEach(() => void vi.useRealTimers())
|
||||
|
||||
it('should fetch initial config', async () => {
|
||||
const cm = new ConfigManager(getConfig)
|
||||
|
||||
const fetchedConfig = await cm.get()
|
||||
|
||||
expect(getConfig).toHaveBeenCalledTimes(1)
|
||||
expect(fetchedConfig).toEqual(config)
|
||||
expect(cm.getNow()).toEqual(config)
|
||||
})
|
||||
|
||||
it('should automatically update config', async () => {
|
||||
const cm = new ConfigManager(getConfig)
|
||||
await cm.update()
|
||||
|
||||
getConfig.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
...config,
|
||||
expires: 600,
|
||||
}),
|
||||
)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(301_000)
|
||||
|
||||
expect(getConfig).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should correctly determine stale config', () => {
|
||||
const cm = new ConfigManager(getConfig)
|
||||
expect(cm.isStale).toBe(true)
|
||||
|
||||
cm.setConfig(config)
|
||||
expect(cm.isStale).toBe(false)
|
||||
|
||||
vi.setSystemTime(300_000)
|
||||
expect(cm.isStale).toBe(true)
|
||||
})
|
||||
|
||||
it('should not update config if not stale', async () => {
|
||||
const cm = new ConfigManager(getConfig)
|
||||
await cm.update()
|
||||
|
||||
getConfig.mockClear()
|
||||
await cm.update()
|
||||
|
||||
expect(getConfig).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not update config twice', async () => {
|
||||
const cm = new ConfigManager(getConfig)
|
||||
await cm.update()
|
||||
|
||||
vi.setSystemTime(300_000)
|
||||
getConfig.mockClear()
|
||||
await Promise.all([cm.update(), cm.update()])
|
||||
|
||||
expect(getConfig).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call listeners on config update', async () => {
|
||||
const cm = new ConfigManager(getConfig)
|
||||
const listener = vi.fn()
|
||||
cm.onConfigUpdate(listener)
|
||||
await cm.update()
|
||||
|
||||
vi.setSystemTime(300_000)
|
||||
cm.offConfigUpdate(listener)
|
||||
await cm.update()
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
expect(listener).toHaveBeenCalledWith(config)
|
||||
})
|
||||
|
||||
it('should correctly destroy', async () => {
|
||||
const cm = new ConfigManager(getConfig)
|
||||
await cm.update()
|
||||
|
||||
cm.destroy()
|
||||
|
||||
getConfig.mockClear()
|
||||
await vi.advanceTimersByTimeAsync(301_000)
|
||||
|
||||
expect(getConfig).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe('findOption', () => {
|
||||
const useDcOptions = (options: tl.RawDcOption[]) => {
|
||||
getConfig.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
...config,
|
||||
dcOptions: options,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const findOption = async (params: Parameters<ConfigManager['findOption']>[0]) => {
|
||||
const cm = new ConfigManager(getConfig)
|
||||
await cm.update()
|
||||
|
||||
return cm.findOption(params)
|
||||
}
|
||||
|
||||
it('should find option by dc id', async () => {
|
||||
useDcOptions([
|
||||
createStub('dcOption', { id: 1, ipAddress: '1.1.1.1' }),
|
||||
createStub('dcOption', { id: 2, ipAddress: '2.2.2.2' }),
|
||||
])
|
||||
|
||||
expect(await findOption({ dcId: 1 })).toMatchObject({
|
||||
id: 1,
|
||||
ipAddress: '1.1.1.1',
|
||||
})
|
||||
expect(await findOption({ dcId: 2 })).toMatchObject({
|
||||
id: 2,
|
||||
ipAddress: '2.2.2.2',
|
||||
})
|
||||
})
|
||||
|
||||
it('should ignore tcpoOnly options', async () => {
|
||||
useDcOptions([
|
||||
createStub('dcOption', { id: 1, ipAddress: '1.1.1.1', tcpoOnly: true }),
|
||||
createStub('dcOption', { id: 1, ipAddress: '1.1.1.2' }),
|
||||
])
|
||||
|
||||
expect(await findOption({ dcId: 1 })).toMatchObject({
|
||||
id: 1,
|
||||
ipAddress: '1.1.1.2',
|
||||
})
|
||||
})
|
||||
|
||||
it('should respect allowMedia flag', async () => {
|
||||
useDcOptions([
|
||||
createStub('dcOption', { id: 2, ipAddress: '2.2.2.2', mediaOnly: true }),
|
||||
createStub('dcOption', { id: 2, ipAddress: '2.2.2.3' }),
|
||||
])
|
||||
|
||||
expect(await findOption({ dcId: 2 })).toMatchObject({
|
||||
id: 2,
|
||||
ipAddress: '2.2.2.3',
|
||||
})
|
||||
|
||||
expect(await findOption({ dcId: 2, allowMedia: true })).toMatchObject({
|
||||
id: 2,
|
||||
ipAddress: '2.2.2.2',
|
||||
})
|
||||
})
|
||||
|
||||
it('should respect preferMedia flag', async () => {
|
||||
useDcOptions([
|
||||
createStub('dcOption', { id: 2, ipAddress: '2.2.2.3' }),
|
||||
createStub('dcOption', { id: 2, ipAddress: '2.2.2.2', mediaOnly: true }),
|
||||
])
|
||||
|
||||
expect(await findOption({ dcId: 2 })).toMatchObject({
|
||||
id: 2,
|
||||
ipAddress: '2.2.2.3',
|
||||
})
|
||||
|
||||
expect(await findOption({ dcId: 2, allowMedia: true, preferMedia: true })).toMatchObject({
|
||||
id: 2,
|
||||
ipAddress: '2.2.2.2',
|
||||
})
|
||||
})
|
||||
|
||||
it('should respect allowIpv6 flag', async () => {
|
||||
useDcOptions([
|
||||
createStub('dcOption', { id: 2, ipAddress: '::1', ipv6: true }),
|
||||
createStub('dcOption', { id: 2, ipAddress: '2.2.2.3' }),
|
||||
])
|
||||
|
||||
expect(await findOption({ dcId: 2 })).toMatchObject({
|
||||
id: 2,
|
||||
ipAddress: '2.2.2.3',
|
||||
})
|
||||
|
||||
expect(await findOption({ dcId: 2, allowIpv6: true })).toMatchObject({
|
||||
id: 2,
|
||||
ipAddress: '::1',
|
||||
})
|
||||
})
|
||||
|
||||
it('should respect preferIpv6 flag', async () => {
|
||||
useDcOptions([
|
||||
createStub('dcOption', { id: 2, ipAddress: '2.2.2.3' }),
|
||||
createStub('dcOption', { id: 2, ipAddress: '::1', ipv6: true }),
|
||||
])
|
||||
|
||||
expect(await findOption({ dcId: 2 })).toMatchObject({
|
||||
id: 2,
|
||||
ipAddress: '2.2.2.3',
|
||||
})
|
||||
|
||||
expect(await findOption({ dcId: 2, allowIpv6: true, preferIpv6: true })).toMatchObject({
|
||||
id: 2,
|
||||
ipAddress: '::1',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -19,7 +19,7 @@ export class ConfigManager {
|
|||
private _listeners: ((config: tl.RawConfig) => void)[] = []
|
||||
|
||||
get isStale(): boolean {
|
||||
return !this._config || this._config.expires < Date.now() / 1000
|
||||
return !this._config || this._config.expires <= Date.now() / 1000
|
||||
}
|
||||
|
||||
update(force = false): Promise<void> {
|
||||
|
@ -28,6 +28,7 @@ export class ConfigManager {
|
|||
|
||||
return (this._updatingPromise = this._update().then((config) => {
|
||||
if (this._destroyed) return
|
||||
this._updatingPromise = undefined
|
||||
|
||||
this.setConfig(config)
|
||||
}))
|
||||
|
|
221
packages/core/src/network/persistent-connection.test.ts
Normal file
221
packages/core/src/network/persistent-connection.test.ts
Normal file
|
@ -0,0 +1,221 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createStub, defaultTestCryptoProvider, StubTelegramTransport } from '@mtcute/test'
|
||||
|
||||
import { LogManager } from '../utils/index.js'
|
||||
import { PersistentConnection, PersistentConnectionParams } from './persistent-connection.js'
|
||||
import { defaultReconnectionStrategy } from './reconnection.js'
|
||||
|
||||
class FakePersistentConnection extends PersistentConnection {
|
||||
constructor(params: PersistentConnectionParams) {
|
||||
const log = new LogManager()
|
||||
log.level = 0
|
||||
super(params, log)
|
||||
}
|
||||
|
||||
onConnected() {
|
||||
this.onConnectionUsable()
|
||||
}
|
||||
onError() {}
|
||||
onMessage() {}
|
||||
}
|
||||
|
||||
describe('PersistentConnection', () => {
|
||||
beforeEach(() => void vi.useFakeTimers())
|
||||
afterEach(() => void vi.useRealTimers())
|
||||
|
||||
const create = async (params?: Partial<PersistentConnectionParams>) => {
|
||||
return new FakePersistentConnection({
|
||||
crypto: await defaultTestCryptoProvider(),
|
||||
transportFactory: () => new StubTelegramTransport({}),
|
||||
dc: createStub('dcOption'),
|
||||
testMode: false,
|
||||
reconnectionStrategy: defaultReconnectionStrategy,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
it('should set up listeners on transport', async () => {
|
||||
const transportFactory = vi.fn().mockImplementation(() => {
|
||||
const transport = new StubTelegramTransport({})
|
||||
|
||||
vi.spyOn(transport, 'on')
|
||||
|
||||
return transport
|
||||
})
|
||||
await create({ transportFactory })
|
||||
|
||||
const transport = transportFactory.mock.results[0].value as StubTelegramTransport
|
||||
|
||||
expect(transport.on).toHaveBeenCalledWith('ready', expect.any(Function))
|
||||
expect(transport.on).toHaveBeenCalledWith('message', expect.any(Function))
|
||||
expect(transport.on).toHaveBeenCalledWith('error', expect.any(Function))
|
||||
expect(transport.on).toHaveBeenCalledWith('close', expect.any(Function))
|
||||
})
|
||||
|
||||
it('should properly reset old transport', async () => {
|
||||
const transportFactory = vi.fn().mockImplementation(() => {
|
||||
const transport = new StubTelegramTransport({})
|
||||
|
||||
vi.spyOn(transport, 'close')
|
||||
|
||||
return transport
|
||||
})
|
||||
const pc = await create({ transportFactory })
|
||||
|
||||
const transport = transportFactory.mock.results[0].value as StubTelegramTransport
|
||||
|
||||
pc.changeTransport(transportFactory)
|
||||
|
||||
expect(transport.close).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should buffer unsent packages', async () => {
|
||||
const transportFactory = vi.fn().mockImplementation(() => {
|
||||
const transport = new StubTelegramTransport({})
|
||||
|
||||
const transportConnect = transport.connect
|
||||
vi.spyOn(transport, 'connect').mockImplementation((dc, test) => {
|
||||
setTimeout(() => {
|
||||
transportConnect.call(transport, dc, test)
|
||||
}, 100)
|
||||
})
|
||||
vi.spyOn(transport, 'send')
|
||||
|
||||
return transport
|
||||
})
|
||||
const pc = await create({ transportFactory })
|
||||
|
||||
const transport = transportFactory.mock.results[0].value as StubTelegramTransport
|
||||
|
||||
const data1 = new Uint8Array([1, 2, 3])
|
||||
const data2 = new Uint8Array([4, 5, 6])
|
||||
|
||||
await pc.send(data1)
|
||||
await pc.send(data2)
|
||||
|
||||
expect(transport.send).toHaveBeenCalledTimes(0)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(150)
|
||||
|
||||
expect(transport.send).toHaveBeenCalledTimes(2)
|
||||
expect(transport.send).toHaveBeenCalledWith(data1)
|
||||
expect(transport.send).toHaveBeenCalledWith(data2)
|
||||
})
|
||||
|
||||
it('should reconnect on close', async () => {
|
||||
const reconnectionStrategy = vi.fn().mockImplementation(() => 1000)
|
||||
const transportFactory = vi.fn().mockImplementation(() => new StubTelegramTransport({}))
|
||||
|
||||
const pc = await create({
|
||||
reconnectionStrategy,
|
||||
transportFactory,
|
||||
})
|
||||
|
||||
const transport = transportFactory.mock.results[0].value as StubTelegramTransport
|
||||
|
||||
pc.connect()
|
||||
|
||||
await vi.waitFor(() => expect(pc.isConnected).toBe(true))
|
||||
|
||||
transport.close()
|
||||
|
||||
expect(reconnectionStrategy).toHaveBeenCalledOnce()
|
||||
expect(pc.isConnected).toBe(false)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
|
||||
expect(pc.isConnected).toBe(true)
|
||||
})
|
||||
|
||||
describe('inactivity timeout', () => {
|
||||
it('should disconnect on inactivity (passed in constructor)', async () => {
|
||||
const pc = await create({
|
||||
inactivityTimeout: 1000,
|
||||
})
|
||||
|
||||
pc.connect()
|
||||
|
||||
await vi.waitFor(() => expect(pc.isConnected).toBe(true))
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
|
||||
await vi.waitFor(() => expect(pc.isConnected).toBe(false))
|
||||
})
|
||||
|
||||
it('should disconnect on inactivity (set up with setInactivityTimeout)', async () => {
|
||||
const pc = await create()
|
||||
|
||||
pc.connect()
|
||||
pc.setInactivityTimeout(1000)
|
||||
|
||||
await vi.waitFor(() => expect(pc.isConnected).toBe(true))
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
|
||||
await vi.waitFor(() => expect(pc.isConnected).toBe(false))
|
||||
})
|
||||
|
||||
it('should not disconnect on inactivity if disabled', async () => {
|
||||
const pc = await create({
|
||||
inactivityTimeout: 1000,
|
||||
})
|
||||
|
||||
pc.connect()
|
||||
pc.setInactivityTimeout(undefined)
|
||||
|
||||
await vi.waitFor(() => expect(pc.isConnected).toBe(true))
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
|
||||
await vi.waitFor(() => expect(pc.isConnected).toBe(true))
|
||||
})
|
||||
|
||||
it('should reconnect after inactivity before sending', async () => {
|
||||
const transportFactory = vi.fn().mockImplementation(() => {
|
||||
const transport = new StubTelegramTransport({})
|
||||
|
||||
vi.spyOn(transport, 'connect')
|
||||
vi.spyOn(transport, 'send')
|
||||
|
||||
return transport
|
||||
})
|
||||
|
||||
const pc = await create({
|
||||
inactivityTimeout: 1000,
|
||||
transportFactory,
|
||||
})
|
||||
const transport = transportFactory.mock.results[0].value as StubTelegramTransport
|
||||
|
||||
pc.connect()
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
|
||||
await vi.waitFor(() => expect(pc.isConnected).toBe(false))
|
||||
|
||||
vi.mocked(transport.connect).mockClear()
|
||||
|
||||
await pc.send(new Uint8Array([1, 2, 3]))
|
||||
|
||||
expect(transport.connect).toHaveBeenCalledOnce()
|
||||
expect(transport.send).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should propagate errors', async () => {
|
||||
const transportFactory = vi.fn().mockImplementation(() => new StubTelegramTransport({}))
|
||||
|
||||
const pc = await create({ transportFactory })
|
||||
const transport = transportFactory.mock.results[0].value as StubTelegramTransport
|
||||
|
||||
pc.connect()
|
||||
|
||||
await vi.waitFor(() => expect(pc.isConnected).toBe(true))
|
||||
|
||||
const onErrorSpy = vi.spyOn(pc, 'onError')
|
||||
|
||||
transport.emit('error', new Error('test error'))
|
||||
|
||||
expect(onErrorSpy).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue