Skip to content
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
2 changes: 2 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"mqtt": "^4.3.7",
"msgpackr": "^1.11.0",
"ora": "^5.4.1",
"pbkdf2": "^3.1.5",
"protobufjs": "^7.2.3",
"pump": "^3.0.0",
"readable-stream": "^3.6.0",
Expand All @@ -55,6 +56,7 @@
"@types/json-bigint": "^1.0.4",
"@types/lodash": "^4.17.1",
"@types/node": "^17.0.43",
"@types/pbkdf2": "^3.1.2",
"@types/pump": "^1.1.1",
"@types/readable-stream": "^2.3.13",
"@types/signale": "^1.4.4",
Expand Down
67 changes: 67 additions & 0 deletions cli/src/__tests__/utils/createMqttClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect, describe, jest } from '@jest/globals'
import { createMqttClient } from '../../utils/mqttConnect'

// Mock mqtt.connect
jest.mock('mqtt', () => ({
connect: jest.fn(() => ({
on: jest.fn(),
handleAuth: undefined,
})),
}))

describe('createMqttClient', () => {
test('should create MQTT client successfully', () => {
const connOpts = {
protocolVersion: 5,
hostname: 'localhost',
port: 1883,
username: 'admin',
password: 'public',
}

const client = createMqttClient(connOpts)

expect(client).toBeDefined()
expect(typeof client.on).toBe('function')
})

test('should create MQTT client with hostname broker.emqx.io', () => {
const connOpts = {
protocolVersion: 5,
hostname: 'broker.emqx.io',
port: 1883,
username: 'admin',
password: 'public',
}

const client = createMqttClient(connOpts)

expect(client).toBeDefined()
})

test('should create MQTT client for MQTT 3.1.1', () => {
const connOpts = {
protocolVersion: 4,
hostname: 'localhost',
port: 1883,
username: 'admin',
password: 'public',
}

const client = createMqttClient(connOpts)

expect(client).toBeDefined()
})

test('should create MQTT client without credentials', () => {
const connOpts = {
protocolVersion: 5,
hostname: 'localhost',
port: 1883,
}

const client = createMqttClient(connOpts)

expect(client).toBeDefined()
})
})
187 changes: 187 additions & 0 deletions cli/src/__tests__/utils/scramAuth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/// <reference types="../../types/global" />

import { ScramAuth, ScramAlgorithm } from '../../utils/scramAuth'
import { expect, describe, it, jest, beforeEach } from '@jest/globals'
import * as crypto from 'crypto'

// Mock crypto.randomBytes to make tests deterministic
jest.mock('crypto', () => ({
...jest.requireActual<typeof crypto>('crypto'),
randomBytes: jest.fn(),
}))

const mockedCrypto = crypto as jest.Mocked<typeof crypto>

describe('ScramAuth', () => {
beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks()
// Mock randomBytes to return a predictable value that will create the client nonce 'fyko+d2lbbFgONRv9qkxdawL'
mockedCrypto.randomBytes.mockImplementation(() => Buffer.from('fyko+d2lbbFgONRv9qkxdawL', 'base64'))
})

describe('constructor', () => {
it('should create ScramAuth instance with default algorithm', () => {
const scramAuth = new ScramAuth('testuser', 'testpass')
expect(scramAuth).toBeInstanceOf(ScramAuth)
})

it('should create ScramAuth instance with specified algorithm', () => {
const scramAuth = new ScramAuth('testuser', 'testpass', 'SCRAM-SHA-1')
expect(scramAuth).toBeInstanceOf(ScramAuth)
})

it('should generate client nonce on construction', () => {
new ScramAuth('testuser', 'testpass')
expect(mockedCrypto.randomBytes).toHaveBeenCalledWith(32)
})
})

describe('getHashFunction', () => {
it('should return correct hash function for SCRAM-SHA-1', () => {
const scramAuth = new ScramAuth('testuser', 'testpass', 'SCRAM-SHA-1')
const firstMessage = scramAuth.clientFirst()
expect(firstMessage).toBeInstanceOf(Buffer)
})

it('should return correct hash function for SCRAM-SHA-256', () => {
const scramAuth = new ScramAuth('testuser', 'testpass', 'SCRAM-SHA-256')
const firstMessage = scramAuth.clientFirst()
expect(firstMessage).toBeInstanceOf(Buffer)
})

it('should return correct hash function for SCRAM-SHA-512', () => {
const scramAuth = new ScramAuth('testuser', 'testpass', 'SCRAM-SHA-512')
const firstMessage = scramAuth.clientFirst()
expect(firstMessage).toBeInstanceOf(Buffer)
})

it('should throw error for unsupported algorithm', () => {
expect(() => {
const scramAuth = new ScramAuth('testuser', 'testpass', 'INVALID-ALGORITHM' as ScramAlgorithm)
scramAuth.clientFirst()
const serverFirstMessage = Buffer.from('r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096')
scramAuth.clientFinal(serverFirstMessage)
}).toThrow('Unsupported algorithm: INVALID-ALGORITHM')
})
})

describe('saslPrep', () => {
it('should escape = and , characters in username', () => {
const scramAuth = new ScramAuth('test=user,name', 'testpass')
const firstMessage = scramAuth.clientFirst()
const messageStr = firstMessage.toString()
expect(messageStr).toContain('n=test=3Duser=2Cname')
})
})

describe('clientFirst', () => {
it('should generate client first message with correct format', () => {
const scramAuth = new ScramAuth('testuser', 'testpass')
const firstMessage = scramAuth.clientFirst()
const messageStr = firstMessage.toString()

expect(messageStr).toMatch(/^n,,n=testuser,r=.+/)
expect(messageStr).toContain('n=testuser')
})

it('should handle special characters in username', () => {
const scramAuth = new ScramAuth('test=user,name', 'testpass')
const firstMessage = scramAuth.clientFirst()
const messageStr = firstMessage.toString()

expect(messageStr).toContain('n=test=3Duser=2Cname')
})

it('should return buffer', () => {
const scramAuth = new ScramAuth('testuser', 'testpass')
const firstMessage = scramAuth.clientFirst()
expect(firstMessage).toBeInstanceOf(Buffer)
})
})

describe('clientFinal', () => {
let scramAuth: ScramAuth

beforeEach(() => {
scramAuth = new ScramAuth('user', 'pencil', 'SCRAM-SHA-1')
scramAuth.clientFirst()
})

it('should generate client final message for valid server response', () => {
const serverFirstMessage = Buffer.from('r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096')

const clientFinal = scramAuth.clientFinal(serverFirstMessage)
expect(clientFinal).toBeInstanceOf(Buffer)

const finalStr = clientFinal.toString()
expect(finalStr).toMatch(/^c=biws,r=.+,p=.+/)
})

it('should throw error for invalid server nonce', () => {
const serverFirstMessage = Buffer.from('r=invalid-nonce,s=QSXCR+Q6sek8bf92,i=4096')

expect(() => {
scramAuth.clientFinal(serverFirstMessage)
}).toThrow('Invalid server nonce')
})

it('should parse server parameters correctly', () => {
const serverFirstMessage = Buffer.from('r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096')

expect(() => {
scramAuth.clientFinal(serverFirstMessage)
}).not.toThrow()
})

it('should handle malformed server message gracefully', () => {
const serverFirstMessage = Buffer.from('invalid=message=format')

expect(() => {
scramAuth.clientFinal(serverFirstMessage)
}).toThrow()
})
})

describe('algorithm-specific behavior', () => {
const testCases: Array<{ algorithm: ScramAlgorithm; expectedLength: number }> = [
{ algorithm: 'SCRAM-SHA-1', expectedLength: 20 },
{ algorithm: 'SCRAM-SHA-256', expectedLength: 32 },
{ algorithm: 'SCRAM-SHA-512', expectedLength: 64 },
]

testCases.forEach(({ algorithm }) => {
it(`should work correctly with ${algorithm}`, () => {
const scramAuth = new ScramAuth('testuser', 'testpass', algorithm)
const firstMessage = scramAuth.clientFirst()

expect(firstMessage).toBeInstanceOf(Buffer)
expect(firstMessage.toString()).toContain('n=testuser')
})
})
})

describe('error handling', () => {
it('should handle empty username', () => {
const scramAuth = new ScramAuth('', 'testpass')
const firstMessage = scramAuth.clientFirst()
expect(firstMessage.toString()).toContain('n=,')
})

it('should handle empty password', () => {
const scramAuth = new ScramAuth('testuser', '')
expect(() => scramAuth.clientFirst()).not.toThrow()
})

it('should handle server message without required parameters', () => {
const scramAuth = new ScramAuth('user', 'pencil')
scramAuth.clientFirst()

const incompleteServerMessage = Buffer.from('r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j')

expect(() => {
scramAuth.clientFinal(incompleteServerMessage)
}).toThrow()
})
})
})
Loading
Loading