--- /dev/null
+Open `@/.github/copilot-instructions.md`, read it and strictly follow the instructions.
--- /dev/null
+{
+ "supervisionUrls": ["ws://localhost:9000"],
+ "supervisionUrlOcppConfiguration": true,
+ "supervisionUrlOcppKey": "CentralSystemAddress",
+ "idTagsFile": "idtags.json",
+ "baseName": "CS-KEBA-OCPP2",
+ "chargePointModel": "KC-P30-ESS400C2-E0R",
+ "chargePointVendor": "Keba AG",
+ "firmwareVersion": "1.10.1",
+ "power": 22080,
+ "powerUnit": "W",
+ "numberOfConnectors": 1,
+ "randomConnectors": false,
+ "amperageLimitationOcppKey": "MaxAvailableCurrent",
+ "amperageLimitationUnit": "mA",
+ "ocppVersion": "2.0.1",
+ "Configuration": {
+ "configurationKey": [
+ {
+ "key": "MeterValuesSampledData",
+ "readonly": false,
+ "value": "Energy.Active.Import.Register,Power.Active.Import,Current.Import,Voltage"
+ },
+ {
+ "key": "MeterValueSampleInterval",
+ "readonly": false,
+ "value": "30"
+ },
+ {
+ "key": "SupportedFeatureProfiles",
+ "readonly": true,
+ "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
+ },
+ {
+ "key": "LocalAuthListEnabled",
+ "readonly": false,
+ "value": "false"
+ },
+ {
+ "key": "AuthorizeRemoteTxRequests",
+ "readonly": false,
+ "value": "false"
+ },
+ {
+ "key": "WebSocketPingInterval",
+ "readonly": false,
+ "value": "60"
+ },
+ {
+ "key": "ReserveConnectorZeroSupported",
+ "readonly": false,
+ "value": "false"
+ }
+ ]
+ },
+ "AutomaticTransactionGenerator": {
+ "enable": false,
+ "minDuration": 30,
+ "maxDuration": 60,
+ "minDelayBetweenTwoTransactions": 15,
+ "maxDelayBetweenTwoTransactions": 30,
+ "probabilityOfStart": 1,
+ "stopAfterHours": 0.3,
+ "requireAuthorize": true
+ },
+ "Evses": {
+ "0": {
+ "Connectors": {
+ "0": {}
+ }
+ },
+ "1": {
+ "Connectors": {
+ "1": {
+ "MeterValues": [
+ {
+ "unit": "V",
+ "measurand": "Voltage"
+ },
+ {
+ "unit": "W",
+ "measurand": "Power.Active.Import"
+ },
+ {
+ "unit": "A",
+ "measurand": "Current.Import"
+ },
+ {
+ "unit": "Wh"
+ }
+ ]
+ }
+ }
+ }
+ }
+}
WebSocketCloseEventStatusCode,
} from '../../types/index.js'
import {
- Constants,
getWebSocketCloseEventStatusString,
isNotEmptyString,
JSONStringify,
}
return undefined
})
- .catch(Constants.EMPTY_FUNCTION)
- .finally(() => {
+ .catch((error: unknown) => {
+ logger.error(
+ `${this.logPrefix(moduleName, 'start.ws.onmessage')} Request handler error:`,
+ error
+ )
this.responseHandlers.delete(requestId)
})
})
--- /dev/null
+// Copyright Jerome Benoit. 2024-2025. All Rights Reserved.
+
+import { expect } from '@std/expect'
+import { describe, it } from 'node:test'
+
+import type { UUIDv4 } from '../../../src/types/index.js'
+
+import { UIHttpServer } from '../../../src/charging-station/ui-server/UIHttpServer.js'
+import { ApplicationProtocol, ResponseStatus } from '../../../src/types/index.js'
+import { TEST_UUID } from './UIServerTestConstants.js'
+import { createMockUIServerConfiguration, MockServerResponse } from './UIServerTestUtils.js'
+
+class TestableUIHttpServer extends UIHttpServer {
+ public addResponseHandler (uuid: UUIDv4, res: MockServerResponse): void {
+ this.responseHandlers.set(uuid, res as never)
+ }
+
+ public getResponseHandlersSize (): number {
+ return this.responseHandlers.size
+ }
+}
+
+await describe('UIHttpServer test suite', async () => {
+ await it('Verify sendResponse() deletes handler after sending', () => {
+ const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
+ const server = new TestableUIHttpServer(config)
+ const res = new MockServerResponse()
+
+ server.addResponseHandler(TEST_UUID, res)
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(true)
+
+ server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
+
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(false)
+ expect(res.ended).toBe(true)
+ expect(res.statusCode).toBe(200)
+ })
+
+ await it('Verify sendResponse() logs error when handler not found', () => {
+ const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
+ const server = new TestableUIHttpServer(config)
+
+ server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
+
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(false)
+ })
+
+ await it('Verify sendResponse() sets correct status code for failure', () => {
+ const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
+ const server = new TestableUIHttpServer(config)
+ const res = new MockServerResponse()
+
+ server.addResponseHandler(TEST_UUID, res)
+ server.sendResponse([TEST_UUID, { status: ResponseStatus.FAILURE }])
+
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(false)
+ expect(res.ended).toBe(true)
+ expect(res.statusCode).toBe(400)
+ })
+
+ await it('Verify sendResponse() handles send errors gracefully', () => {
+ const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
+ const server = new TestableUIHttpServer(config)
+ const res = new MockServerResponse()
+ res.end = (): never => {
+ throw new Error('HTTP response end error')
+ }
+
+ server.addResponseHandler(TEST_UUID, res)
+ server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
+
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(false)
+ })
+
+ await it('Verify sendResponse() sets correct Content-Type header', () => {
+ const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
+ const server = new TestableUIHttpServer(config)
+ const res = new MockServerResponse()
+
+ server.addResponseHandler(TEST_UUID, res)
+ server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
+
+ expect(res.headers['Content-Type']).toBe('application/json')
+ })
+
+ await it('Verify response handlers cleanup', () => {
+ const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
+ const server = new TestableUIHttpServer(config)
+ const res1 = new MockServerResponse()
+ const res2 = new MockServerResponse()
+
+ server.addResponseHandler('uuid-1' as UUIDv4, res1)
+ server.addResponseHandler('uuid-2' as UUIDv4, res2)
+ expect(server.getResponseHandlersSize()).toBe(2)
+
+ server.sendResponse(['uuid-1' as UUIDv4, { status: ResponseStatus.SUCCESS }])
+ expect(server.getResponseHandlersSize()).toBe(1)
+
+ server.sendResponse(['uuid-2' as UUIDv4, { status: ResponseStatus.SUCCESS }])
+ expect(server.getResponseHandlersSize()).toBe(0)
+ })
+
+ await it('Verify handlers cleared on server stop', () => {
+ const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
+ const server = new TestableUIHttpServer(config)
+ const res = new MockServerResponse()
+
+ server.addResponseHandler(TEST_UUID, res)
+ expect(server.getResponseHandlersSize()).toBe(1)
+
+ server.stop()
+
+ expect(server.getResponseHandlersSize()).toBe(0)
+ })
+
+ await it('Verify response payload serialization', () => {
+ const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
+ const server = new TestableUIHttpServer(config)
+ const res = new MockServerResponse()
+ const payload = {
+ hashIdsSucceeded: ['station-1', 'station-2'],
+ status: ResponseStatus.SUCCESS,
+ }
+
+ server.addResponseHandler(TEST_UUID, res)
+ server.sendResponse([TEST_UUID, payload])
+
+ expect(res.body).toBeDefined()
+ // HTTP server sends only the payload, not [uuid, payload]
+ const parsedBody = JSON.parse(res.body ?? '{}') as Record<string, unknown>
+ expect(parsedBody.status).toBe('success')
+ expect(parsedBody.hashIdsSucceeded).toEqual(['station-1', 'station-2'])
+ })
+
+ await it('Verify response with error details', () => {
+ const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
+ const server = new TestableUIHttpServer(config)
+ const res = new MockServerResponse()
+ const payload = {
+ errorMessage: 'Test error',
+ hashIdsFailed: ['station-1'],
+ status: ResponseStatus.FAILURE,
+ }
+
+ server.addResponseHandler(TEST_UUID, res)
+ server.sendResponse([TEST_UUID, payload])
+
+ expect(res.body).toBeDefined()
+ // HTTP server sends only the payload, not [uuid, payload]
+ const parsedBody = JSON.parse(res.body ?? '{}') as Record<string, unknown>
+ expect(parsedBody.status).toBe('failure')
+ expect(parsedBody.errorMessage).toBe('Test error')
+ expect(parsedBody.hashIdsFailed).toEqual(['station-1'])
+ })
+
+ await it('Verify valid HTTP configuration', () => {
+ const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
+ const server = new UIHttpServer(config)
+
+ expect(server).toBeDefined()
+ })
+
+ await it('Verify HTTP server with custom config', () => {
+ const config = createMockUIServerConfiguration({
+ options: {
+ host: 'localhost',
+ port: 9090,
+ },
+ type: ApplicationProtocol.HTTP,
+ })
+
+ const server = new UIHttpServer(config)
+ expect(server).toBeDefined()
+ })
+})
--- /dev/null
+// Copyright Jerome Benoit. 2024-2025. All Rights Reserved.
+
+import type { UUIDv4 } from '../../../src/types/index.js'
+
+export const TEST_UUID = '550e8400-e29b-41d4-a716-446655440000' as UUIDv4
+export const TEST_UUID_2 = '6ba7b810-9dad-11d1-80b4-00c04fd430c8' as UUIDv4
+
+export const TEST_PROCEDURES = {
+ AUTHORIZE: 'Authorize',
+ DELETE_CHARGING_STATIONS: 'deleteChargingStations',
+ LIST_CHARGING_STATIONS: 'listChargingStations',
+ START_CHARGING_STATION: 'startChargingStation',
+ STOP_CHARGING_STATION: 'stopChargingStation',
+} as const
+
+export const TEST_HASH_ID = 'test-station-001' as const
+export const TEST_HASH_ID_2 = 'test-station-002' as const
--- /dev/null
+// Copyright Jerome Benoit. 2024-2025. All Rights Reserved.
+
+import type { IncomingMessage } from 'node:http'
+
+import { EventEmitter } from 'node:events'
+
+import type {
+ ChargingStationData,
+ ProtocolRequest,
+ ProtocolResponse,
+ RequestPayload,
+ UIServerConfiguration,
+ UUIDv4,
+} from '../../../src/types/index.js'
+
+import {
+ ApplicationProtocol,
+ ApplicationProtocolVersion,
+ AuthenticationType,
+ ProcedureName,
+ ResponseStatus,
+} from '../../../src/types/index.js'
+
+export const createMockUIServerConfiguration = (
+ overrides?: Partial<UIServerConfiguration>
+): UIServerConfiguration => {
+ return {
+ enabled: true,
+ options: {
+ host: 'localhost',
+ port: 8080,
+ },
+ type: ApplicationProtocol.WS,
+ version: ApplicationProtocolVersion.VERSION_11,
+ ...overrides,
+ }
+}
+
+export const createMockUIServerConfigurationWithAuth = (
+ overrides?: Partial<UIServerConfiguration>
+): UIServerConfiguration => {
+ return createMockUIServerConfiguration({
+ authentication: {
+ enabled: true,
+ password: 'test-password',
+ type: AuthenticationType.BASIC_AUTH,
+ username: 'test-user',
+ },
+ ...overrides,
+ })
+}
+
+export class MockServerResponse extends EventEmitter {
+ public body?: string
+ public ended = false
+ public headers: Record<string, string> = {}
+ public statusCode?: number
+
+ public end (data?: string): this {
+ this.body = data
+ this.ended = true
+ this.emit('finish')
+ return this
+ }
+
+ public getResponsePayload (): ProtocolResponse | undefined {
+ if (this.body == null) {
+ return undefined
+ }
+ return JSON.parse(this.body) as ProtocolResponse
+ }
+
+ public writeHead (statusCode: number, headers?: Record<string, string>): this {
+ this.statusCode = statusCode
+ if (headers != null) {
+ this.headers = headers
+ }
+ return this
+ }
+}
+
+export class MockWebSocket extends EventEmitter {
+ public protocol = 'ui0.0.1'
+ public readyState = 1 // OPEN
+ public sentMessages: string[] = []
+
+ public close (code?: number): void {
+ this.readyState = 3 // CLOSED
+ this.emit('close', code, Buffer.from(''))
+ }
+
+ public getLastSentMessage (): ProtocolResponse | undefined {
+ if (this.sentMessages.length === 0) {
+ return undefined
+ }
+ return JSON.parse(this.sentMessages[this.sentMessages.length - 1]) as ProtocolResponse
+ }
+
+ public send (data: string): void {
+ this.sentMessages.push(data)
+ }
+
+ public simulateError (error: Error): void {
+ this.emit('error', error)
+ }
+
+ public simulateMessage (data: string): void {
+ this.emit('message', Buffer.from(data))
+ }
+}
+
+export const createMockIncomingMessage = (
+ overrides?: Partial<IncomingMessage>
+): IncomingMessage => {
+ return {
+ headers: {},
+ method: 'POST',
+ url: '/ui',
+ ...overrides,
+ } as IncomingMessage
+}
+
+export const createMockChargingStationData = (
+ hashId: string,
+ overrides?: Partial<ChargingStationData>
+): ChargingStationData => {
+ return {
+ hashId,
+ started: true,
+ stationInfo: {
+ chargingStationId: hashId,
+ hashId,
+ },
+ timestamp: Date.now(),
+ ...overrides,
+ } as ChargingStationData
+}
+
+export const createProtocolRequest = (
+ uuid: UUIDv4,
+ procedureName: ProcedureName,
+ payload: RequestPayload = {}
+): ProtocolRequest => {
+ return [uuid, procedureName, payload]
+}
+
+export const createValidAuthorizeRequest = (uuid: UUIDv4, hashId: string): string => {
+ return JSON.stringify(
+ createProtocolRequest(uuid, ProcedureName.AUTHORIZE, {
+ hashIds: [hashId],
+ idTag: 'test-id-tag',
+ })
+ )
+}
+
+export const createValidListRequest = (uuid: UUIDv4): string => {
+ return JSON.stringify(createProtocolRequest(uuid, ProcedureName.LIST_CHARGING_STATIONS, {}))
+}
+
+export const createInvalidRequest = (): string => {
+ return '{"invalid": "json"'
+}
+
+export const createMalformedRequest = (): string => {
+ return JSON.stringify({ not: 'an array' })
+}
+
+export const waitForCondition = async (
+ condition: () => boolean,
+ timeout = 1000,
+ interval = 10
+): Promise<void> => {
+ const startTime = Date.now()
+ while (!condition()) {
+ if (Date.now() - startTime > timeout) {
+ throw new Error('Timeout waiting for condition')
+ }
+ await new Promise(resolve => {
+ setTimeout(resolve, interval)
+ })
+ }
+}
+
+export const createMockBroadcastResponse = (
+ uuid: string,
+ hashId: string,
+ status: ResponseStatus = ResponseStatus.SUCCESS
+): [string, { hashId: string; status: ResponseStatus }] => {
+ return [uuid, { hashId, status }]
+}
+
+export class MockUIServiceBroadcast {
+ requestHandler (): Promise<undefined> {
+ return Promise.resolve(undefined)
+ }
+}
+
+export class MockUIServiceError {
+ requestHandler (): Promise<never> {
+ return Promise.reject(new Error('Request handler error'))
+ }
+}
+
+export class MockUIServiceNonBroadcast {
+ requestHandler (request: ProtocolRequest): Promise<ProtocolResponse> {
+ return Promise.resolve([request[0], { status: ResponseStatus.SUCCESS }])
+ }
+}
--- /dev/null
+// Copyright Jerome Benoit. 2024-2025. All Rights Reserved.
+
+import { expect } from '@std/expect'
+import { describe, it } from 'node:test'
+
+import type { UUIDv4 } from '../../../src/types/index.js'
+
+import { UIWebSocketServer } from '../../../src/charging-station/ui-server/UIWebSocketServer.js'
+import { ProcedureName, ResponseStatus } from '../../../src/types/index.js'
+import { TEST_UUID } from './UIServerTestConstants.js'
+import {
+ createMockUIServerConfiguration,
+ MockUIServiceBroadcast,
+ MockUIServiceError,
+ MockUIServiceNonBroadcast,
+ MockWebSocket,
+} from './UIServerTestUtils.js'
+
+class TestableUIWebSocketServer extends UIWebSocketServer {
+ public addResponseHandler (uuid: UUIDv4, ws: MockWebSocket): void {
+ this.responseHandlers.set(uuid, ws as never)
+ }
+
+ public getResponseHandlersSize (): number {
+ return this.responseHandlers.size
+ }
+
+ public registerMockUIService (version: string, service: unknown): void {
+ this.uiServices.set(version as never, service as never)
+ }
+}
+
+await describe('UIWebSocketServer test suite', async () => {
+ await it('Verify sendResponse() deletes handler after sending', () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+ const ws = new MockWebSocket()
+
+ server.addResponseHandler(TEST_UUID, ws)
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(true)
+
+ server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
+
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(false)
+ expect(ws.sentMessages.length).toBe(1)
+ })
+
+ await it('Verify sendResponse() logs error when handler not found', () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+
+ server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
+
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(false)
+ })
+
+ await it('Verify sendResponse() deletes handler when WebSocket not open', () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+ const ws = new MockWebSocket()
+ ws.readyState = 0
+
+ server.addResponseHandler(TEST_UUID, ws)
+ server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
+
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(false)
+ expect(ws.sentMessages.length).toBe(0)
+ })
+
+ await it('Verify sendResponse() handles send errors gracefully', () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+ const ws = new MockWebSocket()
+ ws.readyState = 1
+ ws.send = (): void => {
+ throw new Error('WebSocket send error')
+ }
+
+ server.addResponseHandler(TEST_UUID, ws)
+ server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
+
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(false)
+ })
+
+ await it('Verify broadcast handler persistence (issue #1642)', async () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+ const mockService = new MockUIServiceBroadcast()
+ const ws = new MockWebSocket()
+
+ server.registerMockUIService('0.0.1', mockService)
+ server.addResponseHandler(TEST_UUID, ws)
+
+ await mockService.requestHandler().then((protocolResponse?: unknown) => {
+ if (protocolResponse != null) {
+ server.sendResponse(protocolResponse as never)
+ }
+ return undefined
+ })
+
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(true)
+
+ server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(false)
+ })
+
+ await it('Verify non-broadcast handler immediate deletion', async () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+ const mockService = new MockUIServiceNonBroadcast()
+ const ws = new MockWebSocket()
+
+ server.registerMockUIService('0.0.1', mockService)
+ server.addResponseHandler(TEST_UUID, ws)
+
+ const response = await mockService.requestHandler([
+ TEST_UUID,
+ ProcedureName.LIST_CHARGING_STATIONS,
+ {},
+ ])
+ server.sendResponse(response)
+
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(false)
+ })
+
+ await it('Verify error handler cleanup', async () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+ const mockService = new MockUIServiceError()
+ const ws = new MockWebSocket()
+
+ server.registerMockUIService('0.0.1', mockService)
+ server.addResponseHandler(TEST_UUID, ws)
+
+ try {
+ await mockService.requestHandler()
+ } catch {
+ // Expected error
+ }
+
+ expect(server.getResponseHandlersSize()).toBe(1)
+ })
+
+ await it('Verify response handlers cleanup', () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+ const ws1 = new MockWebSocket()
+ const ws2 = new MockWebSocket()
+
+ server.addResponseHandler('uuid-1' as UUIDv4, ws1)
+ server.addResponseHandler('uuid-2' as UUIDv4, ws2)
+
+ expect(server.getResponseHandlersSize()).toBe(2)
+
+ server.sendResponse(['uuid-1' as UUIDv4, { status: ResponseStatus.SUCCESS }])
+ expect(server.getResponseHandlersSize()).toBe(1)
+
+ server.sendResponse(['uuid-2' as UUIDv4, { status: ResponseStatus.SUCCESS }])
+ expect(server.getResponseHandlersSize()).toBe(0)
+ })
+
+ await it('Verify handlers cleared on server stop', () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+ const ws = new MockWebSocket()
+
+ server.addResponseHandler(TEST_UUID, ws)
+ expect(server.getResponseHandlersSize()).toBe(1)
+
+ server.stop()
+
+ expect(server.getResponseHandlersSize()).toBe(0)
+ })
+
+ await it('Verify valid WebSocket configuration', () => {
+ const config = createMockUIServerConfiguration()
+ const server = new UIWebSocketServer(config)
+
+ expect(server).toBeDefined()
+ })
+
+ await it('Verify WebSocket server with custom config', () => {
+ const config = createMockUIServerConfiguration({
+ options: {
+ host: 'localhost',
+ port: 9090,
+ },
+ })
+
+ const server = new UIWebSocketServer(config)
+ expect(server).toBeDefined()
+ })
+})
--- /dev/null
+// Copyright Jerome Benoit. 2024-2025. All Rights Reserved.
+
+import { expect } from '@std/expect'
+import { describe, it } from 'node:test'
+
+import { UIWebSocketServer } from '../../../../src/charging-station/ui-server/UIWebSocketServer.js'
+import { ProcedureName, ProtocolVersion, ResponseStatus } from '../../../../src/types/index.js'
+import { TEST_HASH_ID, TEST_UUID } from '../UIServerTestConstants.js'
+import {
+ createMockChargingStationData,
+ createMockUIServerConfiguration,
+ createProtocolRequest,
+} from '../UIServerTestUtils.js'
+
+class TestableUIWebSocketServer extends UIWebSocketServer {
+ public getUIService (version: ProtocolVersion) {
+ return this.uiServices.get(version)
+ }
+
+ public testRegisterProtocolVersionUIService (version: ProtocolVersion): void {
+ this.registerProtocolVersionUIService(version)
+ }
+}
+
+await describe('AbstractUIService test suite', async () => {
+ await it('Verify sendResponse checks for response handler existence', () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+
+ server.testRegisterProtocolVersionUIService(ProtocolVersion['0.0.1'])
+ const service = server.getUIService(ProtocolVersion['0.0.1'])
+
+ expect(service).toBeDefined()
+ if (service != null) {
+ service.sendResponse(TEST_UUID, { status: ResponseStatus.SUCCESS })
+ }
+
+ expect(server.hasResponseHandler(TEST_UUID)).toBe(false)
+ })
+
+ await it('Verify requestHandler returns response for LIST_CHARGING_STATIONS', async () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+
+ server.testRegisterProtocolVersionUIService(ProtocolVersion['0.0.1'])
+
+ server.setChargingStationData(TEST_HASH_ID, createMockChargingStationData(TEST_HASH_ID))
+
+ const service = server.getUIService(ProtocolVersion['0.0.1'])
+
+ const request = createProtocolRequest(TEST_UUID, ProcedureName.LIST_CHARGING_STATIONS, {})
+
+ expect(service).toBeDefined()
+ if (service != null) {
+ const response = await service.requestHandler(request)
+
+ expect(response).toBeDefined()
+ if (response != null) {
+ expect(response[0]).toBe(TEST_UUID)
+ expect(response[1].status).toBe(ResponseStatus.SUCCESS)
+ expect(response[1].chargingStations).toBeDefined()
+ expect(Array.isArray(response[1].chargingStations)).toBe(true)
+ }
+ }
+ })
+
+ await it('Verify requestHandler returns response for LIST_TEMPLATES', async () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+
+ server.testRegisterProtocolVersionUIService(ProtocolVersion['0.0.1'])
+
+ server.setChargingStationTemplates(['template1.json', 'template2.json'])
+
+ const service = server.getUIService(ProtocolVersion['0.0.1'])
+
+ const request = createProtocolRequest(TEST_UUID, ProcedureName.LIST_TEMPLATES, {})
+
+ expect(service).toBeDefined()
+ if (service != null) {
+ const response = await service.requestHandler(request)
+
+ expect(response).toBeDefined()
+ if (response != null) {
+ expect(response[0]).toBe(TEST_UUID)
+ expect(response[1].status).toBe(ResponseStatus.SUCCESS)
+ expect(response[1].templates).toBeDefined()
+ expect(Array.isArray(response[1].templates)).toBe(true)
+ expect(response[1].templates).toContain('template1.json')
+ expect(response[1].templates).toContain('template2.json')
+ }
+ }
+ })
+
+ await it('Verify requestHandler returns error response for unknown procedure', async () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+
+ server.testRegisterProtocolVersionUIService(ProtocolVersion['0.0.1'])
+
+ const service = server.getUIService(ProtocolVersion['0.0.1'])
+
+ const request = createProtocolRequest(TEST_UUID, 'UnknownProcedure' as ProcedureName, {})
+
+ expect(service).toBeDefined()
+ if (service != null) {
+ const response = await service.requestHandler(request)
+
+ expect(response).toBeDefined()
+ if (response != null) {
+ expect(response[0]).toBe(TEST_UUID)
+ expect(response[1].status).toBe(ResponseStatus.FAILURE)
+ expect(response[1].errorMessage).toBeDefined()
+ }
+ }
+ })
+
+ await it('Verify broadcast channel request tracking initialization', () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+
+ server.testRegisterProtocolVersionUIService(ProtocolVersion['0.0.1'])
+
+ const service = server.getUIService(ProtocolVersion['0.0.1'])
+
+ expect(service).toBeDefined()
+ if (service != null) {
+ expect(service.getBroadcastChannelExpectedResponses(TEST_UUID)).toBe(0)
+ }
+ })
+
+ await it('Verify broadcast channel cleanup on stop', () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+
+ server.testRegisterProtocolVersionUIService(ProtocolVersion['0.0.1'])
+
+ const service = server.getUIService(ProtocolVersion['0.0.1'])
+
+ expect(service).toBeDefined()
+ if (service != null) {
+ service.stop()
+ expect(service.getBroadcastChannelExpectedResponses(TEST_UUID)).toBe(0)
+ }
+ })
+
+ await it('Verify requestHandler handles errors gracefully', async () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+
+ server.testRegisterProtocolVersionUIService(ProtocolVersion['0.0.1'])
+
+ const service = server.getUIService(ProtocolVersion['0.0.1'])
+
+ const request = createProtocolRequest(TEST_UUID, ProcedureName.ADD_CHARGING_STATIONS, {})
+
+ expect(service).toBeDefined()
+ if (service != null) {
+ const response = await service.requestHandler(request)
+
+ expect(response).toBeDefined()
+ if (response != null) {
+ expect(response[0]).toBe(TEST_UUID)
+ expect(response[1].status).toBe(ResponseStatus.FAILURE)
+ }
+ }
+ })
+
+ await it('Verify UI service initialization', () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+
+ server.testRegisterProtocolVersionUIService(ProtocolVersion['0.0.1'])
+
+ const service = server.getUIService(ProtocolVersion['0.0.1'])
+
+ expect(service).toBeDefined()
+ })
+
+ await it('Verify multiple service registrations', () => {
+ const config = createMockUIServerConfiguration()
+ const server = new TestableUIWebSocketServer(config)
+
+ server.testRegisterProtocolVersionUIService(ProtocolVersion['0.0.1'])
+ server.testRegisterProtocolVersionUIService(ProtocolVersion['0.0.1'])
+
+ const uiServicesMap = Reflect.get(server, 'uiServices') as Map<unknown, unknown>
+
+ expect(uiServicesMap.size).toBe(1)
+ })
+})