Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(cli): add more utils test cases #1750

Merged
merged 3 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/src/__tests__/utils/binaryFormats.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
1 change: 1 addition & 0 deletions cli/src/__tests__/utils/delay.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
1 change: 1 addition & 0 deletions cli/src/__tests__/utils/jsonUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { jsonParse, jsonStringify } from '../../utils/jsonUtils'
import { expect, describe, it } from '@jest/globals'

describe('jsonUtils', () => {
describe('jsonParse', () => {
Expand Down
79 changes: 79 additions & 0 deletions cli/src/__tests__/utils/mqttErrorReason.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
150 changes: 150 additions & 0 deletions cli/src/__tests__/utils/parse.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
})
32 changes: 32 additions & 0 deletions cli/src/__tests__/utils/protobufErrors.test.ts
Original file line number Diff line number Diff line change
@@ -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$/,
)
})
})
73 changes: 73 additions & 0 deletions cli/src/__tests__/utils/simulate.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
Loading