Skip to content

Commit

Permalink
chore: test
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian committed Oct 5, 2023
1 parent e5698e5 commit 633a0cf
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 29 deletions.
249 changes: 249 additions & 0 deletions src/Ember/Client/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import {
NumberedTreeNode,
EmberElement,
NumberedTreeNodeImpl,
EmberNodeImpl,
ParameterImpl,
ParameterType,
QualifiedElementImpl,
} from '../../../model'
import { Collection, Root, RootElement } from '../../../types/types'
import { EmberClient } from '../'
import S101ClientMock from '../../../__mocks__/S101Client'
import { DecodeResult } from '../../../encodings/ber/decoder/DecodeResult'
// import { EmberTreeNode, RootElement } from '../../../types/types'
// import { ElementType, EmberElement } from '../../../model/EmberElement'
// import { Parameter, ParameterType } from '../../../model/Parameter'

jest.mock('../../Socket/S101Client', () => require('../../../__mocks__/S101Client'))

describe('client', () => {
const onSocketCreate = jest.fn()
const onConnection = jest.fn()
const onSocketClose = jest.fn()
const onSocketWrite = jest.fn()
const onConnectionChanged = jest.fn()

function setupSocketMock() {
S101ClientMock.mockOnNextSocket((socket: any) => {
onSocketCreate()

socket.onConnect = onConnection
socket.onWrite = onSocketWrite
socket.onClose = onSocketClose
})
}

beforeEach(() => {
setupSocketMock()
})
afterEach(() => {
const sockets = S101ClientMock.openSockets()
// Destroy any lingering sockets, to prevent a failing test from affecting other tests:
sockets.forEach((s) => s.destroy())

S101ClientMock.clearMockOnNextSocket()
onSocketCreate.mockClear()
onConnection.mockClear()
onSocketClose.mockClear()
onSocketWrite.mockClear()
onConnectionChanged.mockClear()

// Just a check to ensure that the unit tests cleaned up the socket after themselves:
// eslint-disable-next-line jest/no-standalone-expect
expect(sockets).toHaveLength(0)
})

async function runWithConnection(fn: (connection: EmberClient, socket: S101ClientMock) => Promise<void>) {
const client = new EmberClient('test')
try {
expect(client).toBeTruthy()

await client.connect()

// Wait for connection
await new Promise(setImmediate)

// Should be connected
expect(client.connected).toBeTruthy()

const sockets = S101ClientMock.openSockets()
expect(sockets).toHaveLength(1)
expect(onSocketWrite).toHaveBeenCalledTimes(0)

await fn(client, sockets[0])
} finally {
// Ensure cleaned up
await client.disconnect()
client.discard()

await new Promise(setImmediate)
}
}

function createQualifiedNodeResponse(
path: string,
content: EmberElement,
children: Collection<NumberedTreeNode<EmberElement>>
): DecodeResult<Root> {
const parent = new QualifiedElementImpl<EmberElement>(path, content, children)

const fixLevel = (node: NumberedTreeNode<EmberElement>, parent: NumberedTreeNode<EmberElement>) => {
node.parent = parent

for (const child of Object.values<NumberedTreeNode<EmberElement>>(node.children ?? {})) {
fixLevel(child, node)
}
}
for (const child of Object.values<NumberedTreeNode<EmberElement>>(children)) {
fixLevel(child, parent as any as NumberedTreeNode<EmberElement>)
}
return {
value: {
0: parent as Exclude<RootElement, NumberedTreeNode<EmberElement>>,
},
}
}

it('getDirectory resolves', async () => {
await runWithConnection(async (client, socket) => {
// Do initial load
const getRootDirReq = await client.getDirectory(client.tree)
getRootDirReq.response?.catch(() => null) // Ensure uncaught response is ok
expect(onSocketWrite).toHaveBeenCalledTimes(1)
// TODO: should the value of the call be checked?

// Mock a valid response
socket.mockData({
value: {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('Ruby', undefined, undefined, true)),
},
})

// Should have a response
const res = (await getRootDirReq.response) as NumberedTreeNodeImpl<EmberElement>
expect(res).toMatchObject(new NumberedTreeNodeImpl(1, new EmberNodeImpl('Ruby', undefined, undefined, true)))
})
})

it('getElementByPath', async () => {
await runWithConnection(async (client, socket) => {
// Do initial load
const getRootDirReq = await client.getDirectory(client.tree)
getRootDirReq.response?.catch(() => null) // Ensure uncaught response is ok
expect(onSocketWrite).toHaveBeenCalledTimes(1)
onSocketWrite.mockClear()

// Mock a valid response
socket.mockData({
value: {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('Ruby', undefined, undefined, true)),
},
})
await getRootDirReq.response

// Run the tree
const getByPathPromise = client.getElementByPath('Ruby.Sums.On')

// First lookup
expect(onSocketWrite).toHaveBeenCalledTimes(1)
socket.mockData({
value: {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('Ruby', undefined, undefined, true), {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('Sums', undefined, undefined, true)),
}),
},
})

await new Promise(setImmediate)

// Second lookup
expect(onSocketWrite).toHaveBeenCalledTimes(2)
socket.mockData({
value: {
1: new QualifiedElementImpl<EmberElement>('1.1', new EmberNodeImpl('Sums', undefined, undefined, false), {
1: new NumberedTreeNodeImpl(1, new ParameterImpl(ParameterType.Boolean, 'On', undefined, false)),
}) as Exclude<RootElement, NumberedTreeNode<EmberElement>>,
},
})

await new Promise(setImmediate)

const res = await getByPathPromise
expect(res).toBeTruthy()
expect(res).toMatchObject(
new NumberedTreeNodeImpl(1, new ParameterImpl(ParameterType.Boolean, 'On', undefined, false))
)
})
})

it('getElementByPath concurrent', async () => {
await runWithConnection(async (client, socket) => {
// Do initial load
const getRootDirReq = await client.getDirectory(client.tree)
getRootDirReq.response?.catch(() => null) // Ensure uncaught response is ok
expect(onSocketWrite).toHaveBeenCalledTimes(1)
onSocketWrite.mockClear()

// Mock a valid response
socket.mockData({
value: {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('Ruby', undefined, undefined, true)),
},
})
await getRootDirReq.response

// Run the tree
const getByPathPromise = client.getElementByPath('Ruby.Sums.MAIN.On')
const getByPathPromise2 = client.getElementByPath('Ruby.Sums.MAIN.Second')

// First lookup from both
expect(onSocketWrite).toHaveBeenCalledTimes(2)
socket.mockData(
createQualifiedNodeResponse('1', new EmberNodeImpl('Ruby', undefined, undefined, true), {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('Sums', undefined, undefined, false)),
})
)

socket.mockData(
createQualifiedNodeResponse('1', new EmberNodeImpl('Ruby', undefined, undefined, true), {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('Sums', undefined, undefined, false)),
})
)

await new Promise(setImmediate)

// Second lookup
expect(onSocketWrite).toHaveBeenCalledTimes(4)
socket.mockData(
createQualifiedNodeResponse('1.1', new EmberNodeImpl('Sums', undefined, undefined, false), {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('MAIN', undefined, undefined, false)),
})
)
await new Promise(setImmediate)
socket.mockData(
createQualifiedNodeResponse('1.1', new EmberNodeImpl('Sums', undefined, undefined, false), {
1: new NumberedTreeNodeImpl(1, new EmberNodeImpl('MAIN', undefined, undefined, false)),
})
)

await new Promise(setImmediate)

// Final lookup
expect(onSocketWrite).toHaveBeenCalledTimes(6)
socket.mockData(
createQualifiedNodeResponse('1.1.1', new EmberNodeImpl('MAIN', undefined, undefined, false), {
1: new NumberedTreeNodeImpl(1, new ParameterImpl(ParameterType.Boolean, 'On', undefined, false)),
2: new NumberedTreeNodeImpl(1, new ParameterImpl(ParameterType.Boolean, 'Second', undefined, false)),
})
)

// Both completed successfully
const res = await getByPathPromise
expect(res).toBeTruthy()

const res2 = await getByPathPromise2
expect(res2).toBeTruthy()
})
})
})
2 changes: 1 addition & 1 deletion src/Ember/Server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class EmberServer extends EventEmitter<EmberServerEvents> {
this._server.on('connection', (client: S101Socket) => {
this._clients.add(client)

client.on('emberTree', (tree: DecodeResult<Collection<RootElement>>) => this._handleIncoming(tree, client))
client.on('emberTree', (tree) => this._handleIncoming(tree as DecodeResult<Collection<RootElement>>, client))

client.on('error', (e) => {
this.emit('clientError', client, e)
Expand Down
4 changes: 2 additions & 2 deletions src/Ember/Socket/S101Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ export default class S101Client extends S101Socket {
return super.disconnect(timeout)
}

handleClose(): void {
protected handleClose(): void {
if (this.keepaliveIntervalTimer) clearInterval(this.keepaliveIntervalTimer)
this.socket?.destroy()
}

_autoReconnectionAttempt(): void {
private _autoReconnectionAttempt(): void {
if (this._autoReconnect) {
if (this._reconnectAttempts > 0) {
// no reconnection if no valid reconnectionAttemps is set
Expand Down
41 changes: 15 additions & 26 deletions src/Ember/Socket/S101Socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,34 @@ import { S101Codec } from '../../S101'
import { berDecode } from '../..'
import { ConnectionStatus } from '../Client'
import { normalizeError } from '../Lib/util'
import { Root } from '../../types'
import { DecodeResult } from '../../encodings/ber/decoder/DecodeResult'

export type Request = any

export type S101SocketEvents = {
error: [Error]
emberPacket: [packet: Buffer]
emberTree: [root: any]
emberTree: [root: DecodeResult<Root>]
connecting: []
connected: []
disconnected: []
}

export default class S101Socket extends EventEmitter<S101SocketEvents> {
socket: Socket | undefined
keepaliveInterval = 10
keepaliveMaxResponseTime = 500
keepaliveIntervalTimer: NodeJS.Timeout | undefined
keepaliveResponseWindowTimer: NodeJS.Timer | null
pendingRequests: Array<Request> = []
activeRequest: Request | undefined
protected socket: Socket | undefined
private readonly keepaliveInterval = 10
private readonly keepaliveMaxResponseTime = 500
protected keepaliveIntervalTimer: NodeJS.Timeout | undefined
private keepaliveResponseWindowTimer: NodeJS.Timer | null
status: ConnectionStatus
codec = new S101Codec()
protected readonly codec = new S101Codec()

constructor(socket?: Socket) {
super()
this.socket = socket
this.keepaliveIntervalTimer = undefined
this.keepaliveResponseWindowTimer = null
this.activeRequest = undefined
this.status = this.isConnected() ? ConnectionStatus.Connected : ConnectionStatus.Disconnected

this.codec.on('keepaliveReq', () => {
Expand All @@ -59,7 +58,7 @@ export default class S101Socket extends EventEmitter<S101SocketEvents> {
this._initSocket()
}

_initSocket(): void {
private _initSocket(): void {
if (this.socket != null) {
this.socket.on('data', (data) => {
try {
Expand All @@ -82,16 +81,6 @@ export default class S101Socket extends EventEmitter<S101SocketEvents> {
}
}

/**
* @returns {string} - ie: "10.1.1.1:9000"
*/
remoteAddress(): string {
if (this.socket === undefined) {
return 'not connected'
}
return `${this.socket.remoteAddress}:${this.socket.remotePort}`
}

/**
* @param {number} timeout=2
*/
Expand Down Expand Up @@ -130,14 +119,14 @@ export default class S101Socket extends EventEmitter<S101SocketEvents> {
/**
*
*/
handleClose(): void {
protected handleClose(): void {
this.socket = undefined
if (this.keepaliveIntervalTimer) clearInterval(this.keepaliveIntervalTimer)
this.status = ConnectionStatus.Disconnected
this.emit('disconnected')
}

isConnected(): boolean {
private isConnected(): boolean {
return this.socket !== undefined && !!this.socket
}

Expand All @@ -161,7 +150,7 @@ export default class S101Socket extends EventEmitter<S101SocketEvents> {
/**
*
*/
sendKeepaliveRequest(): void {
private sendKeepaliveRequest(): void {
if (this.isConnected() && this.socket) {
try {
this.socket.write(this.codec.keepAliveRequest())
Expand All @@ -177,7 +166,7 @@ export default class S101Socket extends EventEmitter<S101SocketEvents> {
/**
*
*/
sendKeepaliveResponse(): void {
private sendKeepaliveResponse(): void {
if (this.isConnected() && this.socket) {
try {
this.socket.write(this.codec.keepAliveResponse())
Expand All @@ -193,7 +182,7 @@ export default class S101Socket extends EventEmitter<S101SocketEvents> {
// this.sendBER(ber)
// }

startKeepAlive(): void {
protected startKeepAlive(): void {
this.keepaliveIntervalTimer = setInterval(() => {
try {
this.sendKeepaliveRequest()
Expand Down
Loading

0 comments on commit 633a0cf

Please sign in to comment.