From 484149eae910f6bb3049956ffc188288d48a765c Mon Sep 17 00:00:00 2001 From: Alina Sireneva Date: Tue, 14 Nov 2023 04:37:00 +0300 Subject: [PATCH] test(core): more tests! --- .../core/src/network/config-manager.test.ts | 218 +++++++++++++++++ packages/core/src/network/config-manager.ts | 3 +- .../src/network/persistent-connection.test.ts | 221 ++++++++++++++++++ 3 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/network/config-manager.test.ts create mode 100644 packages/core/src/network/persistent-connection.test.ts diff --git a/packages/core/src/network/config-manager.test.ts b/packages/core/src/network/config-manager.test.ts new file mode 100644 index 00000000..43e0442f --- /dev/null +++ b/packages/core/src/network/config-manager.test.ts @@ -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[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', + }) + }) + }) +}) diff --git a/packages/core/src/network/config-manager.ts b/packages/core/src/network/config-manager.ts index ce182bdc..8123258a 100644 --- a/packages/core/src/network/config-manager.ts +++ b/packages/core/src/network/config-manager.ts @@ -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 { @@ -28,6 +28,7 @@ export class ConfigManager { return (this._updatingPromise = this._update().then((config) => { if (this._destroyed) return + this._updatingPromise = undefined this.setConfig(config) })) diff --git a/packages/core/src/network/persistent-connection.test.ts b/packages/core/src/network/persistent-connection.test.ts new file mode 100644 index 00000000..1a9680eb --- /dev/null +++ b/packages/core/src/network/persistent-connection.test.ts @@ -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) => { + 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() + }) + }) +})