import { AuthorizationStatus, type StartTransactionResponse } from '../../src/types/index.js'
import { createMockChargingStation, standardCleanup } from './ChargingStationTestUtils.js'
+type ConnectorStatus = ReturnType<AutomaticTransactionGenerator['connectorsStatus']['get']>
+
/**
- *
- * @param station
+ * Adds required ATG configuration methods to a mock station.
+ * @param station - The station to augment with ATG methods
*/
function addATGMethodsToStation (station: ChargingStation): void {
const stationExt = station as unknown as {
}
/**
- *
- * @param started
+ * Creates a mock station pre-configured for ATG tests.
+ * @param started - Whether the station should be in started state (default: true)
+ * @returns A mock ChargingStation with ATG methods configured
*/
function createStationForATG (started = true): ChargingStation {
const { station } = createMockChargingStation({ started })
}
/**
- *
- * @param station
+ * Retrieves connector status from the ATG, asserting it exists and narrowing the type.
+ * @param atg - The ATG instance to query
+ * @param connectorId - The connector ID to look up
+ * @returns The non-null connector status
+ */
+function getConnectorStatus (
+ atg: AutomaticTransactionGenerator,
+ connectorId: number
+): NonNullable<ConnectorStatus> {
+ const status = atg.connectorsStatus.get(connectorId)
+ expect(status).toBeDefined()
+ if (status == null) {
+ throw new BaseError(`Connector ${String(connectorId)} status unexpectedly undefined`)
+ }
+ return status
+}
+
+/**
+ * Gets the ATG instance for a station, asserting it exists and narrowing the type.
+ * @param station - The station to get the ATG instance for
+ * @returns The non-null ATG instance
*/
function getDefinedATG (station: ChargingStation): AutomaticTransactionGenerator {
const atg = AutomaticTransactionGenerator.getInstance(station)
}
/**
- *
- * @param atg
+ * Extracts the private handleStartTransactionResponse method from an ATG instance.
+ * @param atg - The ATG instance to extract the method from
+ * @returns The bound handleStartTransactionResponse method
+ */
+function getHandleStartTransactionResponse (
+ atg: AutomaticTransactionGenerator
+): (connectorId: number, response: StartTransactionResponse) => void {
+ return (
+ atg as unknown as {
+ handleStartTransactionResponse: (
+ connectorId: number,
+ response: StartTransactionResponse
+ ) => void
+ }
+ ).handleStartTransactionResponse.bind(atg)
+}
+
+/**
+ * Replaces the async internalStartConnector with a no-op to avoid real timer usage.
+ * @param atg - The ATG instance to mock
*/
function mockInternalStartConnector (atg: AutomaticTransactionGenerator): void {
const atgPrivate = atg as unknown as {
}
/**
- *
+ * Clears all ATG singleton instances to prevent test pollution.
*/
function resetATGInstances (): void {
const atgClass = AutomaticTransactionGenerator as unknown as {
await it('should stop a running connector', () => {
const station = createStationForATG()
const atg = getDefinedATG(station)
-
- const connectorStatus = atg.connectorsStatus.get(1)
- expect(connectorStatus).toBeDefined()
- if (connectorStatus == null) {
- throw new BaseError('Connector status unexpectedly undefined')
- }
+ const connectorStatus = getConnectorStatus(atg, 1)
connectorStatus.start = true
atg.stopConnector(1)
await it('should increment accepted counters on accepted start response', () => {
const station = createStationForATG()
const atg = getDefinedATG(station)
-
- const connectorStatus = atg.connectorsStatus.get(1)
- expect(connectorStatus).toBeDefined()
- if (connectorStatus == null) {
- throw new BaseError('Connector status unexpectedly undefined')
- }
- const handleResponse = (
- atg as unknown as {
- handleStartTransactionResponse: (
- connectorId: number,
- response: StartTransactionResponse
- ) => void
- }
- ).handleStartTransactionResponse.bind(atg)
+ const connectorStatus = getConnectorStatus(atg, 1)
+ const handleResponse = getHandleStartTransactionResponse(atg)
handleResponse(1, {
idTagInfo: { status: AuthorizationStatus.ACCEPTED },
await it('should increment rejected counters on rejected start response', () => {
const station = createStationForATG()
const atg = getDefinedATG(station)
-
- const connectorStatus = atg.connectorsStatus.get(1)
- expect(connectorStatus).toBeDefined()
- if (connectorStatus == null) {
- throw new BaseError('Connector status unexpectedly undefined')
- }
- const handleResponse = (
- atg as unknown as {
- handleStartTransactionResponse: (
- connectorId: number,
- response: StartTransactionResponse
- ) => void
- }
- ).handleStartTransactionResponse.bind(atg)
+ const connectorStatus = getConnectorStatus(atg, 1)
+ const handleResponse = getHandleStartTransactionResponse(atg)
handleResponse(1, {
idTagInfo: { status: AuthorizationStatus.INVALID },
}
/**
- *
- * @param cache
- * @param file
- * @param idTags
+ * Injects id tags directly into the cache's internal Map, bypassing file I/O.
+ * @param cache - The IdTagsCache instance
+ * @param file - The file path key to use
+ * @param idTags - Array of id tags to cache
*/
function populateCache (cache: IdTagsCache, file: string, idTags: string[]): void {
const internal = cache as unknown as IdTagsCacheInternal
}
/**
- *
+ * Resets the IdTagsCache singleton so subsequent getInstance() creates a fresh cache.
*/
function resetIdTagsCache (): void {
;(IdTagsCache as unknown as { instance: null }).instance = null
}
/**
- *
- * @param station
+ * Resolves the idTags file path for a mock station, throwing if unresolvable.
+ * @param station - The station whose stationInfo is used
+ * @returns The resolved file path string
*/
function resolveIdTagsFile (station: ChargingStation): string {
const stationInfo = station.stationInfo
resetIdTagsCache()
const instance2 = IdTagsCache.getInstance()
- expect(instance1 === instance2).toBe(false)
+ expect(instance1).not.toBe(instance2)
})
})
}
/**
- *
- * @param hash
+ * Creates a cacheable ChargingStationConfiguration fixture with the given hash.
+ * @param hash - The configurationHash to assign
+ * @returns A ChargingStationConfiguration fixture
*/
function createCacheableConfiguration (hash: string): ChargingStationConfiguration {
return {
}
/**
- *
- * @param hash
+ * Creates a ChargingStationTemplate fixture with the given hash.
+ * @param hash - The templateHash to assign
+ * @returns A ChargingStationTemplate fixture
*/
function createTemplate (hash: string): ChargingStationTemplate {
return {
} as ChargingStationTemplate
}
-// Inject a mock Bootstrap singleton so that SharedLRUCache constructor reads numeric getters
-// instead of triggering a real Bootstrap construction.
/**
- *
+ * Injects a mock Bootstrap singleton so SharedLRUCache reads numeric getters
+ * instead of triggering real Bootstrap construction.
*/
function installMockBootstrap (): void {
;(Bootstrap as unknown as BootstrapStatic).instance = {
}
/**
- *
+ * Resets the SharedLRUCache singleton so subsequent getInstance() creates a fresh cache.
*/
function resetSharedLRUCache (): void {
;(SharedLRUCache as unknown as { instance: null }).instance = null
await describe('template operations', async () => {
await it('should store and retrieve a charging station template', () => {
const cache = SharedLRUCache.getInstance()
- const template = createTemplate('tmpl-hash-1')
+ const template = createTemplate('template-hash-1')
cache.setChargingStationTemplate(template)
- const retrieved = cache.getChargingStationTemplate('tmpl-hash-1')
+ const retrieved = cache.getChargingStationTemplate('template-hash-1')
expect(retrieved).toStrictEqual(template)
})
await it('should report has correctly for templates', () => {
const cache = SharedLRUCache.getInstance()
- const template = createTemplate('tmpl-hash-2')
+ const template = createTemplate('template-hash-2')
cache.setChargingStationTemplate(template)
- expect(cache.hasChargingStationTemplate('tmpl-hash-2')).toBe(true)
+ expect(cache.hasChargingStationTemplate('template-hash-2')).toBe(true)
expect(cache.hasChargingStationTemplate('unknown-hash')).toBe(false)
})
await it('should delete a charging station template', () => {
const cache = SharedLRUCache.getInstance()
- const template = createTemplate('tmpl-hash-3')
+ const template = createTemplate('template-hash-3')
cache.setChargingStationTemplate(template)
- cache.deleteChargingStationTemplate('tmpl-hash-3')
+ cache.deleteChargingStationTemplate('template-hash-3')
- expect(cache.hasChargingStationTemplate('tmpl-hash-3')).toBe(false)
+ expect(cache.hasChargingStationTemplate('template-hash-3')).toBe(false)
})
})
await describe('clear', async () => {
await it('should clear all cached entries', () => {
const cache = SharedLRUCache.getInstance()
- const template = createTemplate('tmpl-clear')
+ const template = createTemplate('template-clear')
const config = createCacheableConfiguration('config-clear')
cache.setChargingStationTemplate(template)
cache.setChargingStationConfiguration(config)
cache.clear()
- expect(cache.hasChargingStationTemplate('tmpl-clear')).toBe(false)
+ expect(cache.hasChargingStationTemplate('template-clear')).toBe(false)
expect(cache.hasChargingStationConfiguration('config-clear')).toBe(false)
})
})
* @description Verifies Heartbeat, NotifyReport, and StatusNotification response handling
*/
+import { expect } from '@std/expect'
import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import type { MockChargingStation } from '../../ChargingStationTestUtils.js'
await describe('G02 - HeartbeatResponse handler', async () => {
await it('should handle Heartbeat response without throwing', async () => {
const payload: OCPP20HeartbeatResponse = { currentTime: new Date() }
- await responseService.responseHandler(
- mockStation,
- OCPP20RequestCommand.HEARTBEAT,
- payload as unknown as Parameters<typeof responseService.responseHandler>[2],
- {} as Parameters<typeof responseService.responseHandler>[3]
- )
+ await expect(
+ responseService.responseHandler(
+ mockStation,
+ OCPP20RequestCommand.HEARTBEAT,
+ payload as unknown as Parameters<typeof responseService.responseHandler>[2],
+ {} as Parameters<typeof responseService.responseHandler>[3]
+ )
+ ).resolves.toBeUndefined()
})
})
await describe('B07 - NotifyReportResponse handler', async () => {
await it('should handle NotifyReport response without throwing', async () => {
const payload: OCPP20NotifyReportResponse = {}
- await responseService.responseHandler(
- mockStation,
- OCPP20RequestCommand.NOTIFY_REPORT,
- payload as unknown as Parameters<typeof responseService.responseHandler>[2],
- {} as Parameters<typeof responseService.responseHandler>[3]
- )
+ await expect(
+ responseService.responseHandler(
+ mockStation,
+ OCPP20RequestCommand.NOTIFY_REPORT,
+ payload as unknown as Parameters<typeof responseService.responseHandler>[2],
+ {} as Parameters<typeof responseService.responseHandler>[3]
+ )
+ ).resolves.toBeUndefined()
})
})
await describe('G01 - StatusNotificationResponse handler', async () => {
await it('should handle StatusNotification response without throwing', async () => {
const payload: OCPP20StatusNotificationResponse = {}
- await responseService.responseHandler(
- mockStation,
- OCPP20RequestCommand.STATUS_NOTIFICATION,
- payload as unknown as Parameters<typeof responseService.responseHandler>[2],
- {} as Parameters<typeof responseService.responseHandler>[3]
- )
+ await expect(
+ responseService.responseHandler(
+ mockStation,
+ OCPP20RequestCommand.STATUS_NOTIFICATION,
+ payload as unknown as Parameters<typeof responseService.responseHandler>[2],
+ {} as Parameters<typeof responseService.responseHandler>[3]
+ )
+ ).resolves.toBeUndefined()
})
})
})
}
/**
- *
- * @param station
- * @param mocks
- * @param tags
+ * Configures local authorization on a mock station with the given id tags.
+ * @param station - The mock station to configure
+ * @param mocks - The mock infrastructure (for idTagsCache injection)
+ * @param tags - Array of id tags to register in the local auth list
*/
function setupLocalAuth (
station: ReturnType<typeof createMockChargingStation>['station'],
import { expect } from '@std/expect'
import { afterEach, describe, it, mock } from 'node:test'
+import type { ChargingStation } from '../../../src/charging-station/ChargingStation.js'
import type { Reservation } from '../../../src/types/index.js'
+import type { MockChargingStationOptions } from '../helpers/StationHelpers.js'
import {
restoreConnectorStatus,
import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
import { createMockChargingStation } from '../ChargingStationTestUtils.js'
+/**
+ * Creates a mock station with a spied requestHandler for verifying OCPP requests.
+ * @param opts - Additional mock station options to merge
+ * @returns The station and its requestHandler spy
+ */
+function createStationWithRequestHandler (opts?: Partial<MockChargingStationOptions>): {
+ requestHandler: ReturnType<typeof mock.fn>
+ station: ChargingStation
+} {
+ const requestHandler = mock.fn(() => Promise.resolve({}))
+ const { station } = createMockChargingStation({
+ ocppRequestService: { requestHandler },
+ ...opts,
+ })
+ return { requestHandler, station }
+}
+
await describe('OCPPServiceUtils — connector status management', async () => {
afterEach(() => {
standardCleanup()
await describe('sendAndSetConnectorStatus', async () => {
await it('should send StatusNotification and update connector status', async () => {
- const requestHandler = mock.fn(() => Promise.resolve({}))
- const { station } = createMockChargingStation({
- ocppRequestService: { requestHandler },
- })
+ const { requestHandler, station } = createStationWithRequestHandler()
await sendAndSetConnectorStatus(station, 1, ConnectorStatusEnum.Occupied)
})
await it('should return early when connector does not exist', async () => {
- const requestHandler = mock.fn(() => Promise.resolve({}))
- const { station } = createMockChargingStation({
- ocppRequestService: { requestHandler },
- })
+ const { requestHandler, station } = createStationWithRequestHandler()
await sendAndSetConnectorStatus(station, 99, ConnectorStatusEnum.Occupied)
})
await it('should skip sending when options.send is false', async () => {
- const requestHandler = mock.fn(() => Promise.resolve({}))
- const { station } = createMockChargingStation({
- ocppRequestService: { requestHandler },
- })
+ const { requestHandler, station } = createStationWithRequestHandler()
await sendAndSetConnectorStatus(station, 1, ConnectorStatusEnum.Occupied, undefined, {
send: false,
})
await it('should update connector status even when send is true', async () => {
- const requestHandler = mock.fn(() => Promise.resolve({}))
- const { station } = createMockChargingStation({
- ocppRequestService: { requestHandler },
- })
+ const { station } = createStationWithRequestHandler()
expect(station.getConnectorStatus(1)?.status).toBe(ConnectorStatusEnum.Available)
})
await it('should call emitChargingStationEvent with connectorStatusChanged', async () => {
- const requestHandler = mock.fn(() => Promise.resolve({}))
- const emitMock = mock.fn()
- const { station } = createMockChargingStation({
- ocppRequestService: { requestHandler },
- })
-
- const stationWithEmit = station as unknown as {
- emitChargingStationEvent: (...args: unknown[]) => void
- }
- stationWithEmit.emitChargingStationEvent = emitMock
+ const { station } = createStationWithRequestHandler()
+ const stationObj = station as unknown as { emitChargingStationEvent: () => void }
+ const emitSpy = mock.method(stationObj, 'emitChargingStationEvent')
await sendAndSetConnectorStatus(station, 1, ConnectorStatusEnum.Occupied)
- expect(emitMock.mock.calls.length).toBe(1)
+ expect(emitSpy.mock.calls.length).toBe(1)
})
await it('should pass evseId to buildStatusNotificationRequest for OCPP 2.0', async () => {
- const requestHandler = mock.fn(() => Promise.resolve({}))
- const { station } = createMockChargingStation({
- ocppRequestService: { requestHandler },
+ const { requestHandler, station } = createStationWithRequestHandler({
ocppVersion: OCPPVersion.VERSION_20,
})
})
await it('should default options.send to true when options not provided', async () => {
- const requestHandler = mock.fn(() => Promise.resolve({}))
- const { station } = createMockChargingStation({
- ocppRequestService: { requestHandler },
- })
+ const { requestHandler, station } = createStationWithRequestHandler()
await sendAndSetConnectorStatus(station, 1, ConnectorStatusEnum.Occupied)
await describe('restoreConnectorStatus', async () => {
await it('should restore to Reserved when connector has reservation and is not Reserved', async () => {
- const requestHandler = mock.fn(() => Promise.resolve({}))
- const { station } = createMockChargingStation({
- ocppRequestService: { requestHandler },
- })
+ const { station } = createStationWithRequestHandler()
const connector = station.getConnectorStatus(1)
if (connector != null) {
})
await it('should restore to Available when connector has no reservation and is not Available', async () => {
- const requestHandler = mock.fn(() => Promise.resolve({}))
- const { station } = createMockChargingStation({
- ocppRequestService: { requestHandler },
- })
+ const { station } = createStationWithRequestHandler()
const connector = station.getConnectorStatus(1)
if (connector != null) {
})
await it('should not change status when connector is already Available with no reservation', async () => {
- const requestHandler = mock.fn(() => Promise.resolve({}))
- const { station } = createMockChargingStation({
- ocppRequestService: { requestHandler },
- })
+ const { requestHandler, station } = createStationWithRequestHandler()
const connector = station.getConnectorStatus(1)
if (connector != null) {
ajvErrorsToErrorType,
convertDateToISOString,
getMessageTypeString,
+ OCPPServiceUtils,
} from '../../../src/charging-station/ocpp/OCPPServiceUtils.js'
-import { OCPPServiceUtils } from '../../../src/charging-station/ocpp/OCPPServiceUtils.js'
import {
ErrorType,
IncomingRequestCommand,
import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
/**
- *
- * @param keyword
+ * Creates a minimal AJV error fixture for testing keyword-based error mapping.
+ * @param keyword - The AJV validation keyword (e.g. 'type', 'required', 'format')
+ * @returns An ErrorObject fixture with the given keyword
*/
function makeAjvError (keyword: string): ErrorObject {
return {
}
/**
- *
+ * Creates a minimal ChargingStation stub with a logPrefix method.
+ * @returns A mock ChargingStation
*/
-function makeStationStub (): ChargingStation {
+function makeStationMock (): ChargingStation {
return { logPrefix: () => '[test-station]' } as unknown as ChargingStation
}
await describe('OCPPServiceUtils.isConnectorIdValid', async () => {
await it('should return true for connector ID greater than zero', () => {
const result = OCPPServiceUtils.isConnectorIdValid(
- makeStationStub(),
+ makeStationMock(),
IncomingRequestCommand.REMOTE_START_TRANSACTION,
1
)
await it('should return true for connector ID zero', () => {
const result = OCPPServiceUtils.isConnectorIdValid(
- makeStationStub(),
+ makeStationMock(),
IncomingRequestCommand.REMOTE_START_TRANSACTION,
0
)
await it('should return false for negative connector ID', () => {
const result = OCPPServiceUtils.isConnectorIdValid(
- makeStationStub(),
+ makeStationMock(),
IncomingRequestCommand.REMOTE_START_TRANSACTION,
-1
)
/**
* Creates a minimal ChargingStation mock with the given stationInfo.
* @param stationInfo - partial stationInfo to set on the mock
+ * @returns A mock ChargingStation
*/
function makeStationMock (stationInfo?: Record<string, unknown>): ChargingStation {
return {
expect(result).toBe(true)
})
- await it('should return true when messageTriggerSupport is empty', () => {
+ await it('should return true when messageTriggerSupport is null', () => {
const station = makeStationMock({
messageTriggerSupport: null,
})
/**
* Get a reference to the private configurationData for injection.
+ * @returns The internal configurationData holder
*/
function getConfigurationInternals (): {
configurationData: ConfigurationData | undefined
})
await it('should default to ROUND_ROBIN when not configured', () => {
- // Arrange
const internals = getConfigurationInternals()
const originalData = internals.configurationData
internals.configurationData = {
stationTemplateUrls: [],
} as ConfigurationData
- // Act
- const distribution = Configuration.getSupervisionUrlDistribution()
-
- // Assert
- expect(distribution).toBe(SupervisionUrlDistribution.ROUND_ROBIN)
-
- internals.configurationData = originalData
- resetSectionCache()
+ try {
+ const distribution = Configuration.getSupervisionUrlDistribution()
+ expect(distribution).toBe(SupervisionUrlDistribution.ROUND_ROBIN)
+ } finally {
+ internals.configurationData = originalData
+ resetSectionCache()
+ }
})
await it('should return false for workerPoolInUse with default workerSet config', () => {