test(core): more tests!

This commit is contained in:
alina 🌸 2023-11-14 04:37:00 +03:00
parent 13be8482e0
commit 484149eae9
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
3 changed files with 441 additions and 1 deletions

View 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',
})
})
})
})

View file

@ -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)
}))

View 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()
})
})
})