- **Test behavior, not implementation**: Focus on what code does, not how
- **Isolation is mandatory**: Each test runs independently with fresh state
- **Determinism required**: Tests must produce identical results on every run
-- **Strict assertions**: Use `assert.strictEqual`, `assert.deepStrictEqual`, `assert.ok` — never loose equality
+- **Strict assertions**: Use `assert.strictEqual`, `assert.deepStrictEqual` — never loose equality. Use `assert.ok` only for boolean/existence checks
- **Coverage target**: 80%+ on new code
---
it('should start successfully with valid configuration', async () => {})
it('should reject invalid identifier', () => {})
+// ✅ Good — with spec traceability prefix (for FR-referenced tests)
+it('G03.FR.01.T5.01 - should evict non-valid entry before valid one', () => {})
+it('G04.INT.01: should wire auth cache into local strategy', async () => {})
+
// ❌ Bad
it('Should start successfully', () => {}) // Capital 'S'
it('Verify generateUUID()', () => {}) // Imperative
The test command uses `--test-force-exit` flag to prevent Windows CI hangs:
```json
-"test": "node --import tsx --test --test-force-exit tests/**/*.test.ts"
+"test": "node --import tsx --test --test-force-exit 'tests/**/*.test.ts'"
```
**Why**: Windows Named Pipes for stdout/stderr remain "ref'd" (keep event loop alive) while Unix file descriptors are auto-unref'd. Without `--test-force-exit`, the Node.js process hangs indefinitely after tests complete on Windows.
### Choose the Right Factory
-| Factory | Use Case | Location |
-| ---------------------------------------- | -------------------------------- | ------------------------------------ |
-| `createMockChargingStation()` | Full OCPP protocol testing | `ChargingStationTestUtils.ts` |
-| `createMockAuthServiceTestStation()` | Auth service tests (lightweight) | `ocpp/auth/helpers/MockFactories.ts` |
-| `createMockStationWithRequestTracking()` | Verify sent OCPP requests | `ocpp/2.0/OCPP20TestUtils.ts` |
+| Factory | Use Case | Location |
+| ------------------------------------------ | ------------------------------- | ------------------------------------ |
+| `createMockChargingStation()` | Full OCPP protocol testing | `helpers/StationHelpers.ts` |
+| `createStandardStation()` | Pre-configured OCPP 1.6 station | `ocpp/1.6/OCPP16TestUtils.ts` |
+| `createOCPP16IncomingRequestTestContext()` | OCPP 1.6 handler test context | `ocpp/1.6/OCPP16TestUtils.ts` |
+| `createOCPP16ListenerStation()` | OCPP 1.6 event listener tests | `ocpp/1.6/OCPP16TestUtils.ts` |
+| `createOCPP20ListenerStation()` | OCPP 2.0 event listener tests | `ocpp/2.0/OCPP20TestUtils.ts` |
+| `createOCPP20RequestTestContext()` | OCPP 2.0 request test context | `ocpp/2.0/OCPP20TestUtils.ts` |
+| `createMockStationWithRequestTracking()` | Verify sent OCPP requests | `ocpp/2.0/OCPP20TestUtils.ts` |
+| `createStationWithCertificateManager()` | Certificate operation tests | `ocpp/2.0/OCPP20TestUtils.ts` |
+| `createMockCertificateManager()` | Certificate manager mock | `ocpp/2.0/OCPP20TestUtils.ts` |
+| `createMockAuthService()` | Auth service mock | `ocpp/auth/helpers/MockFactories.ts` |
+| `createMockAuthServiceTestStation()` | Auth service integration tests | `ocpp/auth/helpers/MockFactories.ts` |
+| `createMockUIWebSocket()` | UI server WebSocket mock | `ui-server/UIServerTestUtils.ts` |
### Usage
})
// Verify sent messages
-assert.ok(mocks.webSocket.sentMessages.includes(expectedMessage))
+assert.strictEqual(mocks.webSocket.sentMessages.length, 1)
```
---
| Utility | Purpose |
| --------------------------------- | ---------------------------------------- |
| `standardCleanup()` | **MANDATORY** afterEach cleanup |
-| `sleep(ms)` | Real-time delay |
+| `flushMicrotasks()` | Drain async side-effects from `emit()` |
| `withMockTimers()` | Execute test with timer mocking |
| `createTimerScope()` | Manual timer control |
+| `sleep(ms)` | Real-time delay (avoid in tests) |
| `createLoggerMocks()` | Create logger spies (error, warn) |
| `createConsoleMocks()` | Create console spies (error, warn, info) |
| `setupConnectorWithTransaction()` | Setup connector in transaction state |
| Utility | Purpose |
| -------------------------------------- | ------------------------------- |
| `createTestableOCPP20RequestService()` | Type-safe private method access |
-| `createMockCertificateManager()` | Certificate operations mock |
| `IdTokenFixtures` | Pre-built IdToken fixtures |
| `TransactionContextFixtures` | Transaction context fixtures |
| `createMockIdentifier()` | UnifiedIdentifier factory |
| `createMockAuthRequest()` | AuthRequest factory |
| `createMockAuthorizationResult()` | AuthorizationResult factory |
-| `expectAcceptedAuthorization()` | Assert accepted result |
+
+---
+
+## 11. Event Listener Testing
+
+Commands that use the post-response event listener pattern (handler validates → returns response → event triggers async action) require dedicated listener tests.
+
+### Structure
+
+```typescript
+await describe('COMMAND_NAME event listener', async () => {
+ let listenerService: OCPP16IncomingRequestService // or OCPP20
+ let requestHandlerMock: ReturnType<typeof mock.fn>
+ let station: ChargingStation
+
+ beforeEach(() => {
+ ;({ requestHandlerMock, station } = createOCPP16ListenerStation('test-listener'))
+ listenerService = new OCPP16IncomingRequestService()
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ // 1. Registration test (always first)
+ await it('should register COMMAND_NAME event listener in constructor', () => {
+ assert.strictEqual(
+ listenerService.listenerCount(OCPP16IncomingRequestCommand.COMMAND_NAME),
+ 1
+ )
+ })
+
+ // 2. Accepted → fires action
+ await it('should call X when response is Accepted', async () => {
+ listenerService.emit(OCPP16IncomingRequestCommand.COMMAND_NAME, station, request, response)
+ await flushMicrotasks()
+ assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+ })
+
+ // 3. Rejected → does NOT fire
+ await it('should NOT call X when response is Rejected', () => {
+ listenerService.emit(...)
+ assert.strictEqual(requestHandlerMock.mock.callCount(), 0)
+ })
+
+ // 4. Error → handled gracefully
+ await it('should handle X failure gracefully', async () => {
+ // Override mock to reject (mock.method for lifecycle, new factory for requestHandler)
+ mock.method(listenerService as unknown as Record<string, unknown>, 'privateMethod',
+ () => Promise.reject(new Error('test'))
+ )
+ listenerService.emit(...)
+ await flushMicrotasks()
+ // No crash = pass
+ })
+})
+```
+
+### Rules
+
+- Use `emit()` directly on the service instance — no wrapper helpers
+- Use `flushMicrotasks()` to drain async side-effects — never `await Promise.resolve()`
+- Use `createOCPP16ListenerStation()` or `createOCPP20ListenerStation()` for `requestHandler` mock
+- Use `mock.method()` in `beforeEach` for private lifecycle methods; override in rejection tests
+- Use `listenerCount` as the first test in every listener describe block
+- Listener tests go inside the same top-level describe as handler tests
---
8. **Types**: No `as any`, use testable interfaces
9. **Mocks**: Use appropriate factory for your use case
10. **Utils**: Leverage lifecycle helpers and mock classes
+11. **Listeners**: `emit()` direct, `flushMicrotasks()`, `listenerCount` first, accepted/rejected/error triad
import { AutomaticTransactionGenerator } from '../../src/charging-station/AutomaticTransactionGenerator.js'
import { BaseError } from '../../src/exception/index.js'
import { AuthorizationStatus, type StartTransactionResponse } from '../../src/types/index.js'
+import { flushMicrotasks } from '../helpers/TestLifecycleHelpers.js'
import { createMockChargingStation, standardCleanup } from './ChargingStationTestUtils.js'
type ConnectorStatus = ReturnType<AutomaticTransactionGenerator['connectorsStatus']['get']>
internalStartConnector: (...args: unknown[]) => Promise<void>
}
atgPrivate.internalStartConnector = async () => {
- await Promise.resolve()
+ await flushMicrotasks()
}
}
ResponseStatus,
} from '../../../src/types/index.js'
import { Constants } from '../../../src/utils/index.js'
-import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
+import { flushMicrotasks, standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
import { createMockChargingStation } from '../ChargingStationTestUtils.js'
import {
createMockStationWithRequestTracking,
],
})
- await new Promise(resolve => {
- setTimeout(resolve, 50)
- })
+ await flushMicrotasks()
assert.strictEqual(sentRequests.length, 1)
assert.strictEqual(sentRequests[0].command, RequestCommand.GET_15118_EV_CERTIFICATE)
],
})
- await new Promise(resolve => {
- setTimeout(resolve, 50)
- })
+ await flushMicrotasks()
assert.strictEqual(sentRequests.length, 1)
assert.strictEqual(sentRequests[0].command, RequestCommand.LOG_STATUS_NOTIFICATION)
],
})
- await new Promise(resolve => {
- setTimeout(resolve, 50)
- })
+ await flushMicrotasks()
assert.strictEqual(sentRequests.length, 1)
assert.strictEqual(sentRequests[0].command, RequestCommand.NOTIFY_CUSTOMER_INFORMATION)
],
})
- await new Promise(resolve => {
- setTimeout(resolve, 50)
- })
+ await flushMicrotasks()
assert.strictEqual(sentRequests.length, 1)
assert.strictEqual(sentRequests[0].command, RequestCommand.NOTIFY_REPORT)
],
})
- await new Promise(resolve => {
- setTimeout(resolve, 50)
- })
+ await flushMicrotasks()
assert.strictEqual(sentRequests.length, 1)
assert.strictEqual(sentRequests[0].command, RequestCommand.SECURITY_EVENT_NOTIFICATION)
],
})
- await new Promise(resolve => {
- setTimeout(resolve, 50)
- })
+ await flushMicrotasks()
assert.strictEqual(sentRequests.length, 1)
assert.strictEqual(sentRequests[0].command, RequestCommand.METER_VALUES)
*/
import assert from 'node:assert/strict'
-import { afterEach, beforeEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import type { GetDiagnosticsRequest } from '../../../../src/types/index.js'
-import { OCPP16StandardParametersKey } from '../../../../src/types/index.js'
+import { OCPP16IncomingRequestService } from '../../../../src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.js'
+import {
+ OCPP16IncomingRequestCommand,
+ OCPP16StandardParametersKey,
+ type OCPP16UpdateFirmwareRequest,
+ type OCPP16UpdateFirmwareResponse,
+} from '../../../../src/types/index.js'
import { OCPP16FirmwareStatus } from '../../../../src/types/ocpp/1.6/Requests.js'
-import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import {
+ flushMicrotasks,
+ standardCleanup,
+ withMockTimers,
+} from '../../../helpers/TestLifecycleHelpers.js'
import {
createOCPP16IncomingRequestTestContext,
+ createOCPP16ListenerStation,
type OCPP16IncomingRequestTestContext,
upsertConfigurationKey,
} from './OCPP16TestUtils.js'
assert.strictEqual(Object.keys(response).length, 0)
})
})
+
+ // §6.4: UpdateFirmware event listener
+ await describe('UPDATE_FIRMWARE event listener', async () => {
+ let listenerService: OCPP16IncomingRequestService
+ let updateFirmwareMock: ReturnType<typeof mock.fn>
+
+ beforeEach(() => {
+ listenerService = new OCPP16IncomingRequestService()
+ updateFirmwareMock = mock.method(
+ listenerService as unknown as {
+ updateFirmwareSimulation: (chargingStation: unknown) => Promise<void>
+ },
+ 'updateFirmwareSimulation',
+ mock.fn(() => Promise.resolve())
+ )
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should register UPDATE_FIRMWARE event listener in constructor', () => {
+ assert.strictEqual(
+ listenerService.listenerCount(OCPP16IncomingRequestCommand.UPDATE_FIRMWARE),
+ 1
+ )
+ })
+
+ await it('should call updateFirmwareSimulation when retrieveDate is in the past', async () => {
+ // Arrange
+ const { station } = createOCPP16ListenerStation('listener-station-past')
+
+ const request: OCPP16UpdateFirmwareRequest = {
+ location: 'ftp://localhost/firmware.bin',
+ retrieveDate: new Date(Date.now() - 10000),
+ }
+ const response: OCPP16UpdateFirmwareResponse = {}
+
+ // Act
+ listenerService.emit(OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
+ await flushMicrotasks()
+
+ // Assert
+ assert.strictEqual(updateFirmwareMock.mock.callCount(), 1)
+ })
+
+ await it('should schedule deferred updateFirmwareSimulation when retrieveDate is in the future', async t => {
+ // Arrange
+ const { station } = createOCPP16ListenerStation('listener-station-future')
+
+ const futureMs = 5000
+ const request: OCPP16UpdateFirmwareRequest = {
+ location: 'ftp://localhost/firmware.bin',
+ retrieveDate: new Date(Date.now() + futureMs),
+ }
+ const response: OCPP16UpdateFirmwareResponse = {}
+
+ // Act & Assert
+ await withMockTimers(t, ['setTimeout'], async () => {
+ listenerService.emit(
+ OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
+ station,
+ request,
+ response
+ )
+
+ // Before tick: simulation not yet called
+ assert.strictEqual(updateFirmwareMock.mock.callCount(), 0)
+
+ // Advance timers past the retrieve date delay
+ t.mock.timers.tick(futureMs + 1)
+ await flushMicrotasks()
+
+ // After tick: simulation should have been called
+ assert.strictEqual(updateFirmwareMock.mock.callCount(), 1)
+ })
+ })
+
+ await it('should handle updateFirmwareSimulation failure gracefully', async () => {
+ // Arrange
+ const { station } = createOCPP16ListenerStation('listener-station-error')
+ mock.method(
+ listenerService as unknown as {
+ updateFirmwareSimulation: (chargingStation: unknown) => Promise<void>
+ },
+ 'updateFirmwareSimulation',
+ mock.fn(() => Promise.reject(new Error('firmware simulation error')))
+ )
+
+ const request: OCPP16UpdateFirmwareRequest = {
+ location: 'ftp://localhost/firmware.bin',
+ retrieveDate: new Date(Date.now() - 1000),
+ }
+ const response: OCPP16UpdateFirmwareResponse = {}
+
+ // Act: emit should not throw even if simulation rejects
+ listenerService.emit(OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
+
+ // Allow the rejected promise to be handled by the error handler in the listener
+ await flushMicrotasks()
+ await flushMicrotasks()
+
+ // Assert: test passes if no unhandled rejection was thrown
+ })
+ })
})
*/
import assert from 'node:assert/strict'
-import { afterEach, beforeEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import type { RemoteStartTransactionRequest } from '../../../../src/types/index.js'
-import { AvailabilityType, GenericStatus } from '../../../../src/types/index.js'
-import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { OCPP16IncomingRequestService } from '../../../../src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.js'
+import {
+ AvailabilityType,
+ GenericStatus,
+ OCPP16IncomingRequestCommand,
+ OCPP16RequestCommand,
+} from '../../../../src/types/index.js'
+import { flushMicrotasks, standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
import {
createOCPP16IncomingRequestTestContext,
+ createOCPP16ListenerStation,
type OCPP16IncomingRequestTestContext,
} from './OCPP16TestUtils.js'
// Assert
assert.strictEqual(response.status, GenericStatus.Rejected)
})
+
+ await describe('REMOTE_START_TRANSACTION event listener', async () => {
+ let incomingRequestService: OCPP16IncomingRequestService
+ let requestHandlerMock: ReturnType<typeof mock.fn>
+ let listenerStation: import('../../../../src/charging-station/ChargingStation.js').ChargingStation
+
+ beforeEach(() => {
+ ;({ requestHandlerMock, station: listenerStation } = createOCPP16ListenerStation(
+ 'test-remote-start-listener'
+ ))
+ incomingRequestService = new OCPP16IncomingRequestService()
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should register REMOTE_START_TRANSACTION event listener in constructor', () => {
+ assert.strictEqual(
+ incomingRequestService.listenerCount(OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION),
+ 1
+ )
+ })
+
+ await it('should call StartTransaction when response is Accepted', async () => {
+ // Arrange
+ const connectorStatus = listenerStation.getConnectorStatus(1)
+ assert.notStrictEqual(connectorStatus, undefined)
+
+ const request: RemoteStartTransactionRequest = {
+ connectorId: 1,
+ idTag: 'TEST-TAG-001',
+ }
+ const response = { status: GenericStatus.Accepted }
+
+ // Act
+ incomingRequestService.emit(
+ OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
+ listenerStation,
+ request,
+ response
+ )
+
+ // Flush microtask queue so the async requestHandler call executes
+ await flushMicrotasks()
+
+ // Assert
+ assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+ const args = requestHandlerMock.mock.calls[0].arguments as [unknown, string, unknown]
+ assert.strictEqual(args[1], OCPP16RequestCommand.START_TRANSACTION)
+ })
+
+ await it('should NOT call StartTransaction when response is Rejected', () => {
+ // Arrange
+ const request: RemoteStartTransactionRequest = {
+ connectorId: 1,
+ idTag: 'TEST-TAG-001',
+ }
+ const response = { status: GenericStatus.Rejected }
+
+ // Act
+ incomingRequestService.emit(
+ OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
+ listenerStation,
+ request,
+ response
+ )
+
+ // Assert
+ assert.strictEqual(requestHandlerMock.mock.callCount(), 0)
+ })
+
+ await it('should set transactionRemoteStarted to true on Accepted', async () => {
+ // Arrange
+ const connectorStatus = listenerStation.getConnectorStatus(1)
+ assert.notStrictEqual(connectorStatus, undefined)
+ if (connectorStatus != null) {
+ connectorStatus.transactionRemoteStarted = false
+ }
+
+ const request: RemoteStartTransactionRequest = {
+ connectorId: 1,
+ idTag: 'TEST-TAG-001',
+ }
+ const response = { status: GenericStatus.Accepted }
+
+ // Act
+ incomingRequestService.emit(
+ OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
+ listenerStation,
+ request,
+ response
+ )
+
+ // Flush microtask queue
+ await flushMicrotasks()
+
+ // Assert
+ assert.strictEqual(connectorStatus?.transactionRemoteStarted, true)
+ })
+
+ await it('should handle StartTransaction failure gracefully', async () => {
+ // Arrange — override requestHandler to reject
+ let startTransactionCallCount = 0
+ ;(
+ listenerStation.ocppRequestService as unknown as {
+ requestHandler: (...args: unknown[]) => Promise<unknown>
+ }
+ ).requestHandler = async (_station: unknown, commandName: unknown) => {
+ if (commandName === OCPP16RequestCommand.START_TRANSACTION) {
+ startTransactionCallCount++
+ throw new Error('StartTransaction rejected by server')
+ }
+ return Promise.resolve({})
+ }
+
+ const request: RemoteStartTransactionRequest = {
+ connectorId: 1,
+ idTag: 'TEST-TAG-001',
+ }
+ const response = { status: GenericStatus.Accepted }
+
+ // Act — should not throw
+ incomingRequestService.emit(
+ OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
+ listenerStation,
+ request,
+ response
+ )
+
+ // Flush microtask queue so .catch(errorHandler) executes
+ await flushMicrotasks()
+
+ // Assert — handler was called and error was swallowed
+ assert.strictEqual(startTransactionCallCount, 1)
+ })
+ })
})
*/
import assert from 'node:assert/strict'
-import { afterEach, beforeEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import type { ChargingStation } from '../../../../src/charging-station/ChargingStation.js'
import type { TestableOCPP16IncomingRequestService } from '../../../../src/charging-station/ocpp/1.6/__testable__/index.js'
+import type { RemoteStopTransactionRequest } from '../../../../src/types/ocpp/1.6/Requests.js'
+import type { GenericResponse } from '../../../../src/types/ocpp/Common.js'
+import { OCPP16IncomingRequestService } from '../../../../src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.js'
+import { OCPP16ServiceUtils } from '../../../../src/charging-station/ocpp/1.6/OCPP16ServiceUtils.js'
+import { OCPP16IncomingRequestCommand } from '../../../../src/types/ocpp/1.6/Requests.js'
import { OCPP16UnlockStatus } from '../../../../src/types/ocpp/1.6/Responses.js'
import { OCPP16AuthorizationStatus } from '../../../../src/types/ocpp/1.6/Transaction.js'
import { GenericStatus } from '../../../../src/types/ocpp/Common.js'
import {
+ flushMicrotasks,
setupConnectorWithTransaction,
standardCleanup,
} from '../../../helpers/TestLifecycleHelpers.js'
-import { createOCPP16IncomingRequestTestContext, setMockRequestHandler } from './OCPP16TestUtils.js'
+import {
+ createOCPP16IncomingRequestTestContext,
+ createOCPP16ListenerStation,
+ setMockRequestHandler,
+} from './OCPP16TestUtils.js'
await describe('OCPP16IncomingRequestService — RemoteStopTransaction and UnlockConnector', async () => {
let station: ChargingStation
assert.notStrictEqual(response.status, undefined)
})
})
+
+ await describe('REMOTE_STOP_TRANSACTION event listener', async () => {
+ let incomingRequestService: OCPP16IncomingRequestService
+ let listenerStation: ChargingStation
+
+ beforeEach(() => {
+ incomingRequestService = new OCPP16IncomingRequestService()
+ ;({ station: listenerStation } = createOCPP16ListenerStation('test-remote-stop-listener'))
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should register REMOTE_STOP_TRANSACTION event listener in constructor', () => {
+ // Assert
+ assert.strictEqual(
+ incomingRequestService.listenerCount(OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION),
+ 1
+ )
+ })
+
+ await it('should call remoteStopTransaction when response is Accepted', async () => {
+ // Arrange
+ setupConnectorWithTransaction(listenerStation, 1, { transactionId: 42 })
+
+ const mockRemoteStop = mock.method(OCPP16ServiceUtils, 'remoteStopTransaction', () =>
+ Promise.resolve({ status: GenericStatus.Accepted } satisfies GenericResponse)
+ )
+
+ const request: RemoteStopTransactionRequest = { transactionId: 42 }
+ const response: GenericResponse = { status: GenericStatus.Accepted }
+
+ // Act
+ incomingRequestService.emit(
+ OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
+ listenerStation,
+ request,
+ response
+ )
+
+ // Flush microtask queue so the async .then() executes
+ await flushMicrotasks()
+
+ // Assert
+ assert.strictEqual(mockRemoteStop.mock.callCount(), 1)
+ assert.strictEqual(mockRemoteStop.mock.calls[0].arguments[0], listenerStation)
+ assert.strictEqual(mockRemoteStop.mock.calls[0].arguments[1], 1)
+ })
+
+ await it('should NOT call remoteStopTransaction when response is Rejected', () => {
+ // Arrange
+ const mockRemoteStop = mock.method(OCPP16ServiceUtils, 'remoteStopTransaction', () =>
+ Promise.resolve({ status: GenericStatus.Rejected } satisfies GenericResponse)
+ )
+
+ const request: RemoteStopTransactionRequest = { transactionId: 99 }
+ const response: GenericResponse = { status: GenericStatus.Rejected }
+
+ // Act
+ incomingRequestService.emit(
+ OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
+ listenerStation,
+ request,
+ response
+ )
+
+ // Assert
+ assert.strictEqual(mockRemoteStop.mock.callCount(), 0)
+ })
+
+ await it('should handle remoteStopTransaction failure gracefully', async () => {
+ // Arrange
+ setupConnectorWithTransaction(listenerStation, 1, { transactionId: 77 })
+
+ mock.method(OCPP16ServiceUtils, 'remoteStopTransaction', () =>
+ Promise.reject(new Error('remoteStopTransaction failed'))
+ )
+
+ const request: RemoteStopTransactionRequest = { transactionId: 77 }
+ const response: GenericResponse = { status: GenericStatus.Accepted }
+
+ // Act — should not throw
+ incomingRequestService.emit(
+ OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
+ listenerStation,
+ request,
+ response
+ )
+
+ // Flush microtask queue so .catch() executes
+ await flushMicrotasks()
+
+ // Assert — no crash, test completes normally
+ })
+ })
})
*/
import assert from 'node:assert/strict'
-import { afterEach, beforeEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
+import type {
+ OCPP16TriggerMessageRequest,
+ OCPP16TriggerMessageResponse,
+} from '../../../../src/types/index.js'
+
+import { OCPP16IncomingRequestService } from '../../../../src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.js'
import {
+ OCPP16IncomingRequestCommand,
OCPP16MessageTrigger,
+ OCPP16RequestCommand,
OCPP16StandardParametersKey,
OCPP16TriggerMessageStatus,
+ OCPPVersion,
} from '../../../../src/types/index.js'
-import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { flushMicrotasks, standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
import {
createOCPP16IncomingRequestTestContext,
+ createOCPP16ListenerStation,
type OCPP16IncomingRequestTestContext,
upsertConfigurationKey,
} from './OCPP16TestUtils.js'
assert.strictEqual(response.status, OCPP16TriggerMessageStatus.NOT_IMPLEMENTED)
})
})
+
+ await describe('TRIGGER_MESSAGE event listener', async () => {
+ let incomingRequestServiceForListener: OCPP16IncomingRequestService
+ let station: ReturnType<typeof createOCPP16ListenerStation>['station']
+ let requestHandlerMock: ReturnType<typeof mock.fn>
+
+ beforeEach(() => {
+ ;({ requestHandlerMock, station } = createOCPP16ListenerStation('test-trigger-listener'))
+ incomingRequestServiceForListener = new OCPP16IncomingRequestService()
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should register TRIGGER_MESSAGE event listener in constructor', () => {
+ assert.strictEqual(
+ incomingRequestServiceForListener.listenerCount(
+ OCPP16IncomingRequestCommand.TRIGGER_MESSAGE
+ ),
+ 1
+ )
+ })
+
+ await it('should NOT fire requestHandler when response is NotImplemented', () => {
+ const request: OCPP16TriggerMessageRequest = {
+ requestedMessage: OCPP16MessageTrigger.BootNotification,
+ }
+ const response: OCPP16TriggerMessageResponse = {
+ status: OCPP16TriggerMessageStatus.NOT_IMPLEMENTED,
+ }
+
+ incomingRequestServiceForListener.emit(
+ OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
+ station,
+ request,
+ response
+ )
+
+ assert.strictEqual(requestHandlerMock.mock.callCount(), 0)
+ })
+
+ const triggerCases: {
+ connectorId?: number
+ expectedCommand: OCPP16RequestCommand
+ name: string
+ trigger: OCPP16MessageTrigger
+ }[] = [
+ {
+ expectedCommand: OCPP16RequestCommand.BOOT_NOTIFICATION,
+ name: 'BootNotification',
+ trigger: OCPP16MessageTrigger.BootNotification,
+ },
+ {
+ expectedCommand: OCPP16RequestCommand.HEARTBEAT,
+ name: 'Heartbeat',
+ trigger: OCPP16MessageTrigger.Heartbeat,
+ },
+ {
+ connectorId: 1,
+ expectedCommand: OCPP16RequestCommand.STATUS_NOTIFICATION,
+ name: 'StatusNotification',
+ trigger: OCPP16MessageTrigger.StatusNotification,
+ },
+ {
+ expectedCommand: OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
+ name: 'FirmwareStatusNotification',
+ trigger: OCPP16MessageTrigger.FirmwareStatusNotification,
+ },
+ {
+ expectedCommand: OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
+ name: 'DiagnosticsStatusNotification',
+ trigger: OCPP16MessageTrigger.DiagnosticsStatusNotification,
+ },
+ ]
+
+ for (const { connectorId, expectedCommand, name, trigger } of triggerCases) {
+ await it(`should fire ${name} requestHandler on Accepted`, () => {
+ const request: OCPP16TriggerMessageRequest = {
+ requestedMessage: trigger,
+ ...(connectorId != null && { connectorId }),
+ }
+ const response: OCPP16TriggerMessageResponse = {
+ status: OCPP16TriggerMessageStatus.ACCEPTED,
+ }
+
+ incomingRequestServiceForListener.emit(
+ OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
+ station,
+ request,
+ response
+ )
+
+ assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+ const args = requestHandlerMock.mock.calls[0].arguments as [unknown, string, ...unknown[]]
+ assert.strictEqual(args[1], expectedCommand)
+ })
+ }
+
+ await it('should handle requestHandler rejection gracefully', async () => {
+ const rejectingMock = mock.fn(async () => Promise.reject(new Error('test error')))
+ const { station: rejectStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 2,
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ ocppRequestService: {
+ requestHandler: rejectingMock,
+ },
+ stationInfo: {
+ ocppVersion: OCPPVersion.VERSION_16,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+
+ const request: OCPP16TriggerMessageRequest = {
+ requestedMessage: OCPP16MessageTrigger.BootNotification,
+ }
+ const response: OCPP16TriggerMessageResponse = {
+ status: OCPP16TriggerMessageStatus.ACCEPTED,
+ }
+
+ incomingRequestServiceForListener.emit(
+ OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
+ rejectStation,
+ request,
+ response
+ )
+
+ // Flush microtask queue so .catch(errorHandler) executes
+ await flushMicrotasks()
+
+ assert.strictEqual(rejectingMock.mock.callCount(), 1)
+ })
+ })
})
* and configuration key helpers for OCPP 1.6 unit and integration tests.
*/
+import { mock } from 'node:test'
+
import type { ChargingStation } from '../../../../src/charging-station/ChargingStation.js'
import type { ChargingStationInfo } from '../../../../src/types/ChargingStationInfo.js'
import type { ConfigurationKey } from '../../../../src/types/ChargingStationOcppConfiguration.js'
return { incomingRequestService, station, testableService }
}
+/**
+ * Create a listener station with a mocked request handler for OCPP 1.6 tests.
+ * @param baseName - Base name for the charging station
+ * @returns Object containing the mock request handler and charging station
+ */
+export function createOCPP16ListenerStation (baseName: string): {
+ requestHandlerMock: ReturnType<typeof mock.fn>
+ station: ChargingStation
+} {
+ const requestHandlerMock = mock.fn(async () => Promise.resolve({}))
+ const { station } = createMockChargingStation({
+ baseName,
+ connectorsCount: 2,
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ ocppRequestService: {
+ requestHandler: requestHandlerMock,
+ },
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_16,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ return { requestHandlerMock, station }
+}
+
/**
* Create a standard OCPP 1.6 request test context with response service,
* request service, testable wrapper, and mock charging station.
} from '../../../../src/types/index.js'
import { OCPP20IdTokenEnumType } from '../../../../src/types/ocpp/2.0/Transaction.js'
import { Constants } from '../../../../src/utils/index.js'
-import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { flushMicrotasks, standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
})
})
- await it('should register CUSTOMER_INFORMATION event listener in constructor', () => {
- const service = new OCPP20IncomingRequestService()
- assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION), 1)
- })
-
- await it('should call sendNotifyCustomerInformation when CUSTOMER_INFORMATION event emitted with Accepted + report=true', () => {
- const service = new OCPP20IncomingRequestService()
- const notifyMock = mock.method(
- service as unknown as {
- sendNotifyCustomerInformation: (
- chargingStation: ChargingStation,
- requestId: number
- ) => Promise<void>
- },
- 'sendNotifyCustomerInformation',
- () => Promise.resolve()
- )
-
- const request: OCPP20CustomerInformationRequest = {
- clear: false,
- idToken: { idToken: 'TOKEN_001', type: OCPP20IdTokenEnumType.Central },
- report: true,
- requestId: 20,
- }
- const response: OCPP20CustomerInformationResponse = {
- status: CustomerInformationStatusEnumType.Accepted,
- }
-
- service.emit(OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION, station, request, response)
-
- assert.strictEqual(notifyMock.mock.callCount(), 1)
- assert.strictEqual(notifyMock.mock.calls[0].arguments[1], 20)
- })
-
- await it('should NOT call sendNotifyCustomerInformation when CUSTOMER_INFORMATION event emitted with Accepted + clear=true only', () => {
- // CRITICAL: clear=true also returns Accepted — listener must NOT fire notification
- const service = new OCPP20IncomingRequestService()
- const notifyMock = mock.method(
- service as unknown as {
- sendNotifyCustomerInformation: (
- chargingStation: ChargingStation,
- requestId: number
- ) => Promise<void>
- },
- 'sendNotifyCustomerInformation',
- () => Promise.resolve()
- )
-
- const request: OCPP20CustomerInformationRequest = {
- clear: true,
- report: false,
- requestId: 21,
- }
- const response: OCPP20CustomerInformationResponse = {
- status: CustomerInformationStatusEnumType.Accepted,
- }
-
- service.emit(OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION, station, request, response)
-
- assert.strictEqual(notifyMock.mock.callCount(), 0)
- })
-
- await it('should NOT call sendNotifyCustomerInformation when CUSTOMER_INFORMATION event emitted with Rejected', () => {
- const service = new OCPP20IncomingRequestService()
- const notifyMock = mock.method(
- service as unknown as {
- sendNotifyCustomerInformation: (
- chargingStation: ChargingStation,
- requestId: number
- ) => Promise<void>
- },
- 'sendNotifyCustomerInformation',
- () => Promise.resolve()
- )
+ await describe('CUSTOMER_INFORMATION event listener', async () => {
+ let incomingRequestService: OCPP20IncomingRequestService
+ let notifyMock: ReturnType<typeof mock.fn>
+
+ beforeEach(() => {
+ incomingRequestService = new OCPP20IncomingRequestService()
+ notifyMock = mock.method(
+ incomingRequestService as unknown as {
+ sendNotifyCustomerInformation: (
+ chargingStation: ChargingStation,
+ requestId: number
+ ) => Promise<void>
+ },
+ 'sendNotifyCustomerInformation',
+ () => Promise.resolve()
+ )
+ })
- const request: OCPP20CustomerInformationRequest = {
- clear: false,
- report: false,
- requestId: 22,
- }
- const response: OCPP20CustomerInformationResponse = {
- status: CustomerInformationStatusEnumType.Rejected,
- }
+ afterEach(() => {
+ standardCleanup()
+ })
- service.emit(OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION, station, request, response)
+ await it('should register CUSTOMER_INFORMATION event listener in constructor', () => {
+ assert.strictEqual(
+ incomingRequestService.listenerCount(OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION),
+ 1
+ )
+ })
- assert.strictEqual(notifyMock.mock.callCount(), 0)
- })
+ await it('should call sendNotifyCustomerInformation when CUSTOMER_INFORMATION event emitted with Accepted + report=true', () => {
+ const request: OCPP20CustomerInformationRequest = {
+ clear: false,
+ idToken: { idToken: 'TOKEN_001', type: OCPP20IdTokenEnumType.Central },
+ report: true,
+ requestId: 20,
+ }
+ const response: OCPP20CustomerInformationResponse = {
+ status: CustomerInformationStatusEnumType.Accepted,
+ }
+
+ incomingRequestService.emit(
+ OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION,
+ station,
+ request,
+ response
+ )
+
+ assert.strictEqual(notifyMock.mock.callCount(), 1)
+ assert.strictEqual(notifyMock.mock.calls[0].arguments[1], 20)
+ })
- await it('should handle sendNotifyCustomerInformation rejection gracefully', async () => {
- const service = new OCPP20IncomingRequestService()
- mock.method(
- service as unknown as {
- sendNotifyCustomerInformation: (
- chargingStation: ChargingStation,
- requestId: number
- ) => Promise<void>
- },
- 'sendNotifyCustomerInformation',
- () => Promise.reject(new Error('notification error'))
- )
+ await it('should NOT call sendNotifyCustomerInformation when CUSTOMER_INFORMATION event emitted with Accepted + clear=true only', () => {
+ // CRITICAL: clear=true also returns Accepted — listener must NOT fire notification
+ const request: OCPP20CustomerInformationRequest = {
+ clear: true,
+ report: false,
+ requestId: 21,
+ }
+ const response: OCPP20CustomerInformationResponse = {
+ status: CustomerInformationStatusEnumType.Accepted,
+ }
+
+ incomingRequestService.emit(
+ OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION,
+ station,
+ request,
+ response
+ )
+
+ assert.strictEqual(notifyMock.mock.callCount(), 0)
+ })
- const request: OCPP20CustomerInformationRequest = {
- clear: false,
- idToken: { idToken: 'TOKEN_001', type: OCPP20IdTokenEnumType.Central },
- report: true,
- requestId: 99,
- }
- const response: OCPP20CustomerInformationResponse = {
- status: CustomerInformationStatusEnumType.Accepted,
- }
+ await it('should NOT call sendNotifyCustomerInformation when CUSTOMER_INFORMATION event emitted with Rejected', () => {
+ const request: OCPP20CustomerInformationRequest = {
+ clear: false,
+ report: false,
+ requestId: 22,
+ }
+ const response: OCPP20CustomerInformationResponse = {
+ status: CustomerInformationStatusEnumType.Rejected,
+ }
+
+ incomingRequestService.emit(
+ OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION,
+ station,
+ request,
+ response
+ )
+
+ assert.strictEqual(notifyMock.mock.callCount(), 0)
+ })
- service.emit(OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION, station, request, response)
+ await it('should handle sendNotifyCustomerInformation rejection gracefully', async () => {
+ mock.method(
+ incomingRequestService as unknown as {
+ sendNotifyCustomerInformation: (
+ chargingStation: ChargingStation,
+ requestId: number
+ ) => Promise<void>
+ },
+ 'sendNotifyCustomerInformation',
+ () => Promise.reject(new Error('notification error'))
+ )
- await Promise.resolve()
+ const request: OCPP20CustomerInformationRequest = {
+ clear: false,
+ idToken: { idToken: 'TOKEN_001', type: OCPP20IdTokenEnumType.Central },
+ report: true,
+ requestId: 99,
+ }
+ const response: OCPP20CustomerInformationResponse = {
+ status: CustomerInformationStatusEnumType.Accepted,
+ }
+
+ incomingRequestService.emit(
+ OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION,
+ station,
+ request,
+ response
+ )
+
+ await flushMicrotasks()
+ })
})
})
-import { millisecondsToSeconds } from 'date-fns'
/**
* @file Tests for OCPP20IncomingRequestService GetBaseReport
* @description Unit tests for OCPP 2.0 GetBaseReport command handling (B07)
*/
+import { millisecondsToSeconds } from 'date-fns'
import assert from 'node:assert/strict'
-import { afterEach, beforeEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import type { ChargingStation } from '../../../../src/charging-station/index.js'
OCPP20ComponentName,
OCPP20DeviceInfoVariableName,
type OCPP20GetBaseReportRequest,
+ type OCPP20GetBaseReportResponse,
+ OCPP20IncomingRequestCommand,
OCPP20OptionalVariableName,
OCPP20RequiredVariableName,
type OCPP20SetVariableResultType,
} from '../../../../src/types/index.js'
import { StandardParametersKey } from '../../../../src/types/ocpp/Configuration.js'
import { Constants } from '../../../../src/utils/index.js'
-import { standardCleanup } from '../../../../tests/helpers/TestLifecycleHelpers.js'
+import { flushMicrotasks, standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
import {
TEST_CHARGE_POINT_MODEL,
TEST_CHARGE_POINT_SERIAL_NUMBER,
assert.ok(Array.isArray(reportData))
assert.strictEqual(reportData.length, 0)
})
+
+ await describe('GET_BASE_REPORT event listener', async () => {
+ let listenerService: OCPP20IncomingRequestService
+ let sendNotifyMock: ReturnType<typeof mock.fn>
+
+ beforeEach(() => {
+ listenerService = new OCPP20IncomingRequestService()
+ sendNotifyMock = mock.method(
+ listenerService as unknown as {
+ sendNotifyReportRequest: (
+ chargingStation: ChargingStation,
+ request: OCPP20GetBaseReportRequest,
+ response: OCPP20GetBaseReportResponse
+ ) => Promise<void>
+ },
+ 'sendNotifyReportRequest',
+ () => Promise.resolve()
+ )
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should register GET_BASE_REPORT event listener in constructor', () => {
+ assert.strictEqual(
+ listenerService.listenerCount(OCPP20IncomingRequestCommand.GET_BASE_REPORT),
+ 1
+ )
+ })
+
+ await it('should call sendNotifyReportRequest when response is Accepted', () => {
+ const request: OCPP20GetBaseReportRequest = {
+ reportBase: ReportBaseEnumType.FullInventory,
+ requestId: 1,
+ }
+ const response: OCPP20GetBaseReportResponse = {
+ status: GenericDeviceModelStatusEnumType.Accepted,
+ }
+
+ listenerService.emit(OCPP20IncomingRequestCommand.GET_BASE_REPORT, station, request, response)
+
+ assert.strictEqual(sendNotifyMock.mock.callCount(), 1)
+ })
+
+ await it('should NOT call sendNotifyReportRequest when response is NotSupported', () => {
+ const request: OCPP20GetBaseReportRequest = {
+ reportBase: ReportBaseEnumType.FullInventory,
+ requestId: 2,
+ }
+ const response: OCPP20GetBaseReportResponse = {
+ status: GenericDeviceModelStatusEnumType.NotSupported,
+ }
+
+ listenerService.emit(OCPP20IncomingRequestCommand.GET_BASE_REPORT, station, request, response)
+
+ assert.strictEqual(sendNotifyMock.mock.callCount(), 0)
+ })
+
+ await it('should handle sendNotifyReportRequest rejection gracefully', async () => {
+ mock.method(
+ listenerService as unknown as {
+ sendNotifyReportRequest: (
+ chargingStation: ChargingStation,
+ request: OCPP20GetBaseReportRequest,
+ response: OCPP20GetBaseReportResponse
+ ) => Promise<void>
+ },
+ 'sendNotifyReportRequest',
+ () => Promise.reject(new Error('notify report error'))
+ )
+
+ const request: OCPP20GetBaseReportRequest = {
+ reportBase: ReportBaseEnumType.FullInventory,
+ requestId: 3,
+ }
+ const response: OCPP20GetBaseReportResponse = {
+ status: GenericDeviceModelStatusEnumType.Accepted,
+ }
+
+ listenerService.emit(OCPP20IncomingRequestCommand.GET_BASE_REPORT, station, request, response)
+
+ await flushMicrotasks()
+ })
+ })
})
})
await describe('GET_LOG event listener', async () => {
- await it('should register GET_LOG event listener in constructor', () => {
- const service = new OCPP20IncomingRequestService()
- assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.GET_LOG), 1)
- })
+ let service: OCPP20IncomingRequestService
+ let simulateMock: ReturnType<typeof mock.fn>
- await it('should call simulateLogUploadLifecycle when GET_LOG event emitted with Accepted response', () => {
- const service = new OCPP20IncomingRequestService()
- const simulateMock = mock.method(
+ beforeEach(() => {
+ service = new OCPP20IncomingRequestService()
+ simulateMock = mock.method(
service as unknown as {
simulateLogUploadLifecycle: (
chargingStation: ChargingStation,
'simulateLogUploadLifecycle',
() => Promise.resolve()
)
+ })
+ await it('should register GET_LOG event listener in constructor', () => {
+ assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.GET_LOG), 1)
+ })
+
+ await it('should call simulateLogUploadLifecycle when GET_LOG event emitted with Accepted response', () => {
const request: OCPP20GetLogRequest = {
log: {
remoteLocation: 'https://csms.example.com/logs',
})
await it('should NOT call simulateLogUploadLifecycle when GET_LOG event emitted with Rejected response', () => {
- const service = new OCPP20IncomingRequestService()
- const simulateMock = mock.method(
- service as unknown as {
- simulateLogUploadLifecycle: (
- chargingStation: ChargingStation,
- requestId: number
- ) => Promise<void>
- },
- 'simulateLogUploadLifecycle',
- () => Promise.resolve()
- )
-
const request: OCPP20GetLogRequest = {
log: {
remoteLocation: 'https://csms.example.com/logs',
})
await it('should handle simulateLogUploadLifecycle rejection gracefully', async () => {
- const service = new OCPP20IncomingRequestService()
mock.method(
service as unknown as {
simulateLogUploadLifecycle: (
service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
- await Promise.resolve()
+ await flushMicrotasks()
})
await describe('N01 - LogStatusNotification lifecycle', async () => {
-import { millisecondsToSeconds } from 'date-fns'
/**
* @file Tests for OCPP20IncomingRequestService GetVariables
* @description Unit tests for OCPP 2.0 GetVariables command handling (B06)
*/
+
+import { millisecondsToSeconds } from 'date-fns'
import assert from 'node:assert/strict'
import { afterEach, beforeEach, describe, it } from 'node:test'
ReasonCodeEnumType,
} from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
-import { standardCleanup } from '../../../../tests/helpers/TestLifecycleHelpers.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
import {
TEST_CHARGING_STATION_BASE_NAME,
TEST_CONNECTOR_ID_VALID_INSTANCE,
type OCPP20IdTokenType,
} from '../../../../src/types/ocpp/2.0/Transaction.js'
import { OCPPVersion } from '../../../../src/types/ocpp/OCPPVersion.js'
-import { standardCleanup } from '../../../../tests/helpers/TestLifecycleHelpers.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
await describe('G03 - Remote Start Pre-Authorization', async () => {
let service: OCPP20IncomingRequestService | undefined
* @description Unit tests for OCPP 2.0 RequestStartTransaction command handling (F01/F02)
*/
import assert from 'node:assert/strict'
-import { afterEach, beforeEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import type { ChargingStation } from '../../../../src/charging-station/index.js'
-import type { OCPP20RequestStartTransactionRequest } from '../../../../src/types/index.js'
+import type {
+ OCPP20RequestStartTransactionRequest,
+ OCPP20RequestStartTransactionResponse,
+ OCPP20TransactionEventRequest,
+} from '../../../../src/types/index.js'
import type {
OCPP20ChargingProfileType,
OCPP20ChargingRateUnitEnumType,
import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
import { OCPPAuthServiceFactory } from '../../../../src/charging-station/ocpp/auth/services/OCPPAuthServiceFactory.js'
-import { OCPPVersion, RequestStartStopStatusEnumType } from '../../../../src/types/index.js'
+import {
+ OCPP20IncomingRequestCommand,
+ OCPP20RequestCommand,
+ OCPP20TransactionEventEnumType,
+ OCPP20TriggerReasonEnumType,
+ OCPPVersion,
+ RequestStartStopStatusEnumType,
+} from '../../../../src/types/index.js'
import {
OCPP20ChargingProfileKindEnumType,
OCPP20ChargingProfilePurposeEnumType,
OCPP20IdTokenEnumType,
} from '../../../../src/types/ocpp/2.0/Transaction.js'
import { Constants } from '../../../../src/utils/index.js'
-import { standardCleanup } from '../../../../tests/helpers/TestLifecycleHelpers.js'
+import { flushMicrotasks, standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
import { createMockAuthService } from '../auth/helpers/MockFactories.js'
import {
+ createOCPP20ListenerStation,
resetConnectorTransactionState,
resetLimits,
resetReportingValueSize,
}
assert.ok(response.transactionId.length > 0)
})
+
+ await describe('REQUEST_START_TRANSACTION event listener', async () => {
+ let listenerService: OCPP20IncomingRequestService
+ let requestHandlerMock: ReturnType<typeof mock.fn>
+ let listenerStation: ChargingStation
+
+ beforeEach(() => {
+ ;({ requestHandlerMock, station: listenerStation } = createOCPP20ListenerStation(
+ TEST_CHARGING_STATION_BASE_NAME + '-LISTENER'
+ ))
+ listenerService = new OCPP20IncomingRequestService()
+ testableService = createTestableIncomingRequestService(listenerService)
+ const stationId = listenerStation.stationInfo?.chargingStationId ?? 'unknown'
+ OCPPAuthServiceFactory.setInstanceForTesting(stationId, createMockAuthService())
+ resetConnectorTransactionState(listenerStation)
+ resetLimits(listenerStation)
+ resetReportingValueSize(listenerStation)
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ OCPPAuthServiceFactory.clearAllInstances()
+ })
+
+ await it('should register REQUEST_START_TRANSACTION event listener in constructor', () => {
+ assert.strictEqual(
+ listenerService.listenerCount(OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION),
+ 1
+ )
+ })
+
+ await it('should call TransactionEvent(Started) when response is Accepted', async () => {
+ const startRequest: OCPP20RequestStartTransactionRequest = {
+ evseId: 1,
+ idToken: {
+ idToken: 'LISTENER_TOKEN_1',
+ type: OCPP20IdTokenEnumType.ISO14443,
+ },
+ remoteStartId: 1,
+ }
+ const startResponse = await testableService.handleRequestStartTransaction(
+ listenerStation,
+ startRequest
+ )
+ assert.strictEqual(startResponse.status, RequestStartStopStatusEnumType.Accepted)
+ requestHandlerMock.mock.resetCalls()
+
+ const request: OCPP20RequestStartTransactionRequest = {
+ evseId: 1,
+ idToken: {
+ idToken: 'LISTENER_TOKEN_1',
+ type: OCPP20IdTokenEnumType.ISO14443,
+ },
+ remoteStartId: 1,
+ }
+ const response: OCPP20RequestStartTransactionResponse = {
+ status: RequestStartStopStatusEnumType.Accepted,
+ transactionId: startResponse.transactionId,
+ }
+
+ listenerService.emit(
+ OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
+ listenerStation,
+ request,
+ response
+ )
+
+ await flushMicrotasks()
+
+ assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+ const args = requestHandlerMock.mock.calls[0].arguments as [
+ unknown,
+ string,
+ OCPP20TransactionEventRequest
+ ]
+ assert.strictEqual(args[1], OCPP20RequestCommand.TRANSACTION_EVENT)
+ assert.strictEqual(args[2].eventType, OCPP20TransactionEventEnumType.Started)
+ })
+
+ await it('should NOT call TransactionEvent when response is Rejected', () => {
+ const request: OCPP20RequestStartTransactionRequest = {
+ evseId: 1,
+ idToken: {
+ idToken: 'REJECTED_TOKEN',
+ type: OCPP20IdTokenEnumType.ISO14443,
+ },
+ remoteStartId: 2,
+ }
+ const response: OCPP20RequestStartTransactionResponse = {
+ status: RequestStartStopStatusEnumType.Rejected,
+ }
+
+ listenerService.emit(
+ OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
+ listenerStation,
+ request,
+ response
+ )
+
+ assert.strictEqual(requestHandlerMock.mock.callCount(), 0)
+ })
+
+ // E02.FR.01 — CS SHALL send TransactionEvent(Started) with RemoteStart trigger reason
+ await it('should send TransactionEvent(Started) with RemoteStart trigger reason', async () => {
+ const startRequest: OCPP20RequestStartTransactionRequest = {
+ evseId: 2,
+ idToken: {
+ idToken: 'TRIGGER_REASON_TOKEN',
+ type: OCPP20IdTokenEnumType.ISO14443,
+ },
+ remoteStartId: 3,
+ }
+ const startResponse = await testableService.handleRequestStartTransaction(
+ listenerStation,
+ startRequest
+ )
+ assert.strictEqual(startResponse.status, RequestStartStopStatusEnumType.Accepted)
+ requestHandlerMock.mock.resetCalls()
+
+ const request: OCPP20RequestStartTransactionRequest = {
+ evseId: 2,
+ idToken: {
+ idToken: 'TRIGGER_REASON_TOKEN',
+ type: OCPP20IdTokenEnumType.ISO14443,
+ },
+ remoteStartId: 3,
+ }
+ const response: OCPP20RequestStartTransactionResponse = {
+ status: RequestStartStopStatusEnumType.Accepted,
+ transactionId: startResponse.transactionId,
+ }
+
+ listenerService.emit(
+ OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
+ listenerStation,
+ request,
+ response
+ )
+
+ await flushMicrotasks()
+
+ assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+ const args = requestHandlerMock.mock.calls[0].arguments as [
+ unknown,
+ string,
+ OCPP20TransactionEventRequest
+ ]
+ const transactionEvent = args[2]
+ assert.strictEqual(transactionEvent.triggerReason, OCPP20TriggerReasonEnumType.RemoteStart)
+ })
+
+ await it('should handle TransactionEvent failure gracefully', async () => {
+ let transactionEventCallCount = 0
+ const { station: failStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME + '-FAIL-START',
+ connectorsCount: 1,
+ evseConfiguration: { evsesCount: 1 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ ocppRequestService: {
+ requestHandler: async (_chargingStation: unknown, commandName: unknown) => {
+ if (commandName === OCPP20RequestCommand.TRANSACTION_EVENT) {
+ transactionEventCallCount++
+ throw new Error('TransactionEvent rejected by server')
+ }
+ return Promise.resolve({})
+ },
+ },
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+
+ const failStationId = failStation.stationInfo?.chargingStationId ?? 'unknown'
+ OCPPAuthServiceFactory.setInstanceForTesting(failStationId, createMockAuthService())
+
+ resetConnectorTransactionState(failStation)
+ const startResponse = await testableService.handleRequestStartTransaction(failStation, {
+ evseId: 1,
+ idToken: {
+ idToken: 'FAIL_START_TOKEN',
+ type: OCPP20IdTokenEnumType.ISO14443,
+ },
+ remoteStartId: 999,
+ })
+ assert.strictEqual(startResponse.status, RequestStartStopStatusEnumType.Accepted)
+
+ listenerService.emit(
+ OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
+ failStation,
+ {
+ evseId: 1,
+ idToken: {
+ idToken: 'FAIL_START_TOKEN',
+ type: OCPP20IdTokenEnumType.ISO14443,
+ },
+ remoteStartId: 999,
+ } satisfies OCPP20RequestStartTransactionRequest,
+ {
+ status: RequestStartStopStatusEnumType.Accepted,
+ transactionId: startResponse.transactionId,
+ } satisfies OCPP20RequestStartTransactionResponse
+ )
+
+ await flushMicrotasks()
+
+ assert.strictEqual(transactionEventCallCount, 1)
+ })
+ })
})
OCPP20ReasonEnumType,
} from '../../../../src/types/ocpp/2.0/Transaction.js'
import { Constants } from '../../../../src/utils/index.js'
-import { standardCleanup } from '../../../../tests/helpers/TestLifecycleHelpers.js'
+import { flushMicrotasks, standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
import { createMockAuthService } from '../auth/helpers/MockFactories.js'
import {
+ createOCPP20ListenerStation,
resetConnectorTransactionState,
resetLimits,
resetReportingValueSize,
} from './OCPP20TestUtils.js'
-/**
- * @param baseName
- * @returns The mock station and its request handler spy
- */
-function createListenerStation (baseName: string): {
- requestHandlerMock: ReturnType<typeof mock.fn>
- station: ChargingStation
-} {
- const requestHandlerMock = mock.fn(async () => Promise.resolve({}))
- const { station } = createMockChargingStation({
- baseName,
- connectorsCount: 3,
- evseConfiguration: { evsesCount: 3 },
- heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
- ocppRequestService: {
- requestHandler: requestHandlerMock,
- },
- stationInfo: {
- ocppStrictCompliance: false,
- ocppVersion: OCPPVersion.VERSION_201,
- },
- websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
- })
- return { requestHandlerMock, station }
-}
-
await describe('F03 - Remote Stop Transaction', async () => {
let mockStation: ChargingStation
let incomingRequestService: OCPP20IncomingRequestService
let testableService: ReturnType<typeof createTestableIncomingRequestService>
beforeEach(() => {
- const { station } = createListenerStation(TEST_CHARGING_STATION_BASE_NAME)
+ const { station } = createOCPP20ListenerStation(TEST_CHARGING_STATION_BASE_NAME)
mockStation = station
incomingRequestService = new OCPP20IncomingRequestService()
testableService = createTestableIncomingRequestService(incomingRequestService)
})
/**
- * @param station
- * @param evseId
- * @param remoteStartId
- * @param skipReset
+ * Starts a transaction via RequestStartTransaction and returns its ID.
+ * @param station - The charging station to start a transaction on
+ * @param evseId - EVSE ID to use
+ * @param remoteStartId - Remote start ID
+ * @param skipReset - Whether to skip resetting mock call counts
* @returns The transaction ID of the started transaction
*/
async function startTransaction (
let listenerStation: ChargingStation
beforeEach(() => {
- ;({ requestHandlerMock, station: listenerStation } = createListenerStation(
+ ;({ requestHandlerMock, station: listenerStation } = createOCPP20ListenerStation(
TEST_CHARGING_STATION_BASE_NAME + '-LISTENER'
))
listenerService = new OCPP20IncomingRequestService()
resetReportingValueSize(listenerStation)
})
+ afterEach(() => {
+ standardCleanup()
+ })
+
await it('should register REQUEST_STOP_TRANSACTION event listener in constructor', () => {
assert.strictEqual(
listenerService.listenerCount(OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION),
response
)
- assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+ await flushMicrotasks()
+
+ assert.strictEqual(requestHandlerMock.mock.callCount(), 2)
const args = requestHandlerMock.mock.calls[0].arguments as [
unknown,
string,
)
// Flush microtask queue so .catch(errorHandler) executes
- await Promise.resolve()
+ await flushMicrotasks()
assert.strictEqual(transactionEventCallCount, 1)
})
TriggerMessageStatusEnumType,
} from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
-import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { flushMicrotasks, standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
assert.strictEqual(requestHandlerMock.mock.callCount(), 0)
})
- await it('should fire BootNotification requestHandler on Accepted', () => {
- const request: OCPP20TriggerMessageRequest = {
- requestedMessage: MessageTriggerEnumType.BootNotification,
- }
- const response: OCPP20TriggerMessageResponse = {
- status: TriggerMessageStatusEnumType.Accepted,
- }
-
- incomingRequestServiceForListener.emit(
- OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
- mockStation,
- request,
- response
- )
-
- assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
- })
-
- await it('should fire Heartbeat requestHandler on Accepted', () => {
- const request: OCPP20TriggerMessageRequest = {
- requestedMessage: MessageTriggerEnumType.Heartbeat,
- }
- const response: OCPP20TriggerMessageResponse = {
- status: TriggerMessageStatusEnumType.Accepted,
- }
-
- incomingRequestServiceForListener.emit(
- OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
- mockStation,
- request,
- response
- )
-
- assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
- })
-
- await it('should fire FirmwareStatusNotification requestHandler on Accepted', () => {
- const request: OCPP20TriggerMessageRequest = {
- requestedMessage: MessageTriggerEnumType.FirmwareStatusNotification,
- }
- const response: OCPP20TriggerMessageResponse = {
- status: TriggerMessageStatusEnumType.Accepted,
- }
-
- incomingRequestServiceForListener.emit(
- OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
- mockStation,
- request,
- response
- )
-
- assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
- })
-
- await it('should fire LogStatusNotification requestHandler on Accepted', () => {
- const request: OCPP20TriggerMessageRequest = {
- requestedMessage: MessageTriggerEnumType.LogStatusNotification,
- }
- const response: OCPP20TriggerMessageResponse = {
- status: TriggerMessageStatusEnumType.Accepted,
- }
-
- incomingRequestServiceForListener.emit(
- OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
- mockStation,
- request,
- response
- )
-
- assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
- })
-
- await it('should fire MeterValues requestHandler on Accepted', () => {
- const request: OCPP20TriggerMessageRequest = {
- requestedMessage: MessageTriggerEnumType.MeterValues,
- }
- const response: OCPP20TriggerMessageResponse = {
- status: TriggerMessageStatusEnumType.Accepted,
- }
-
- incomingRequestServiceForListener.emit(
- OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
- mockStation,
- request,
- response
- )
-
- assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
- })
+ const triggerCases: {
+ name: string
+ trigger: MessageTriggerEnumType
+ }[] = [
+ {
+ name: 'BootNotification',
+ trigger: MessageTriggerEnumType.BootNotification,
+ },
+ {
+ name: 'Heartbeat',
+ trigger: MessageTriggerEnumType.Heartbeat,
+ },
+ {
+ name: 'FirmwareStatusNotification',
+ trigger: MessageTriggerEnumType.FirmwareStatusNotification,
+ },
+ {
+ name: 'LogStatusNotification',
+ trigger: MessageTriggerEnumType.LogStatusNotification,
+ },
+ {
+ name: 'MeterValues',
+ trigger: MessageTriggerEnumType.MeterValues,
+ },
+ ]
+
+ for (const { name, trigger } of triggerCases) {
+ await it(`should fire ${name} requestHandler on Accepted`, () => {
+ const request: OCPP20TriggerMessageRequest = {
+ requestedMessage: trigger,
+ }
+ const response: OCPP20TriggerMessageResponse = {
+ status: TriggerMessageStatusEnumType.Accepted,
+ }
+
+ incomingRequestServiceForListener.emit(
+ OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
+ mockStation,
+ request,
+ response
+ )
+
+ assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+ })
+ }
await it('should broadcast StatusNotification for all EVSEs on Accepted without specific EVSE', () => {
const request: OCPP20TriggerMessageRequest = {
)
// Flush microtask queue so .catch(errorHandler) executes
- await Promise.resolve()
+ await flushMicrotasks()
assert.strictEqual(rejectingMock.mock.callCount(), 1)
})
})
await describe('UPDATE_FIRMWARE event listener', async () => {
- await it('should register UPDATE_FIRMWARE event listener in constructor', () => {
- const service = new OCPP20IncomingRequestService()
- assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE), 1)
- })
+ let service: OCPP20IncomingRequestService
+ let simulateMock: ReturnType<typeof mock.fn>
- await it('should call simulateFirmwareUpdateLifecycle when UPDATE_FIRMWARE event emitted with Accepted response', () => {
- const service = new OCPP20IncomingRequestService()
- const simulateMock = mock.method(
+ beforeEach(() => {
+ service = new OCPP20IncomingRequestService()
+ simulateMock = mock.method(
service as unknown as {
simulateFirmwareUpdateLifecycle: (
chargingStation: ChargingStation,
'simulateFirmwareUpdateLifecycle',
() => Promise.resolve()
)
+ })
+ await it('should register UPDATE_FIRMWARE event listener in constructor', () => {
+ assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE), 1)
+ })
+
+ await it('should call simulateFirmwareUpdateLifecycle when UPDATE_FIRMWARE event emitted with Accepted response', () => {
const request: OCPP20UpdateFirmwareRequest = {
firmware: {
location: 'https://firmware.example.com/update.bin',
})
await it('should NOT call simulateFirmwareUpdateLifecycle when UPDATE_FIRMWARE event emitted with Rejected response', () => {
- const service = new OCPP20IncomingRequestService()
- const simulateMock = mock.method(
- service as unknown as {
- simulateFirmwareUpdateLifecycle: (
- chargingStation: ChargingStation,
- requestId: number,
- firmware: FirmwareType
- ) => Promise<void>
- },
- 'simulateFirmwareUpdateLifecycle',
- () => Promise.resolve()
- )
-
const request: OCPP20UpdateFirmwareRequest = {
firmware: {
location: 'https://firmware.example.com/update.bin',
})
await it('should handle simulateFirmwareUpdateLifecycle rejection gracefully', async () => {
- const service = new OCPP20IncomingRequestService()
mock.method(
service as unknown as {
simulateFirmwareUpdateLifecycle: (
service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
- await Promise.resolve()
+ await flushMicrotasks()
})
await it('should cancel previous firmware update when new one arrives', async t => {
type OCPP20TransactionContext,
} from '../../../../src/types/ocpp/2.0/Transaction.js'
import { Constants, generateUUID } from '../../../../src/utils/index.js'
-import { standardCleanup } from '../../../../tests/helpers/TestLifecycleHelpers.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
import {
+/**
+ * @file OCPP 2.0 test utilities
+ * @description Shared helpers, mock factories, and fixtures for OCPP 2.0 test suites
+ */
import { mock } from 'node:test'
import type { ChargingStation } from '../../../../src/charging-station/ChargingStation.js'
}
}
+/**
+ * Create a mock OCPP 2.0 charging station with a spy requestHandler for listener tests.
+ * @param baseName - Base name for the mock charging station
+ * @returns The mock station and its request handler spy
+ */
+export function createOCPP20ListenerStation (baseName: string): {
+ requestHandlerMock: ReturnType<typeof mock.fn>
+ station: ChargingStation
+} {
+ const requestHandlerMock = mock.fn(async () => Promise.resolve({}))
+ const { station } = createMockChargingStation({
+ baseName,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ ocppRequestService: {
+ requestHandler: requestHandlerMock,
+ },
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ return { requestHandlerMock, station }
+}
+
/**
* Create a mock ChargingStation with certificate manager for testing.
* This encapsulates the type casting pattern for ChargingStationWithCertificateManager.
} from '../../../../src/types/index.js'
import { StandardParametersKey } from '../../../../src/types/ocpp/Configuration.js'
import { Constants } from '../../../../src/utils/index.js'
-import { standardCleanup } from '../../../../tests/helpers/TestLifecycleHelpers.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
import {
-import { CircularBuffer } from 'mnemonist'
/**
* @file Tests for MessageChannelUtils
* @description Unit tests for charging station worker message builders and performance statistics conversion
*/
+
+import { CircularBuffer } from 'mnemonist'
import assert from 'node:assert/strict'
import { afterEach, describe, it } from 'node:test'
-import { hoursToMilliseconds, hoursToSeconds } from 'date-fns'
-import { CircularBuffer } from 'mnemonist'
/**
* @file Tests for Utils
* @description Unit tests for general utility functions
*/
+
+import { hoursToMilliseconds, hoursToSeconds } from 'date-fns'
+import { CircularBuffer } from 'mnemonist'
import assert from 'node:assert/strict'
import { randomInt } from 'node:crypto'
import process from 'node:process'