From 8533eba0fdf4fa86c7844aee35865f4dc25c1eb1 Mon Sep 17 00:00:00 2001 From: YuShifan <894402575bt@gmail.com> Date: Tue, 3 Sep 2024 18:08:13 +0800 Subject: [PATCH 1/3] test(cli): add more utils test cases --- .../__tests__/utils/mqttErrorReason.test.ts | 79 +++++++++ cli/src/__tests__/utils/parse.test.ts | 150 ++++++++++++++++++ .../__tests__/utils/protobufErrors.test.ts | 32 ++++ 3 files changed, 261 insertions(+) create mode 100644 cli/src/__tests__/utils/mqttErrorReason.test.ts create mode 100644 cli/src/__tests__/utils/parse.test.ts create mode 100644 cli/src/__tests__/utils/protobufErrors.test.ts diff --git a/cli/src/__tests__/utils/mqttErrorReason.test.ts b/cli/src/__tests__/utils/mqttErrorReason.test.ts new file mode 100644 index 000000000..11c5746fd --- /dev/null +++ b/cli/src/__tests__/utils/mqttErrorReason.test.ts @@ -0,0 +1,79 @@ +import getErrorReason from '../../utils/mqttErrorReason' +import { expect, test } from '@jest/globals' + +describe('getErrorReason', () => { + test.each([ + [4, 'Disconnect with Will Message'], + [16, 'No matching subscribers'], + [17, 'No subscription existed'], + [24, 'Continue authentication'], + [25, 'Re-authenticate'], + [128, 'Unspecified error'], + [129, 'Malformed Packet'], + [130, 'Protocol Error'], + [131, 'Implementation specific error'], + [132, 'Unsupported Protocol Version'], + [133, 'Client Identifier not valid'], + [134, 'Bad User Name or Password'], + [135, 'Not authorized'], + [136, 'Server unavailable'], + [137, 'Server busy'], + [138, 'Banned'], + [139, 'Server shutting down'], + [140, 'Bad authentication method'], + [141, 'Keep Alive timeout'], + [142, 'Session taken over'], + [143, 'Topic Filter invalid'], + [144, 'Topic Name invalid'], + [145, 'Packet Identifier in use'], + [146, 'Packet Identifier not found'], + [147, 'Receive Maximum exceeded'], + [148, 'Topic Alias invalid'], + [149, 'Packet too large'], + [150, 'Message rate too high'], + [151, 'Quota exceeded'], + [152, 'Administrative action'], + [153, 'Payload format invalid'], + [154, 'Retain not supported'], + [155, 'QoS not supported'], + [156, 'Use another server'], + [157, 'Server moved'], + [158, 'Shared Subscriptions not supported'], + [159, 'Connection rate exceeded'], + [160, 'Maximum connect time'], + [161, 'Subscription Identifiers not supported'], + [162, 'Wildcard Subscriptions not supported'], + ])('returns correct error reason for code %i', (code, expected) => { + expect(getErrorReason(code)).toBe(expected) + }) + + test.each([[0], [1], [163], [1000], [Number.MAX_SAFE_INTEGER]])( + 'returns "Unknown error" for unknown error code %i', + (code) => { + expect(getErrorReason(code)).toBe('Unknown error') + }, + ) + + test.each([ + [-1], + [-Infinity], + [Infinity], + [NaN], + [null], + [undefined], + ['string'], + [true], + [false], + [[]], + [{}], + [() => {}], + ])('handles edge case input %p', (input: unknown) => { + expect(getErrorReason(input as number)).toBe('Unknown error') + }) + + test('function should not modify input', () => { + const originalInput = 128 + getErrorReason(originalInput) + expect(originalInput).toBe(128) + }) +}) diff --git a/cli/src/__tests__/utils/parse.test.ts b/cli/src/__tests__/utils/parse.test.ts new file mode 100644 index 000000000..9688669cb --- /dev/null +++ b/cli/src/__tests__/utils/parse.test.ts @@ -0,0 +1,150 @@ +import { + parseNumber, + parseProtocol, + parseMQTTVersion, + parseKeyValues, + parseQoS, + parseFormat, + parseSchemaOptions, +} from '../../utils/parse' +import logWrapper from '../../utils/logWrapper' +import { expect, jest } from '@jest/globals' + +// Mock the logWrapper and process.exit +jest.mock('../../utils/logWrapper', () => ({ + fail: jest.fn(), +})) +const mockExit = jest.spyOn(process, 'exit').mockImplementation((code?: number) => { + throw new Error(`Process exited with code ${code}`) +}) + +describe('parse utilities', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('parseNumber', () => { + it('should parse valid numbers', () => { + expect(parseNumber('42')).toBe(42) + expect(parseNumber('-3.14')).toBe(-3.14) + }) + + it('should throw an error for invalid numbers', () => { + expect(() => parseNumber('not a number')).toThrow() + expect(logWrapper.fail).toHaveBeenCalledWith('not a number is not a number.') + expect(mockExit).toHaveBeenCalledWith(1) + }) + }) + + describe('parseProtocol', () => { + it('should accept valid protocols', () => { + expect(parseProtocol('mqtt')).toBe('mqtt') + expect(parseProtocol('mqtts')).toBe('mqtts') + expect(parseProtocol('ws')).toBe('ws') + expect(parseProtocol('wss')).toBe('wss') + }) + + it('should throw an error for invalid protocols', () => { + expect(() => parseProtocol('http')).toThrow() + expect(logWrapper.fail).toHaveBeenCalledWith('Only mqtt, mqtts, ws and wss are supported.') + expect(mockExit).toHaveBeenCalledWith(1) + }) + }) + + describe('parseMQTTVersion', () => { + it('should parse valid MQTT versions', () => { + expect(parseMQTTVersion('3.1')).toBe(3) + expect(parseMQTTVersion('3.1.1')).toBe(4) + expect(parseMQTTVersion('5')).toBe(5) + expect(parseMQTTVersion('5.0')).toBe(5) + }) + + it('should throw an error for invalid MQTT versions', () => { + expect(() => parseMQTTVersion('4.0')).toThrow() + expect(logWrapper.fail).toHaveBeenCalledWith('Not a valid MQTT version.') + expect(mockExit).toHaveBeenCalledWith(1) + }) + + it('should parse "5.0" as version 5', () => { + expect(parseMQTTVersion('5.0')).toBe(5) + }) + }) + + describe('parseKeyValues', () => { + it('should parse a single key-value pair', () => { + expect(parseKeyValues('key: value')).toEqual({ key: 'value' }) + }) + + it('should add to existing key-value pairs', () => { + const previous = { existingKey: 'existingValue' } + expect(parseKeyValues('newKey: newValue', previous)).toEqual({ + existingKey: 'existingValue', + newKey: 'newValue', + }) + }) + + it('should handle multiple values for the same key', () => { + const previous = { key: 'value1' } + expect(parseKeyValues('key: value2', previous)).toEqual({ key: ['value1', 'value2'] }) + }) + + it('should throw an error for invalid key-value pairs', () => { + expect(() => parseKeyValues('invalid')).toThrow() + expect(logWrapper.fail).toHaveBeenCalledWith( + 'Invalid key-value pair: "invalid". Expected format is "key: value".', + ) + expect(mockExit).toHaveBeenCalledWith(1) + }) + }) + + describe('parseQoS', () => { + it('should parse valid QoS values', () => { + expect(parseQoS('0', undefined)).toEqual([0]) + expect(parseQoS('1', [0])).toEqual([0, 1]) + expect(parseQoS('2', [0, 1])).toEqual([0, 1, 2]) + }) + + it('should throw an error for invalid QoS values', () => { + expect(() => parseQoS('3', undefined)).toThrow() + expect(logWrapper.fail).toHaveBeenCalledWith('3 is not a valid QoS.') + expect(mockExit).toHaveBeenCalledWith(1) + }) + }) + + describe('parseFormat', () => { + it('should accept valid format types', () => { + expect(parseFormat('base64')).toBe('base64') + expect(parseFormat('json')).toBe('json') + expect(parseFormat('hex')).toBe('hex') + expect(parseFormat('cbor')).toBe('cbor') + expect(parseFormat('binary')).toBe('binary') + }) + + it('should throw an error for invalid format types', () => { + expect(() => parseFormat('xml')).toThrow() + expect(logWrapper.fail).toHaveBeenCalledWith('Not a valid format type.') + expect(mockExit).toHaveBeenCalledWith(1) + }) + }) + + describe('parseSchemaOptions', () => { + it('should return protobuf schema options when protobuf parameters are provided', () => { + expect(parseSchemaOptions('path/to/proto', 'MessageName')).toEqual({ + type: 'protobuf', + protobufPath: 'path/to/proto', + protobufMessageName: 'MessageName', + }) + }) + + it('should return avro schema options when avsc path is provided', () => { + expect(parseSchemaOptions(undefined, undefined, 'path/to/avsc')).toEqual({ + type: 'avro', + avscPath: 'path/to/avsc', + }) + }) + + it('should return undefined when no schema options are provided', () => { + expect(parseSchemaOptions()).toBeUndefined() + }) + }) +}) diff --git a/cli/src/__tests__/utils/protobufErrors.test.ts b/cli/src/__tests__/utils/protobufErrors.test.ts new file mode 100644 index 000000000..45ef2a07e --- /dev/null +++ b/cli/src/__tests__/utils/protobufErrors.test.ts @@ -0,0 +1,32 @@ +import { transformPBJSError } from '../../utils/protobufErrors' +import { expect, describe, it } from '@jest/globals' + +describe('transformPBJSError', () => { + it('prepends message with deserialization error', () => { + const error = new Error('Some error') + const transformedError = transformPBJSError(error) + expect(transformedError.message).toMatch(/^Message deserialization error: Some error$/) + }) + + it('transforms index out of range error', () => { + const error = new Error('index out of range: 10 + 5 > 12') + const transformedError = transformPBJSError(error) + expect(transformedError.message).toMatch( + /^Message deserialization error: Index out of range: the reader was at position 10 and tried to read 5 more \(bytes\), but the given buffer was 12 bytes$/, + ) + }) + + it('handles non-matching index out of range error', () => { + const error = new Error('Some other index out of range error') + const transformedError = transformPBJSError(error) + expect(transformedError.message).toBe('Message deserialization error: Some other index out of range error') + }) + + it('handles multiple transformations', () => { + const error = new Error('index out of range: 100 + 50 > 120') + const transformedError = transformPBJSError(error) + expect(transformedError.message).toMatch( + /^Message deserialization error: Index out of range: the reader was at position 100 and tried to read 50 more \(bytes\), but the given buffer was 120 bytes$/, + ) + }) +}) From bb16fca8cd21330c876699c4981da1a1ffc17f12 Mon Sep 17 00:00:00 2001 From: YuShifan <894402575bt@gmail.com> Date: Wed, 4 Sep 2024 00:47:16 +0800 Subject: [PATCH 2/3] fix(cli): fix type error on test files --- cli/src/__tests__/utils/binaryFormats.test.ts | 1 + cli/src/__tests__/utils/delay.test.ts | 1 + cli/src/__tests__/utils/jsonUtils.test.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/cli/src/__tests__/utils/binaryFormats.test.ts b/cli/src/__tests__/utils/binaryFormats.test.ts index 7673a4eff..1af1c43f8 100644 --- a/cli/src/__tests__/utils/binaryFormats.test.ts +++ b/cli/src/__tests__/utils/binaryFormats.test.ts @@ -1,4 +1,5 @@ import isSupportedBinaryFormatForMQTT, { supportedBinaryFormatsForMQTT } from '../../utils/binaryFormats' +import { expect, describe, it } from '@jest/globals' describe('isSupportedBinaryFormatForMQTT', () => { it('should return true for supported binary formats', () => { diff --git a/cli/src/__tests__/utils/delay.test.ts b/cli/src/__tests__/utils/delay.test.ts index 4ede4d036..96f140300 100644 --- a/cli/src/__tests__/utils/delay.test.ts +++ b/cli/src/__tests__/utils/delay.test.ts @@ -1,4 +1,5 @@ import delay from '../../utils/delay' +import { expect, describe, it } from '@jest/globals' describe('delay function', () => { it('should delay execution for the specified time', async () => { diff --git a/cli/src/__tests__/utils/jsonUtils.test.ts b/cli/src/__tests__/utils/jsonUtils.test.ts index 31945f2da..38db2a4ec 100644 --- a/cli/src/__tests__/utils/jsonUtils.test.ts +++ b/cli/src/__tests__/utils/jsonUtils.test.ts @@ -1,4 +1,5 @@ import { jsonParse, jsonStringify } from '../../utils/jsonUtils' +import { expect, describe, it } from '@jest/globals' describe('jsonUtils', () => { describe('jsonParse', () => { From 51eb2b276a8809483a7208c2f52f6622049b6a26 Mon Sep 17 00:00:00 2001 From: YuShifan <894402575bt@gmail.com> Date: Wed, 4 Sep 2024 01:26:44 +0800 Subject: [PATCH 3/3] test(cli): add simulate function test case --- cli/src/__tests__/utils/simulate.test.ts | 73 ++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 cli/src/__tests__/utils/simulate.test.ts diff --git a/cli/src/__tests__/utils/simulate.test.ts b/cli/src/__tests__/utils/simulate.test.ts new file mode 100644 index 000000000..050e66b9d --- /dev/null +++ b/cli/src/__tests__/utils/simulate.test.ts @@ -0,0 +1,73 @@ +import { loadSimulator } from '../../utils/simulate' +import { expect, describe, it, jest, beforeEach } from '@jest/globals' +import * as path from 'path' + +jest.mock('fs', () => ({ + existsSync: jest.fn().mockReturnValue(true), + readdirSync: jest.fn().mockReturnValue(['IEM.js', 'tesla.js']), + statSync: jest.fn().mockReturnValue({ birthtime: new Date() }), +})) + +const actualPath = jest.requireActual('path') +jest.mock('path', () => { + const actualPath = jest.requireActual('path') as typeof import('path') + return { + ...actualPath, + join: jest.fn().mockImplementation((...args: any[]) => actualPath.join(...args)), + resolve: jest.fn().mockImplementation((...args: any[]) => actualPath.resolve(...args)), + } +}) + +describe('loadSimulator', () => { + const mockGenerator = jest.fn() + const mockSimulatorModule = { + generator: mockGenerator, + name: 'IEM', + author: 'EMQX Team', + dataFormat: 'JSON', + version: '1.0.0', + description: 'Test simulator description', + } + + beforeEach(() => { + jest.resetModules() + const scenarioPath = path.join(__dirname, '../../scenarios/IEM.js') + jest.doMock(scenarioPath, () => mockSimulatorModule, { virtual: true }) + }) + + it('should load a simulator successfully', () => { + const simulator = loadSimulator('IEM') + + expect(simulator).toEqual({ + ...mockSimulatorModule, + generator: expect.any(Function), + file: undefined, + realFilePath: expect.stringContaining('IEM.js'), + }) + + const options: any = { clientId: 'test', count: 1 } + simulator.generator(options) + expect(mockGenerator).toHaveBeenCalledWith(expect.any(Object), options) + }) + + it('should throw an error for invalid file type', () => { + expect(() => loadSimulator('invalidFile', 'invalid.txt')).toThrow('Invalid file type') + }) + + // it('should throw an error for non-existent file', () => { + // const fs = require('fs') + // fs.existsSync.mockReturnValueOnce(false) + // expect(() => loadSimulator(undefined, 'nonexistent.js')).toThrow((error: Error) => { + // expect(error).toBeInstanceOf(Error) + // expect(error.message).toMatch(/Load simulator error: Error: Cannot find module/) + // expect(error.message).toContain('nonexistent.js') + // return true + // }) + // }) + + it('should throw an error for invalid simulator module', () => { + const invalidModulePath = path.join(__dirname, '../../scenarios/invalidModule.js') + jest.doMock(invalidModulePath, () => ({}), { virtual: true }) + expect(() => loadSimulator('invalidModule')).toThrow('Not a valid simulator module') + }) +})