From 75cb24c62ff4aad77e77cc3306003454cefa29e9 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 27 Mar 2026 21:15:44 +0100 Subject: [PATCH] refactor: align variable naming and remove non-null assertions in tests and UI MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Apply the same naming conventions established in src/ to tests/ and ui/: - connector → connectorStatus for ConnectorStatus values (48 instances) - Stats → Statistics: heartbeatStats, authStats, rateLimitStats (3) - Config → Configuration: atgConfig, originalConfig, updatedConfig, singleServerConfig, multiServerConfig (7) - Remove 11 eslint-disable no-non-null-assertion suppressions in tests/ and ui/web/src/ with proper null guards --- .../ChargingStation-Connectors.test.ts | 18 +- ...OCPP16IncomingRequestService-Reset.test.ts | 16 +- .../OCPP16Integration-Reservations.test.ts | 10 +- .../OCPP16Integration-Transactions.test.ts | 14 +- ...OCPP16ResponseService-Transactions.test.ts | 80 ++-- ...OCPP20IncomingRequestService-Reset.test.ts | 54 ++- ...mingRequestService-UnlockConnector.test.ts | 6 +- ...20ResponseService-TransactionEvent.test.ts | 6 +- ...CPP20ServiceUtils-TransactionEvent.test.ts | 116 ++--- .../OCPPServiceUtils-StopTransaction.test.ts | 48 +- .../OCPPServiceUtils-connectorStatus.test.ts | 26 +- tests/helpers/OCPPAuthIntegrationTest.ts | 16 +- tests/helpers/TestLifecycleHelpers.ts | 44 +- tests/ocpp2-e2e-test-plan.md | 414 ++++++++++++++++++ .../ChargingStationConfigurationUtils.test.ts | 24 +- tests/utils/MessageChannelUtils.test.ts | 8 +- ui/web/src/composables/UIClient.ts | 6 +- ui/web/tests/unit/CSConnector.test.ts | 15 +- .../tests/unit/ChargingStationsView.test.ts | 28 +- 19 files changed, 693 insertions(+), 256 deletions(-) create mode 100644 tests/ocpp2-e2e-test-plan.md diff --git a/tests/charging-station/ChargingStation-Connectors.test.ts b/tests/charging-station/ChargingStation-Connectors.test.ts index e90c2cc3..98e65e56 100644 --- a/tests/charging-station/ChargingStation-Connectors.test.ts +++ b/tests/charging-station/ChargingStation-Connectors.test.ts @@ -372,10 +372,11 @@ await describe('ChargingStation Connector and EVSE State', async () => { assert.strictEqual(station.inPendingState(), true) // Act - transition from PENDING to ACCEPTED - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - station.bootNotificationResponse!.status = RegistrationStatusEnumType.ACCEPTED - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - station.bootNotificationResponse!.currentTime = new Date() + if (station.bootNotificationResponse == null) { + throw new Error('Expected bootNotificationResponse to be defined') + } + station.bootNotificationResponse.status = RegistrationStatusEnumType.ACCEPTED + station.bootNotificationResponse.currentTime = new Date() // Assert assert.strictEqual(station.inAcceptedState(), true) @@ -391,10 +392,11 @@ await describe('ChargingStation Connector and EVSE State', async () => { assert.strictEqual(station.inPendingState(), true) // Act - transition from PENDING to REJECTED - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - station.bootNotificationResponse!.status = RegistrationStatusEnumType.REJECTED - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - station.bootNotificationResponse!.currentTime = new Date() + if (station.bootNotificationResponse == null) { + throw new Error('Expected bootNotificationResponse to be defined') + } + station.bootNotificationResponse.status = RegistrationStatusEnumType.REJECTED + station.bootNotificationResponse.currentTime = new Date() // Assert assert.strictEqual(station.inRejectedState(), true) diff --git a/tests/charging-station/ocpp/1.6/OCPP16IncomingRequestService-Reset.test.ts b/tests/charging-station/ocpp/1.6/OCPP16IncomingRequestService-Reset.test.ts index 77f75214..191983aa 100644 --- a/tests/charging-station/ocpp/1.6/OCPP16IncomingRequestService-Reset.test.ts +++ b/tests/charging-station/ocpp/1.6/OCPP16IncomingRequestService-Reset.test.ts @@ -70,10 +70,10 @@ await describe('OCPP16IncomingRequestService — Reset', async () => { // Arrange const { testableService } = testContext const station = ResetFixtures.createStandardStation(1) - const connector = station.getConnectorStatus(1) - if (connector != null) { - connector.transactionStarted = true - connector.transactionId = 1 + const connectorStatus = station.getConnectorStatus(1) + if (connectorStatus != null) { + connectorStatus.transactionStarted = true + connectorStatus.transactionId = 1 } const resetRequest: ResetRequest = { @@ -95,10 +95,10 @@ await describe('OCPP16IncomingRequestService — Reset', async () => { // Arrange const { testableService } = testContext const station = ResetFixtures.createStandardStation(1) - const connector = station.getConnectorStatus(1) - if (connector != null) { - connector.transactionStarted = true - connector.transactionId = 1 + const connectorStatus = station.getConnectorStatus(1) + if (connectorStatus != null) { + connectorStatus.transactionStarted = true + connectorStatus.transactionId = 1 } const resetRequest: ResetRequest = { diff --git a/tests/charging-station/ocpp/1.6/OCPP16Integration-Reservations.test.ts b/tests/charging-station/ocpp/1.6/OCPP16Integration-Reservations.test.ts index 7f33970f..455e05f7 100644 --- a/tests/charging-station/ocpp/1.6/OCPP16Integration-Reservations.test.ts +++ b/tests/charging-station/ocpp/1.6/OCPP16Integration-Reservations.test.ts @@ -284,15 +284,15 @@ await describe('OCPP16 Integration — Reservation Flow', async () => { assert.strictEqual(cancelResponse.status, GenericStatus.Rejected) // Assert — original reservation still intact - const connector = station.getConnectorStatus(1) - if (connector == null) { + const connectorStatus = station.getConnectorStatus(1) + if (connectorStatus == null) { assert.fail('Expected connector to be defined') } - if (connector.reservation == null) { + if (connectorStatus.reservation == null) { assert.fail('Expected reservation to be defined') } - assert.strictEqual(connector.reservation.reservationId, 500) - assert.strictEqual(connector.reservation.idTag, 'TAG-KEEP') + assert.strictEqual(connectorStatus.reservation.reservationId, 500) + assert.strictEqual(connectorStatus.reservation.idTag, 'TAG-KEEP') }) }) }) diff --git a/tests/charging-station/ocpp/1.6/OCPP16Integration-Transactions.test.ts b/tests/charging-station/ocpp/1.6/OCPP16Integration-Transactions.test.ts index f03c1e29..bac5fe3e 100644 --- a/tests/charging-station/ocpp/1.6/OCPP16Integration-Transactions.test.ts +++ b/tests/charging-station/ocpp/1.6/OCPP16Integration-Transactions.test.ts @@ -93,9 +93,9 @@ function createIntegrationContext (): { // Add MeterValues template required by buildTransactionBeginMeterValue for (const [connectorId] of station.connectors) { if (connectorId > 0) { - const connector = station.getConnectorStatus(connectorId) - if (connector != null) { - connector.MeterValues = [{ unit: OCPP16MeterValueUnit.WATT_HOUR, value: '0' }] + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus != null) { + connectorStatus.MeterValues = [{ unit: OCPP16MeterValueUnit.WATT_HOUR, value: '0' }] } } } @@ -282,12 +282,12 @@ await describe('OCPP16 Integration — Transaction Lifecycle', async () => { ) // Assert: connector should be reset, no active transaction - const connector = station.getConnectorStatus(connectorId) - if (connector == null) { + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus == null) { assert.fail('Expected connector to be defined') } - assert.strictEqual(connector.transactionStarted, false) - assert.strictEqual(connector.transactionId, undefined) + assert.strictEqual(connectorStatus.transactionStarted, false) + assert.strictEqual(connectorStatus.transactionId, undefined) }) // ─── State consistency ─────────────────────────────────────────────── diff --git a/tests/charging-station/ocpp/1.6/OCPP16ResponseService-Transactions.test.ts b/tests/charging-station/ocpp/1.6/OCPP16ResponseService-Transactions.test.ts index ae76e763..61daa7d5 100644 --- a/tests/charging-station/ocpp/1.6/OCPP16ResponseService-Transactions.test.ts +++ b/tests/charging-station/ocpp/1.6/OCPP16ResponseService-Transactions.test.ts @@ -52,9 +52,9 @@ await describe('OCPP16ResponseService — StartTransaction and StopTransaction', // Add MeterValues template required by buildTransactionBeginMeterValue for (const [connectorId] of station.connectors) { if (connectorId > 0) { - const connector = station.getConnectorStatus(connectorId) - if (connector != null) { - connector.MeterValues = [{ unit: OCPP16MeterValueUnit.WATT_HOUR, value: '0' }] + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus != null) { + connectorStatus.MeterValues = [{ unit: OCPP16MeterValueUnit.WATT_HOUR, value: '0' }] } } } @@ -92,14 +92,14 @@ await describe('OCPP16ResponseService — StartTransaction and StopTransaction', ) // Assert - const connector = station.getConnectorStatus(connectorId) - if (connector == null) { + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus == null) { assert.fail('Expected connector to be defined') } - assert.strictEqual(connector.transactionId, transactionId) - assert.strictEqual(connector.transactionStarted, true) - assert.strictEqual(connector.transactionIdTag, 'TEST-TAG-001') - assert.strictEqual(connector.transactionEnergyActiveImportRegisterValue, 0) + assert.strictEqual(connectorStatus.transactionId, transactionId) + assert.strictEqual(connectorStatus.transactionStarted, true) + assert.strictEqual(connectorStatus.transactionIdTag, 'TEST-TAG-001') + assert.strictEqual(connectorStatus.transactionEnergyActiveImportRegisterValue, 0) }) // @spec §5.14 — TC_004_CS @@ -126,12 +126,12 @@ await describe('OCPP16ResponseService — StartTransaction and StopTransaction', ) // Assert — connector should be reset (no transactionId) - const connector = station.getConnectorStatus(connectorId) - if (connector == null) { + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus == null) { assert.fail('Expected connector to be defined') } - assert.strictEqual(connector.transactionStarted, false) - assert.strictEqual(connector.transactionId, undefined) + assert.strictEqual(connectorStatus.transactionStarted, false) + assert.strictEqual(connectorStatus.transactionId, undefined) }) // @spec §5.14 — TC_010_CS @@ -139,9 +139,9 @@ await describe('OCPP16ResponseService — StartTransaction and StopTransaction', // Arrange const connectorId = 1 const reservationId = 5 - const connector = station.getConnectorStatus(connectorId) - if (connector != null) { - connector.reservation = { + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus != null) { + connectorStatus.reservation = { connectorId, expiryDate: new Date(Date.now() + 3600000), idTag: 'TEST-TAG-001', @@ -202,12 +202,12 @@ await describe('OCPP16ResponseService — StartTransaction and StopTransaction', ) // Assert - const connector = station.getConnectorStatus(connectorId) - if (connector == null) { + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus == null) { assert.fail('Expected connector to be defined') } - assert.strictEqual(connector.transactionStarted, true) - assert.deepStrictEqual(connector.transactionStart, requestTimestamp) + assert.strictEqual(connectorStatus.transactionStarted, true) + assert.deepStrictEqual(connectorStatus.transactionStart, requestTimestamp) }) await it('should reset connector on rejected with Invalid status', async () => { @@ -233,13 +233,13 @@ await describe('OCPP16ResponseService — StartTransaction and StopTransaction', ) // Assert — connector should be reset - const connector = station.getConnectorStatus(connectorId) - if (connector == null) { + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus == null) { assert.fail('Expected connector to be defined') } - assert.strictEqual(connector.transactionStarted, false) - assert.strictEqual(connector.transactionId, undefined) - assert.strictEqual(connector.transactionIdTag, undefined) + assert.strictEqual(connectorStatus.transactionStarted, false) + assert.strictEqual(connectorStatus.transactionId, undefined) + assert.strictEqual(connectorStatus.transactionIdTag, undefined) }) }) @@ -268,12 +268,12 @@ await describe('OCPP16ResponseService — StartTransaction and StopTransaction', ) // Assert — connector should be reset after stop - const connector = station.getConnectorStatus(1) - if (connector == null) { + const connectorStatus = station.getConnectorStatus(1) + if (connectorStatus == null) { assert.fail('Expected connector to be defined') } - assert.strictEqual(connector.transactionStarted, false) - assert.strictEqual(connector.transactionId, undefined) + assert.strictEqual(connectorStatus.transactionStarted, false) + assert.strictEqual(connectorStatus.transactionId, undefined) }) // @spec §5.16 — TC_072_CS @@ -296,12 +296,12 @@ await describe('OCPP16ResponseService — StartTransaction and StopTransaction', ) // Assert — connector should still be reset - const connector = station.getConnectorStatus(1) - if (connector == null) { + const connectorStatus = station.getConnectorStatus(1) + if (connectorStatus == null) { assert.fail('Expected connector to be defined') } - assert.strictEqual(connector.transactionStarted, false) - assert.strictEqual(connector.transactionId, undefined) + assert.strictEqual(connectorStatus.transactionStarted, false) + assert.strictEqual(connectorStatus.transactionId, undefined) }) await it('should clear transactionIdTag and energy register after stop', async () => { @@ -329,15 +329,15 @@ await describe('OCPP16ResponseService — StartTransaction and StopTransaction', ) // Assert - const connector = station.getConnectorStatus(1) - if (connector == null) { + const connectorStatus = station.getConnectorStatus(1) + if (connectorStatus == null) { assert.fail('Expected connector to be defined') } - assert.strictEqual(connector.transactionStarted, false) - assert.strictEqual(connector.transactionId, undefined) - assert.strictEqual(connector.transactionIdTag, undefined) - assert.strictEqual(connector.transactionEnergyActiveImportRegisterValue, 0) - assert.strictEqual(connector.transactionRemoteStarted, false) + assert.strictEqual(connectorStatus.transactionStarted, false) + assert.strictEqual(connectorStatus.transactionId, undefined) + assert.strictEqual(connectorStatus.transactionIdTag, undefined) + assert.strictEqual(connectorStatus.transactionEnergyActiveImportRegisterValue, 0) + assert.strictEqual(connectorStatus.transactionRemoteStarted, false) }) await it('should not throw when transactionId does not match any connector', async () => { diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts index a888f5bd..6ce2be69 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts @@ -306,8 +306,10 @@ await describe('B11 & B12 - Reset', async () => { await it('should return Rejected/FwUpdateInProgress when firmware is Downloading', async () => { const station = createTestStation() // Firmware check runs before OnIdle idle-state logic — always returns Rejected - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Object.assign(station.stationInfo!, { + if (station.stationInfo == null) { + throw new Error('Expected stationInfo to be defined') + } + Object.assign(station.stationInfo, { firmwareStatus: FirmwareStatus.Downloading, }) @@ -328,8 +330,10 @@ await describe('B11 & B12 - Reset', async () => { await it('should return Rejected/FwUpdateInProgress when firmware is Downloaded', async () => { const station = createTestStation() // Firmware check runs before OnIdle idle-state logic — always returns Rejected - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Object.assign(station.stationInfo!, { + if (station.stationInfo == null) { + throw new Error('Expected stationInfo to be defined') + } + Object.assign(station.stationInfo, { firmwareStatus: FirmwareStatus.Downloaded, }) @@ -350,8 +354,10 @@ await describe('B11 & B12 - Reset', async () => { await it('should return Rejected/FwUpdateInProgress when firmware is Installing', async () => { const station = createTestStation() // Firmware check runs before OnIdle idle-state logic — always returns Rejected - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Object.assign(station.stationInfo!, { + if (station.stationInfo == null) { + throw new Error('Expected stationInfo to be defined') + } + Object.assign(station.stationInfo, { firmwareStatus: FirmwareStatus.Installing, }) @@ -372,8 +378,10 @@ await describe('B11 & B12 - Reset', async () => { await it('should return Accepted when firmware is Installed (complete)', async () => { const station = createTestStation() // Firmware status: Installed (complete) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Object.assign(station.stationInfo!, { + if (station.stationInfo == null) { + throw new Error('Expected stationInfo to be defined') + } + Object.assign(station.stationInfo, { firmwareStatus: FirmwareStatus.Installed, }) @@ -393,8 +401,10 @@ await describe('B11 & B12 - Reset', async () => { await it('should return Accepted when firmware status is Idle', async () => { const station = createTestStation() // Firmware status: Idle - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Object.assign(station.stationInfo!, { + if (station.stationInfo == null) { + throw new Error('Expected stationInfo to be defined') + } + Object.assign(station.stationInfo, { firmwareStatus: FirmwareStatus.Idle, }) @@ -429,9 +439,9 @@ await describe('B11 & B12 - Reset', async () => { const evse: EvseStatus | undefined = station.evses.get(1) if (evse) { const connectorId = [...evse.connectors.keys()][0] - const connector = evse.connectors.get(connectorId) - if (connector) { - connector.reservation = mockReservation as Reservation + const connectorStatus = evse.connectors.get(connectorId) + if (connectorStatus) { + connectorStatus.reservation = mockReservation as Reservation } } @@ -462,9 +472,9 @@ await describe('B11 & B12 - Reset', async () => { const evse: EvseStatus | undefined = station.evses.get(1) if (evse) { const connectorId = [...evse.connectors.keys()][0] - const connector = evse.connectors.get(connectorId) - if (connector) { - connector.reservation = mockReservation as Reservation + const connectorStatus = evse.connectors.get(connectorId) + if (connectorStatus) { + connectorStatus.reservation = mockReservation as Reservation } } @@ -508,8 +518,10 @@ await describe('B11 & B12 - Reset', async () => { // No transactions station.getNumberOfRunningTransactions = () => 0 // No firmware update - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Object.assign(station.stationInfo!, { + if (station.stationInfo == null) { + throw new Error('Expected stationInfo to be defined') + } + Object.assign(station.stationInfo, { firmwareStatus: FirmwareStatus.Idle, }) // No reservations (default) @@ -541,9 +553,9 @@ await describe('B11 & B12 - Reset', async () => { const evse: EvseStatus | undefined = station.evses.get(1) if (evse) { const connectorId = [...evse.connectors.keys()][0] - const connector = evse.connectors.get(connectorId) - if (connector) { - connector.reservation = mockReservation as Reservation + const connectorStatus = evse.connectors.get(connectorId) + if (connectorStatus) { + connectorStatus.reservation = mockReservation as Reservation } } diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UnlockConnector.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UnlockConnector.test.ts index af71d2b1..ea86a334 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UnlockConnector.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UnlockConnector.test.ts @@ -138,9 +138,9 @@ await describe('F05 - UnlockConnector', async () => { const { mockStation } = createUnlockConnectorStation() const evseStatus = mockStation.evses.get(1) - const connector = evseStatus?.connectors.get(1) - if (connector != null) { - connector.transactionId = 'tx-001' + const connectorStatus = evseStatus?.connectors.get(1) + if (connectorStatus != null) { + connectorStatus.transactionId = 'tx-001' } const request: OCPP20UnlockConnectorRequest = { connectorId: 1, evseId: 1 } diff --git a/tests/charging-station/ocpp/2.0/OCPP20ResponseService-TransactionEvent.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ResponseService-TransactionEvent.test.ts index ee3edeca..b578be1b 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ResponseService-TransactionEvent.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ResponseService-TransactionEvent.test.ts @@ -94,9 +94,9 @@ await describe('D01 - TransactionEvent Response', async () => { // Set connector transactionId to the UUID string used in request payloads setupConnectorWithTransaction(station, 1, { transactionId: 100 }) // Override with UUID string so getConnectorIdByTransactionId can find it - const connector = station.getConnectorStatus(1) - if (connector != null) { - connector.transactionId = TEST_TRANSACTION_ID + const connectorStatus = station.getConnectorStatus(1) + if (connectorStatus != null) { + connectorStatus.transactionId = TEST_TRANSACTION_ID } const responseService = new OCPP20ResponseService() testable = createTestableResponseService(responseService) diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts index 26dfae65..c213fe7a 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts @@ -1419,9 +1419,9 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { afterEach(() => { for (let connectorId = 1; connectorId <= 3; connectorId++) { - const connector = mockStation.getConnectorStatus(connectorId) - if (connector != null) { - connector.transactionEventQueue = undefined + const connectorStatus = mockStation.getConnectorStatus(connectorId) + if (connectorStatus != null) { + connectorStatus.transactionEventQueue = undefined } } standardCleanup() @@ -1448,11 +1448,11 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { assert.strictEqual(response.idTokenInfo, undefined) - const connector = mockStation.getConnectorStatus(connectorId) - assert(connector != null) - assert(connector.transactionEventQueue != null) - assert.strictEqual(connector.transactionEventQueue.length, 1) - assert.strictEqual(connector.transactionEventQueue[0].seqNo, 0) + const connectorStatus = mockStation.getConnectorStatus(connectorId) + assert(connectorStatus != null) + assert(connectorStatus.transactionEventQueue != null) + assert.strictEqual(connectorStatus.transactionEventQueue.length, 1) + assert.strictEqual(connectorStatus.transactionEventQueue[0].seqNo, 0) }) await it('should queue multiple TransactionEvents in order when offline', async () => { @@ -1487,23 +1487,23 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { transactionId ) - const connector = mockStation.getConnectorStatus(connectorId) - assert.strictEqual(connector?.transactionEventQueue?.length, 3) + const connectorStatus = mockStation.getConnectorStatus(connectorId) + assert.strictEqual(connectorStatus?.transactionEventQueue?.length, 3) - assert.strictEqual(connector.transactionEventQueue[0].seqNo, 0) - assert.strictEqual(connector.transactionEventQueue[1].seqNo, 1) - assert.strictEqual(connector.transactionEventQueue[2].seqNo, 2) + assert.strictEqual(connectorStatus.transactionEventQueue[0].seqNo, 0) + assert.strictEqual(connectorStatus.transactionEventQueue[1].seqNo, 1) + assert.strictEqual(connectorStatus.transactionEventQueue[2].seqNo, 2) assert.ok( - connector.transactionEventQueue[0].request.eventType, + connectorStatus.transactionEventQueue[0].request.eventType, OCPP20TransactionEventEnumType.Started ) assert.strictEqual( - connector.transactionEventQueue[1].request.eventType, + connectorStatus.transactionEventQueue[1].request.eventType, OCPP20TransactionEventEnumType.Updated ) assert.strictEqual( - connector.transactionEventQueue[2].request.eventType, + connectorStatus.transactionEventQueue[2].request.eventType, OCPP20TransactionEventEnumType.Ended ) }) @@ -1547,11 +1547,11 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { transactionId ) - const connector = mockStation.getConnectorStatus(connectorId) - assert.strictEqual(connector?.transactionEventQueue?.length, 2) + const connectorStatus = mockStation.getConnectorStatus(connectorId) + assert.strictEqual(connectorStatus?.transactionEventQueue?.length, 2) // Online path with mock doesn't call buildTransactionEvent, so seqNo starts from 0 - assert.strictEqual(connector.transactionEventQueue[0].seqNo, 0) - assert.strictEqual(connector.transactionEventQueue[1].seqNo, 1) + assert.strictEqual(connectorStatus.transactionEventQueue[0].seqNo, 0) + assert.strictEqual(connectorStatus.transactionEventQueue[1].seqNo, 1) }) await it('should include timestamp in queued events', async () => { @@ -1571,13 +1571,15 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { ) const afterQueue = new Date() - const connector = mockStation.getConnectorStatus(connectorId) - assert.ok(connector?.transactionEventQueue?.[0]?.timestamp instanceof Date) + const connectorStatus = mockStation.getConnectorStatus(connectorId) + assert.ok(connectorStatus?.transactionEventQueue?.[0]?.timestamp instanceof Date) assert.strictEqual( - connector.transactionEventQueue[0].timestamp.getTime() >= beforeQueue.getTime(), + connectorStatus.transactionEventQueue[0].timestamp.getTime() >= beforeQueue.getTime(), true ) - assert.ok(connector.transactionEventQueue[0].timestamp.getTime() <= afterQueue.getTime()) + assert.ok( + connectorStatus.transactionEventQueue[0].timestamp.getTime() <= afterQueue.getTime() + ) }) await it('should set offline flag to true when queueing transaction event while station is offline', async () => { @@ -1595,10 +1597,10 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { transactionId ) - const connector = mockStation.getConnectorStatus(connectorId) - assert.ok(connector?.transactionEventQueue != null) - assert.strictEqual(connector.transactionEventQueue.length, 1) - assert.strictEqual(connector.transactionEventQueue[0].request.offline, true) + const connectorStatus = mockStation.getConnectorStatus(connectorId) + assert.ok(connectorStatus?.transactionEventQueue != null) + assert.strictEqual(connectorStatus.transactionEventQueue.length, 1) + assert.strictEqual(connectorStatus.transactionEventQueue[0].request.offline, true) }) }) @@ -1660,20 +1662,20 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { transactionId ) - const connector = mockStation.getConnectorStatus(connectorId) - assert(connector != null) - connector.transactionStarted = true - connector.transactionId = transactionId - connector.locked = true - assert.strictEqual(connector.transactionEventQueue?.length, 2) + const connectorStatus = mockStation.getConnectorStatus(connectorId) + assert(connectorStatus != null) + connectorStatus.transactionStarted = true + connectorStatus.transactionId = transactionId + connectorStatus.locked = true + assert.strictEqual(connectorStatus.transactionEventQueue?.length, 2) setOnline(true) await OCPP20ServiceUtils.sendQueuedTransactionEvents(mockStation, connectorId) - assert.strictEqual(connector.transactionEventQueue.length, 0) - assert.strictEqual(connector.transactionStarted, false) - assert.strictEqual(connector.transactionId, undefined) - assert.strictEqual(connector.locked, false) + assert.strictEqual(connectorStatus.transactionEventQueue.length, 0) + assert.strictEqual(connectorStatus.transactionStarted, false) + assert.strictEqual(connectorStatus.transactionId, undefined) + assert.strictEqual(connectorStatus.locked, false) }) await it('should preserve FIFO order when draining queue', async () => { @@ -1737,9 +1739,9 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { await it('should handle null queue gracefully', async () => { const connectorId = 1 - const connector = mockStation.getConnectorStatus(connectorId) - assert(connector != null) - connector.transactionEventQueue = undefined + const connectorStatus = mockStation.getConnectorStatus(connectorId) + assert(connectorStatus != null) + connectorStatus.transactionEventQueue = undefined await assert.doesNotReject( OCPP20ServiceUtils.sendQueuedTransactionEvents(mockStation, connectorId) @@ -1996,10 +1998,10 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { afterEach(() => { // Clean up any running timers for (let connectorId = 1; connectorId <= 3; connectorId++) { - const connector = mockStation.getConnectorStatus(connectorId) - if (connector?.transactionMeterValuesSetInterval != null) { - clearInterval(connector.transactionMeterValuesSetInterval) - connector.transactionMeterValuesSetInterval = undefined + const connectorStatus = mockStation.getConnectorStatus(connectorId) + if (connectorStatus?.transactionMeterValuesSetInterval != null) { + clearInterval(connectorStatus.transactionMeterValuesSetInterval) + connectorStatus.transactionMeterValuesSetInterval = undefined } } standardCleanup() @@ -2018,8 +2020,8 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { await startPeriodicMeterValues(ocpp16Station, 1, 60000) - const connector = ocpp16Station.getConnectorStatus(1) - assert.strictEqual(connector?.transactionMeterValuesSetInterval, undefined) + const connectorStatus = ocpp16Station.getConnectorStatus(1) + assert.strictEqual(connectorStatus?.transactionMeterValuesSetInterval, undefined) }) }) @@ -2027,23 +2029,23 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { const connectorId = 1 // Simulate startTxUpdatedInterval with zero interval - const connector = mockStation.getConnectorStatus(connectorId) - assert.notStrictEqual(connector, undefined) - assert(connector != null) + const connectorStatus = mockStation.getConnectorStatus(connectorId) + assert.notStrictEqual(connectorStatus, undefined) + assert(connectorStatus != null) // Zero interval should not start timer // This is verified by the implementation logging debug message - assert.strictEqual(connector.transactionMeterValuesSetInterval, undefined) + assert.strictEqual(connectorStatus.transactionMeterValuesSetInterval, undefined) }) await it('should not start timer when interval is negative', () => { const connectorId = 1 - const connector = mockStation.getConnectorStatus(connectorId) - assert.notStrictEqual(connector, undefined) - assert(connector != null) + const connectorStatus = mockStation.getConnectorStatus(connectorId) + assert.notStrictEqual(connectorStatus, undefined) + assert(connectorStatus != null) // Negative interval should not start timer - assert.strictEqual(connector.transactionMeterValuesSetInterval, undefined) + assert.strictEqual(connectorStatus.transactionMeterValuesSetInterval, undefined) }) await it('should handle non-existent connector gracefully', () => { @@ -2117,8 +2119,8 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => { } // Verify sequence numbers are continuous: 0, 1, 2, 3 - const connector = mockStation.getConnectorStatus(connectorId) - assert.strictEqual(connector?.transactionSeqNo, 3) + const connectorStatus = mockStation.getConnectorStatus(connectorId) + assert.strictEqual(connectorStatus?.transactionSeqNo, 3) }) await it('should maintain correct eventType (Updated) for periodic events', async () => { diff --git a/tests/charging-station/ocpp/OCPPServiceUtils-StopTransaction.test.ts b/tests/charging-station/ocpp/OCPPServiceUtils-StopTransaction.test.ts index fe323936..ffa3dc92 100644 --- a/tests/charging-station/ocpp/OCPPServiceUtils-StopTransaction.test.ts +++ b/tests/charging-station/ocpp/OCPPServiceUtils-StopTransaction.test.ts @@ -53,14 +53,14 @@ function setupPendingTransaction ( connectorId: number, txId: string ): void { - const connector = station.getConnectorStatus(connectorId) - if (connector == null) { + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus == null) { throw new Error(`Connector ${String(connectorId)} not found`) } - connector.transactionPending = true - connector.transactionStarted = false - connector.transactionId = txId - connector.transactionStart = new Date() + connectorStatus.transactionPending = true + connectorStatus.transactionStarted = false + connectorStatus.transactionId = txId + connectorStatus.transactionStart = new Date() } /** @@ -74,15 +74,15 @@ function setupTransaction ( connectorId: number, txId: number | string ): void { - const connector = station.getConnectorStatus(connectorId) - if (connector == null) { + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus == null) { throw new Error(`Connector ${String(connectorId)} not found`) } - connector.transactionStarted = true - connector.transactionId = txId - connector.transactionIdTag = `TAG-${String(txId)}` - connector.transactionStart = new Date() - connector.idTagAuthorized = true + connectorStatus.transactionStarted = true + connectorStatus.transactionId = txId + connectorStatus.transactionIdTag = `TAG-${String(txId)}` + connectorStatus.transactionStart = new Date() + connectorStatus.idTagAuthorized = true } await describe('OCPPServiceUtils — stop transaction functions', async () => { @@ -262,15 +262,15 @@ await describe('OCPPServiceUtils — stop transaction functions', async () => { requestHandler.mock.mockImplementation(async (..._args: unknown[]) => Promise.resolve({ idTokenInfo: { status: 'Accepted' } }) ) - const connector = station.getConnectorStatus(1) - assert.notStrictEqual(connector, undefined) - assert(connector != null) - delete connector.transactionId + const connectorStatus = station.getConnectorStatus(1) + assert.notStrictEqual(connectorStatus, undefined) + assert(connectorStatus != null) + delete connectorStatus.transactionId await startTransactionOnConnector(station, 1) - assert.notStrictEqual(connector.transactionId, undefined) - assert.strictEqual(typeof connector.transactionId, 'string') + assert.notStrictEqual(connectorStatus.transactionId, undefined) + assert.strictEqual(typeof connectorStatus.transactionId, 'string') }) }) @@ -287,10 +287,10 @@ await describe('OCPPServiceUtils — stop transaction functions', async () => { ocppVersion: OCPPVersion.VERSION_20, }) requestHandler.mock.mockImplementation(async (..._args: unknown[]) => Promise.resolve({})) - const connector = station.getConnectorStatus(1) - assert.notStrictEqual(connector, undefined) - assert(connector != null) - connector.transactionEventQueue = [ + const connectorStatus = station.getConnectorStatus(1) + assert.notStrictEqual(connectorStatus, undefined) + assert(connectorStatus != null) + connectorStatus.transactionEventQueue = [ { request: { eventType: 'Updated', @@ -307,7 +307,7 @@ await describe('OCPPServiceUtils — stop transaction functions', async () => { await flushQueuedTransactionMessages(station) - assert.strictEqual(connector.transactionEventQueue.length, 0) + assert.strictEqual(connectorStatus.transactionEventQueue.length, 0) }) }) diff --git a/tests/charging-station/ocpp/OCPPServiceUtils-connectorStatus.test.ts b/tests/charging-station/ocpp/OCPPServiceUtils-connectorStatus.test.ts index 94ab7111..7a6f0802 100644 --- a/tests/charging-station/ocpp/OCPPServiceUtils-connectorStatus.test.ts +++ b/tests/charging-station/ocpp/OCPPServiceUtils-connectorStatus.test.ts @@ -148,18 +148,18 @@ await describe('OCPPServiceUtils — connector status management', async () => { await it('should restore to Reserved when connector has reservation and is not Reserved', async () => { const { station } = createStationWithRequestHandler() - const connector = station.getConnectorStatus(1) - if (connector != null) { - connector.reservation = { + const connectorStatus = station.getConnectorStatus(1) + if (connectorStatus != null) { + connectorStatus.reservation = { connectorId: 1, expiryDate: new Date().toISOString(), idTag: 'TEST-TAG', reservationId: 1, } as unknown as Reservation - connector.status = ConnectorStatusEnum.Occupied + connectorStatus.status = ConnectorStatusEnum.Occupied } - await restoreConnectorStatus(station, 1, connector) + await restoreConnectorStatus(station, 1, connectorStatus) assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Reserved) }) @@ -167,12 +167,12 @@ await describe('OCPPServiceUtils — connector status management', async () => { await it('should restore to Available when connector has no reservation and is not Available', async () => { const { station } = createStationWithRequestHandler() - const connector = station.getConnectorStatus(1) - if (connector != null) { - connector.status = ConnectorStatusEnum.Occupied + const connectorStatus = station.getConnectorStatus(1) + if (connectorStatus != null) { + connectorStatus.status = ConnectorStatusEnum.Occupied } - await restoreConnectorStatus(station, 1, connector) + await restoreConnectorStatus(station, 1, connectorStatus) assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Available) }) @@ -180,12 +180,12 @@ await describe('OCPPServiceUtils — connector status management', async () => { await it('should not change status when connector is already Available with no reservation', async () => { const { requestHandler, station } = createStationWithRequestHandler() - const connector = station.getConnectorStatus(1) - if (connector != null) { - connector.status = ConnectorStatusEnum.Available + const connectorStatus = station.getConnectorStatus(1) + if (connectorStatus != null) { + connectorStatus.status = ConnectorStatusEnum.Available } - await restoreConnectorStatus(station, 1, connector) + await restoreConnectorStatus(station, 1, connectorStatus) assert.strictEqual(requestHandler.mock.calls.length, 0) assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Available) diff --git a/tests/helpers/OCPPAuthIntegrationTest.ts b/tests/helpers/OCPPAuthIntegrationTest.ts index 47ad76df..d873e234 100644 --- a/tests/helpers/OCPPAuthIntegrationTest.ts +++ b/tests/helpers/OCPPAuthIntegrationTest.ts @@ -126,7 +126,7 @@ export class OCPPAuthIntegrationTest { } private testConfigurationManagement (): void { - const originalConfig = this.authService.getConfiguration() + const originalConfiguration = this.authService.getConfiguration() const updates: Partial = { authorizationTimeout: 60, @@ -136,21 +136,21 @@ export class OCPPAuthIntegrationTest { this.authService.updateConfiguration(updates) - const updatedConfig = this.authService.getConfiguration() + const updatedConfiguration = this.authService.getConfiguration() - if (updatedConfig.authorizationTimeout !== 60) { + if (updatedConfiguration.authorizationTimeout !== 60) { throw new Error('Configuration update failed: authorizationTimeout') } - if (updatedConfig.localAuthListEnabled) { + if (updatedConfiguration.localAuthListEnabled) { throw new Error('Configuration update failed: localAuthListEnabled') } - if (updatedConfig.maxCacheEntries !== 2000) { + if (updatedConfiguration.maxCacheEntries !== 2000) { throw new Error('Configuration update failed: maxCacheEntries') } - this.authService.updateConfiguration(originalConfig) + this.authService.updateConfiguration(originalConfiguration) logger.debug(`${this.chargingStation.logPrefix()} Configuration management test completed`) } @@ -256,8 +256,8 @@ export class OCPPAuthIntegrationTest { throw new Error('Invalid statistics object') } - const authStats = this.authService.getAuthenticationStats() - if (!Array.isArray(authStats.availableStrategies)) { + const authStatistics = this.authService.getAuthenticationStats() + if (!Array.isArray(authStatistics.availableStrategies)) { throw new Error('Invalid authentication statistics') } diff --git a/tests/helpers/TestLifecycleHelpers.ts b/tests/helpers/TestLifecycleHelpers.ts index e12c807f..92362823 100644 --- a/tests/helpers/TestLifecycleHelpers.ts +++ b/tests/helpers/TestLifecycleHelpers.ts @@ -98,24 +98,24 @@ interface TimerTestContext { * @param connectorId - Connector to clear */ export function clearConnectorTransaction (station: ChargingStation, connectorId: number): void { - const connector = station.getConnectorStatus(connectorId) - if (connector == null) { + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus == null) { return } - connector.transactionStarted = false - connector.transactionId = undefined - connector.transactionIdTag = undefined - connector.transactionEnergyActiveImportRegisterValue = 0 - connector.transactionRemoteStarted = false - connector.transactionStart = undefined - connector.idTagAuthorized = false - connector.idTagLocalAuthorized = false + connectorStatus.transactionStarted = false + connectorStatus.transactionId = undefined + connectorStatus.transactionIdTag = undefined + connectorStatus.transactionEnergyActiveImportRegisterValue = 0 + connectorStatus.transactionRemoteStarted = false + connectorStatus.transactionStart = undefined + connectorStatus.idTagAuthorized = false + connectorStatus.idTagLocalAuthorized = false // Clear any transaction interval - if (connector.transactionMeterValuesSetInterval != null) { - clearInterval(connector.transactionMeterValuesSetInterval) - connector.transactionMeterValuesSetInterval = undefined + if (connectorStatus.transactionMeterValuesSetInterval != null) { + clearInterval(connectorStatus.transactionMeterValuesSetInterval) + connectorStatus.transactionMeterValuesSetInterval = undefined } } @@ -267,18 +267,18 @@ export function setupConnectorWithTransaction ( transactionId: number } ): void { - const connector = station.getConnectorStatus(connectorId) - if (connector == null) { + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus == null) { throw new Error(`Connector ${String(connectorId)} not found`) } - connector.transactionStarted = true - connector.transactionId = options.transactionId - connector.transactionIdTag = options.idTag ?? `TAG-${String(options.transactionId)}` - connector.transactionEnergyActiveImportRegisterValue = options.energyImport ?? 0 - connector.transactionRemoteStarted = options.remoteStarted ?? false - connector.transactionStart = new Date() - connector.idTagAuthorized = true + connectorStatus.transactionStarted = true + connectorStatus.transactionId = options.transactionId + connectorStatus.transactionIdTag = options.idTag ?? `TAG-${String(options.transactionId)}` + connectorStatus.transactionEnergyActiveImportRegisterValue = options.energyImport ?? 0 + connectorStatus.transactionRemoteStarted = options.remoteStarted ?? false + connectorStatus.transactionStart = new Date() + connectorStatus.idTagAuthorized = true } /** diff --git a/tests/ocpp2-e2e-test-plan.md b/tests/ocpp2-e2e-test-plan.md new file mode 100644 index 00000000..220285cc --- /dev/null +++ b/tests/ocpp2-e2e-test-plan.md @@ -0,0 +1,414 @@ +# OCPP 2.0.1 End-to-End Test Plan + +E2E test scenarios for the charging station simulator's OCPP 2.0.1 stack. +Executed via MCP tools against the mock OCPP server (`tests/ocpp-server/`). + +## Conventions + +| Item | Value | +| ---------------- | --------------------------------------------------------------- | +| Mock server | `cd tests/ocpp-server && poetry run python server.py [OPTIONS]` | +| Station template | `keba-ocpp2.station-template.json` | +| Station ID | `CS-KEBA-OCPP2-00001` | +| EVSE / Connector | 1 / 1 | +| Supervision URL | `ws://localhost:9000` | + +### Execution Rules + +- **Tester manages the mock server only** — start/stop/restart with options. +- All `--boot-status` and enum CLI values are **Title-Case** (`Accepted`, not `accepted`). + +### Reconnection + +The station auto-reconnects when the server restarts, with a **fixed 30s delay** (`reconnectExponentialDelay: false`, `ConnectionTimeOut: 30`). This means: + +- After a server restart, the station takes ~30s to reconnect (WebSocket close → sleep 30s → reopen). +- The station does NOT re-send `BootNotification` if it already has `bootNotificationResponse.status = Accepted` in cache. It connects silently. +- To force a fresh boot (e.g., to clear cached Inoperative state), use `stopChargingStation`/`startChargingStation` as a **setup step**, not as a test step. + +**To avoid the 30s reconnect delay between server restarts**, use this pattern: + +``` +1. Kill mock server +2. Start new mock server with new options +3. MCP: closeConnection (triggers CLOSE_NORMAL → resets retry count to 0) +4. MCP: openConnection (immediate reconnect, no 30s wait) +5. Wait ~5s for WebSocket handshake +6. Proceed with test +``` + +### Verification + +A test case **passes** when ALL of: + +1. MCP tool response: `"status": "success"` (no `responsesFailed`) +2. `readCombinedLog`: expected OCPP messages in correct order +3. `listChargingStations`: expected station/connector state +4. `readErrorLog`: no unexpected errors + +### Server Lifecycle + +Tests are grouped by server configuration to minimize restarts. +Within a group, tests execute sequentially without restart. +Between groups, use the close/open pattern above to avoid cumulative reconnect delays. + +--- + +## A — Security + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | --------------------------- | ------------------------------- | ----------------------------------------------------------- | --------------------------- | +| A03 | CS-initiated cert update | MCP `signCertificate` | Send CSR with `certificateType: ChargingStationCertificate` | Response `status: Accepted` | +| A04 | Security event notification | MCP `securityEventNotification` | Send `type: FirmwareUpdated` | Response empty (success) | + +### Server: `--boot-status Accepted --command CertificateSigned --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| --- | -------------------------- | -------------- | --------- | --------------------------------------------------------------------------------------------------------------------- | +| A02 | CSMS-initiated cert update | Server command | Wait ~15s | CertificateSigned received → Rejected (statusInfo.reasonCode: InternalError — no cert manager in keba-ocpp2 template) | + +--- + +## B — Provisioning + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | -------------------- | --------------------- | ---------------------------------- | ----------------------------------------------------------------------------- | +| B01 | Cold Boot — Accepted | Auto (server restart) | Restart server, wait for reconnect | BootNotification → Accepted, StatusNotification(Available), Heartbeat started | +| B04 | Offline reconnection | Server kill/restart | Kill server, wait 10s, restart | Station reconnects, re-sends BootNotification, returns to Available | + +### Server: `--boot-status-sequence Pending,Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | ---------------------------- | --------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------- | +| B02 | Cold Boot — Pending→Accepted | Auto (server restart) | Restart server, wait for 2 boot cycles | 1st BootNotification → Pending, station retries, 2nd → Accepted, StatusNotification(Available) | + +### Server: `--boot-status Rejected` + +| TC | Use Case | Via | Steps | Expected | +| --- | -------------------- | --------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| B03 | Cold Boot — Rejected | Auto (server restart) | Restart server, wait for reconnect | BootNotification → Rejected → 1 retry (registrationMaxRetries defaults to 0) → Rejected → "Registration failure" log. No StatusNotification sent. Station stays in Rejected state but can still retry BootNotification on next reconnection (B03.FR.06). | + +### Server: various `--command X --delay 5` + +| TC | Use Case | Server flags | Expected | +| ---- | ---------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| B05 | SetVariables | `--command SetVariables --set-variables "OCPPCommCtrlr.HeartbeatInterval=30"` | SetVariablesResponse with result status | +| B06 | GetVariables | `--command GetVariables --get-variables "ChargingStation.AvailabilityState"` | GetVariablesResponse with variable value | +| B07 | GetBaseReport | `--command GetBaseReport` | GetBaseReportResponse Accepted + NotifyReport sequence | +| B09 | SetNetworkProfile | `--command SetNetworkProfile` | Response Rejected (NoSecurityDowngrade per B09.FR.01) | +| B11 | Reset (no transaction) | `--command Reset` | Reset Accepted → StatusNotification(Unavailable) → close → re-boot → Available | +| B11b | Reset OnIdle (no active txn) | `--command Reset --reset-type OnIdle` | Reset Accepted → StatusNotification(Unavailable) → re-boot → Available (no transaction active, immediate reset) | + +--- + +## C — Authorization + +### Server: `--boot-status Accepted` (normal auth) + +| TC | Use Case | Via | Steps | Expected | +| --- | ------------------ | --------------- | --------------------------------------------------- | ------------------------------ | +| C01 | Authorize — normal | MCP `authorize` | `idToken: {idToken: "any_token", type: "ISO14443"}` | `idTokenInfo.status: Accepted` | + +### Server: `--boot-status Accepted --auth-mode whitelist --whitelist valid_token test_token` + +| TC | Use Case | Via | Steps | Expected | +| ------------- | --------------------------- | --------------- | ------------------------ | ------------------ | +| C01-WL-OK | Authorize — whitelisted | MCP `authorize` | `idToken: test_token` | `status: Accepted` | +| C01-WL-REJECT | Authorize — not whitelisted | MCP `authorize` | `idToken: unknown_token` | `status: Blocked` | + +### Server: `--boot-status Accepted --auth-mode blacklist --blacklist blocked_token` + +| TC | Use Case | Via | Steps | Expected | +| ------------- | --------------------------- | --------------- | ------------------------ | ------------------ | +| C01-BL-OK | Authorize — not blacklisted | MCP `authorize` | `idToken: good_token` | `status: Accepted` | +| C01-BL-REJECT | Authorize — blacklisted | MCP `authorize` | `idToken: blocked_token` | `status: Blocked` | + +### Server: `--boot-status Accepted --auth-mode rate_limit` + +| TC | Use Case | Via | Steps | Expected | +| ------ | ------------------------ | --------------- | --------- | ----------------------- | +| C01-RL | Authorize — rate limited | MCP `authorize` | Any token | `status: NotAtThisTime` | + +### Server: `--boot-status Accepted --offline` + +| TC | Use Case | Via | Steps | Expected | +| ----------- | --------------------------- | --------------- | --------- | ------------------------- | +| C01-OFFLINE | Authorize — network failure | MCP `authorize` | Any token | InternalError from server | + +### Server: `--boot-status Accepted --auth-group-id MyGroup --auth-cache-expiry 3600` + +| TC | Use Case | Via | Steps | Expected | +| --- | ------------------- | --------------- | --------- | -------------------------------------------------------------------------- | +| C09 | GroupId in response | MCP `authorize` | Any token | `idTokenInfo.groupIdToken.idToken: MyGroup`, `cacheExpiryDateTime` present | + +### Server: `--boot-status Accepted --command ClearCache --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| --- | ---------------- | -------------- | --------- | --------------------- | +| C11 | Clear auth cache | Server command | Wait ~15s | ClearCache → Accepted | + +--- + +## E — Transactions + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| ---------- | --------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| E01-ATG | Transaction lifecycle (ATG) | MCP `startAutomaticTransactionGenerator` → wait 30s → `stopAutomaticTransactionGenerator` | Wait for full cycle | Authorize → TransactionEvent.Started(seqNo=0) → Updated(seqNo=1+, MeterValues) → Ended(seqNo=N, stoppedReason=Local) | +| E01-DIRECT | TransactionEvent direct | MCP `transactionEvent` | Send Started → Updated → Ended with `transactionId: "mcp-test-001"` | All 3 accepted, seqNo sequential | + +### Server: `--boot-status Accepted --commands "RequestStartTransaction:15,RequestStopTransaction:45"` + +| TC | Use Case | Via | Steps | Expected | +| ------- | -------------------------- | --------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| F01+F03 | Remote Start → Remote Stop | Server commands | Wait ~60s for full cycle | RequestStartTransaction → Accepted → TransactionEvent.Started(RemoteStart) → MeterValues → RequestStopTransaction (real txn ID via tracking) → TransactionEvent.Ended(Remote) | + +### Server: `--boot-status Accepted --command RequestStopTransaction --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| ---- | --------------------------- | -------------- | --------- | ---------------------------------------------------------------------------------------------------------------------- | +| F03b | Remote Stop — no active txn | Server command | Wait ~15s | RequestStopTransaction with fallback ID `test_transaction_123` → Rejected (invalid transaction ID format — not a UUID) | + +### Server: `--boot-status Accepted --total-cost 25.50` + +| TC | Use Case | Via | Steps | Expected | +| --- | ------------------ | --------------------------------------------------- | ---------------------------------------- | ------------------------------------ | +| I02 | Running total cost | MCP `startAutomaticTransactionGenerator` → wait 30s | Check TransactionEvent.Updated responses | `totalCost: 25.5` in server response | + +--- + +## F — Remote Control + +### Server: various `--command X --delay 5` + +| TC | Use Case | Server flags | Expected | +| --- | ------------------------------------ | -------------------------------- | --------------------------------------------------------------- | +| F05 | Unlock connector | `--command UnlockConnector` | UnlockConnector → Unlocked | +| E14 | GetTransactionStatus (no active txn) | `--command GetTransactionStatus` | GetTransactionStatus → messagesInQueue: false, uses fallback ID | + +### Server: `--boot-status Accepted --commands "RequestStartTransaction:15,GetTransactionStatus:25"` + +| TC | Use Case | Via | Steps | Expected | +| ---------- | ------------------------------------ | --------------- | --------- | ------------------------------------------------------------------------------------------------ | +| E14-ACTIVE | GetTransactionStatus with active txn | Server commands | Wait ~35s | GetTransactionStatus → ongoingIndicator: true, messagesInQueue: false (real txn ID via tracking) | + +### Server: `--boot-status Accepted --commands "RequestStartTransaction:15,UnlockConnector:25"` + +| TC | Use Case | Via | Steps | Expected | +| ------- | --------------------------------- | --------------- | --------- | ---------------------------------------------- | +| F05-TXN | UnlockConnector during active txn | Server commands | Wait ~35s | UnlockConnector → OngoingAuthorizedTransaction | + +### Server: various `--command X --delay 5` (continued) + +| F06-SN | TriggerMessage (StatusNotification) | `--command TriggerMessage` | TriggerMessage → Accepted → StatusNotification sent | +| F06-BN | TriggerMessage (BootNotification) | `--command TriggerMessage --trigger-message BootNotification` | TriggerMessage → Rejected(NotEnabled, F06.FR.17 — already accepted) | +| F06-HB | TriggerMessage (Heartbeat) | `--command TriggerMessage --trigger-message Heartbeat` | TriggerMessage → Accepted → Heartbeat sent | +| F06-MV | TriggerMessage (MeterValues) | `--command TriggerMessage --trigger-message MeterValues` | TriggerMessage → Accepted → MeterValues sent | +| F06-FW | TriggerMessage (FirmwareStatus) | `--command TriggerMessage --trigger-message FirmwareStatusNotification` | TriggerMessage → Accepted → FirmwareStatusNotification(Idle) sent | +| F06-LS | TriggerMessage (LogStatus) | `--command TriggerMessage --trigger-message LogStatusNotification` | TriggerMessage → Accepted → LogStatusNotification(Idle) sent | + +--- + +## G — Availability + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | ------------------ | ------------------------ | ------------------------------------------------------------------------- | ------------------------------ | +| G01 | StatusNotification | MCP `statusNotification` | Send for each status: Available, Occupied, Faulted, Unavailable, Reserved | All succeed (empty response) | +| G02 | Heartbeat | MCP `heartbeat` | Send 5x rapid | All succeed with `currentTime` | + +### Server: various `--command ChangeAvailability --delay 5` + +| TC | Use Case | Server flags | Expected | +| --- | ------------------------------ | ---------------------------------------------------------------- | ------------------------------------------ | +| G03 | ChangeAvailability Operative | `--command ChangeAvailability` | Accepted + StatusNotification(Available) | +| G04 | ChangeAvailability Inoperative | `--command ChangeAvailability --availability-status Inoperative` | Accepted + StatusNotification(Unavailable) | + +--- + +## J — MeterValues + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | --------------------------- | ------------------------------------------------------ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| J01 | Non-transaction MeterValues | MCP `meterValues` | Send Voltage=230V on evseId=1 | Response empty (success) | +| J02 | Transaction MeterValues | MCP `startAutomaticTransactionGenerator` → wait 60-90s | Check logs | TransactionEvent.Updated contains Voltage, Energy, Power, Current with context `Sample.Periodic`. Note: ATG start delay (15-30s) + MeterValueSampleInterval (30s) = first MeterValues ~45-60s after ATG start. | + +--- + +## L — Firmware Management + +### Server: `--boot-status Accepted --command UpdateFirmware --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| --- | ---------------------- | -------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| L01 | Secure Firmware Update | Server command | Wait ~40s | UpdateFirmware → Accepted → FirmwareStatusNotification: Downloading → Downloaded → Installing → Installed → SecurityEventNotification(FirmwareUpdated) | + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| ------- | -------------------------- | -------------------------------- | -------------------------------------- | ------------------------ | +| L01-MCP | FirmwareStatusNotification | MCP `firmwareStatusNotification` | Send `status: Installed, requestId: 1` | Response empty (success) | + +--- + +## M — ISO 15118 Certificate Management + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | ------------------------ | --------------------------- | ------------------- | --------------------------- | +| M01 | Get 15118 EV Certificate | MCP `get15118EVCertificate` | Send Install action | Response `status: Accepted` | +| M06 | Get Certificate Status | MCP `getCertificateStatus` | Send OCSP data | Response `status: Accepted` | + +### Server: various `--command X --delay 5` + +| TC | Use Case | Server flags | Expected | +| --- | ---------------------- | -------------------------------------- | --------------------------------------- | +| M03 | Get installed cert IDs | `--command GetInstalledCertificateIds` | Response NotFound (cert manager absent) | +| M04 | Delete certificate | `--command DeleteCertificate` | Response Failed (cert manager absent) | +| M05 | Install certificate | `--command InstallCertificate` | Response Failed (cert manager absent) | + +--- + +## N — Diagnostics + +### Server: `--boot-status Accepted --command GetLog --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| --- | ------------ | -------------- | --------- | -------------------------------------------------------------------- | +| N01 | Retrieve Log | Server command | Wait ~15s | GetLog(DiagnosticsLog) → Accepted → LogStatusNotification(Uploading) | + +### Server: `--boot-status Accepted --command CustomerInformation --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| --- | -------------------- | -------------- | --------- | ------------------------------------------------------------------------------------------------------------- | +| N09 | Customer Information | Server command | Wait ~15s | CustomerInformation(report=true, customerIdentifier=test_customer_001) → Accepted → NotifyCustomerInformation | + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| -------------- | ------------------------- | ------------------------------- | ---------------------------- | ------------------------ | +| N01-MCP | LogStatusNotification | MCP `logStatusNotification` | Send `status: Uploaded` | Response empty (success) | +| N09-MCP | NotifyCustomerInformation | MCP `notifyCustomerInformation` | Send data with requestId | Response empty (success) | +| N-NOTIF-REPORT | NotifyReport | MCP `notifyReport` | Send with requestId, seqNo=0 | Response empty (success) | + +--- + +## P — DataTransfer + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | -------------------- | ------------------ | ----------------------------------------------- | --------------------------- | +| P02 | DataTransfer CS→CSMS | MCP `dataTransfer` | Send `vendorId: TestVendor, messageId: TestMsg` | Response `status: Accepted` | + +### Server: `--boot-status Accepted --command DataTransfer --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| --- | -------------------- | -------------- | --------- | ---------------------------------------------------------------------- | +| P01 | DataTransfer CSMS→CS | Server command | Wait ~15s | DataTransfer received → Response `UnknownVendorId` (no custom handler) | + +--- + +## B12 — Reset With Active Transaction + +### Server: `--boot-status Accepted --commands "RequestStartTransaction:15,Reset:30"` + +| TC | Use Case | Via | Steps | Expected | +| --- | ---------------------- | -------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| B12 | Reset with ongoing txn | Server commands (single session) | Wait ~60s for full cycle | RequestStartTransaction → Accepted → TransactionEvent.Started → Reset(Immediate) → station stops transaction → StatusNotification(Unavailable) → re-boot → Available | + +--- + +## Offline / Reconnection + +### Server: `--boot-status Accepted` (kill/restart cycle) + +| TC | Use Case | Steps | Expected | +| -------- | -------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| B04-FULL | Server down and reconnect | Kill server → wait 10s → restart | Station enters reconnection loop, reconnects, re-boots, returns to Available | +| B04-TXN | Offline during transaction | Start ATG → kill server → wait 15s → restart → stop ATG | Transaction stopped on server kill (stopTransactionsOnStopped=true). After reconnect: BootNotification → Accepted → StatusNotification(Available). No queued TransactionEvents. | + +--- + +## Edge Cases / Negative Tests + +### Server: `--boot-status Accepted` + +| TC | Description | Via | Expected | +| ------ | ----------------------------------- | ------------------------------------------------------ | ------------------------------------------- | +| ERR-03 | Multi-measurand MeterValues | MCP `meterValues` with Voltage+Power+Current+Energy | All accepted | +| ERR-04 | FirmwareStatus all statuses | MCP `firmwareStatusNotification` × 14 statuses | All succeed | +| ERR-05 | Orphaned LogStatusNotification | MCP `logStatusNotification` with `requestId: 999` | Succeeds (no prior GetLog required) | +| ERR-06 | Orphaned FirmwareStatusNotification | MCP `firmwareStatusNotification` with `requestId: 999` | Succeeds (no prior UpdateFirmware required) | + +### Server: `--boot-status Accepted --commands "RequestStartTransaction:15,RequestStartTransaction:25"` + +| TC | Description | Expected | +| -------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------ | +| E-DOUBLE-START | Second RequestStartTransaction while first txn active | First → Accepted + TransactionEvent.Started. Second → Rejected (connector occupied). | + +### Server: `--boot-status-sequence Pending,Accepted --command Reset --delay 8` + +| TC | Description | Expected | +| ----------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| B11-PENDING | Reset while in Pending state | Station receives Reset → Accepted (Reset is not blocked by registration state). StatusNotification(Unavailable) → reconnect → re-boot. | + +--- + +## Execution Order + +Tests grouped by server configuration to minimize restarts. +Use `closeConnection`/`openConnection` between groups 8-18 to avoid cumulative reconnect delays. + +| # | Server Config | Test Cases | +| --- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | `--boot-status Accepted` | B01, B04, A03, A04, C01, G01, G02, J01, J02, P02, E01-ATG, E01-DIRECT, L01-MCP, M01, M06, N01-MCP, N09-MCP, N-NOTIF-REPORT, ERR-03→06 | +| 2 | `--boot-status Accepted --auth-mode whitelist --whitelist valid_token test_token` | C01-WL-OK, C01-WL-REJECT | +| 3 | `--boot-status Accepted --auth-mode blacklist --blacklist blocked_token` | C01-BL-OK, C01-BL-REJECT | +| 4 | `--boot-status Accepted --auth-mode rate_limit` | C01-RL | +| 5 | `--boot-status Accepted --offline` | C01-OFFLINE | +| 6 | `--boot-status Accepted --auth-group-id MyGroup --auth-cache-expiry 3600` | C09 | +| 7 | `--boot-status Accepted --total-cost 25.50` | I02 | +| 8 | `--boot-status Accepted --command X --delay 5` (sequential restarts) | A02, B05, B06, B07, B09, B11, B11b, C11, E14, F05, F06-SN, F06-BN, F06-HB, F06-MV, F06-FW, F06-LS, G03, G04, L01, M03, M04, M05, N01, N09, P01 | +| 9 | `--boot-status Accepted --commands "RequestStartTransaction:15,RequestStopTransaction:45"` | F01+F03 | +| 10 | `--boot-status Accepted --command RequestStopTransaction --delay 5` | F03b | +| 11 | `--boot-status Accepted --commands "RequestStartTransaction:15,Reset:30"` | B12 | +| 12 | `--boot-status Accepted` (kill/restart cycle) | B04-FULL, B04-TXN | +| 13 | `--boot-status Accepted --commands "RequestStartTransaction:15,RequestStartTransaction:25"` | E-DOUBLE-START | +| 14 | `--boot-status Accepted --commands "RequestStartTransaction:15,GetTransactionStatus:25"` | E14-ACTIVE | +| 15 | `--boot-status Accepted --commands "RequestStartTransaction:15,UnlockConnector:25"` | F05-TXN | +| 16 | `--boot-status-sequence Pending,Accepted` | B02 | +| 17 | `--boot-status-sequence Pending,Accepted --command Reset --delay 8` | B11-PENDING | +| 18 | `--boot-status Rejected` | B03 | + +## Coverage + +| Block | Implemented Use Cases Covered | Test Count | +| ----------------- | ------------------------------- | ---------- | +| A. Security | A02, A03, A04 | 3 | +| B. Provisioning | B01-B04, B05-B07, B09, B11, B12 | 11 | +| C. Authorization | C01, C09, C11 | 8 | +| E. Transactions | E01, E14 | 5 | +| F. Remote Control | F01, F03, F05, F06 | 10 | +| G. Availability | G01-G04 | 6 | +| I. Tariff/Cost | I02 | 1 | +| J. MeterValues | J01, J02 | 2 | +| L. Firmware | L01 | 2 | +| M. ISO15118 Certs | M01, M03-M06 | 5 | +| N. Diagnostics | N01, N09 | 5 | +| P. DataTransfer | P01, P02 | 2 | +| Edge/Negative | — | 7 | +| **Total** | **34/34 commands** | **~70** | + +### Not Testable (simulator not implemented) + +D (LocalAuthList), H (Reservation), K (SmartCharging), O (DisplayMessage), N02-N08 (Monitoring). diff --git a/tests/utils/ChargingStationConfigurationUtils.test.ts b/tests/utils/ChargingStationConfigurationUtils.test.ts index b69d2706..7ba16216 100644 --- a/tests/utils/ChargingStationConfigurationUtils.test.ts +++ b/tests/utils/ChargingStationConfigurationUtils.test.ts @@ -200,9 +200,9 @@ await describe('ChargingStationConfigurationUtils', async () => { assert.strictEqual(connectorsStatus.length, 1) assert.strictEqual(connectorsStatus[0][0], 1) - const connector = connectorsStatus[0][1] - assert.ok(!('transactionMeterValuesSetInterval' in connector)) - assert.ok(!('transactionEventQueue' in connector)) + const connectorStatus = connectorsStatus[0][1] + assert.ok(!('transactionMeterValuesSetInterval' in connectorStatus)) + assert.ok(!('transactionEventQueue' in connectorStatus)) }) await it('should preserve connector IDs across serialization', () => { @@ -275,30 +275,30 @@ await describe('ChargingStationConfigurationUtils', async () => { await describe('buildChargingStationAutomaticTransactionGeneratorConfiguration', async () => { await it('should return ATG configuration when present', () => { - const atgConfig = { enable: true, maxDuration: 120, minDuration: 60 } + const atgConfiguration = { enable: true, maxDuration: 120, minDuration: 60 } const station = createMockStationForConfigUtils({ automaticTransactionGenerator: { connectorsStatus: new Map([[1, { start: false }]]), }, - getAutomaticTransactionGeneratorConfiguration: () => atgConfig, + getAutomaticTransactionGeneratorConfiguration: () => atgConfiguration, }) const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station) - assert.deepStrictEqual(result.automaticTransactionGenerator, atgConfig) + assert.deepStrictEqual(result.automaticTransactionGenerator, atgConfiguration) assert.notStrictEqual(result.automaticTransactionGeneratorStatuses, undefined) assert.ok(Array.isArray(result.automaticTransactionGeneratorStatuses)) assert.strictEqual(result.automaticTransactionGeneratorStatuses.length, 1) }) await it('should return ATG configuration without statuses when no ATG instance', () => { - const atgConfig = { enable: false } + const atgConfiguration = { enable: false } const station = createMockStationForConfigUtils({ automaticTransactionGenerator: undefined, - getAutomaticTransactionGeneratorConfiguration: () => atgConfig, + getAutomaticTransactionGeneratorConfiguration: () => atgConfiguration, }) const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station) - assert.deepStrictEqual(result.automaticTransactionGenerator, atgConfig) + assert.deepStrictEqual(result.automaticTransactionGenerator, atgConfiguration) assert.strictEqual(result.automaticTransactionGeneratorStatuses, undefined) }) @@ -314,16 +314,16 @@ await describe('ChargingStationConfigurationUtils', async () => { }) await it('should return ATG configuration without statuses when connectorsStatus is null', () => { - const atgConfig = { enable: true } + const atgConfiguration = { enable: true } const station = createMockStationForConfigUtils({ automaticTransactionGenerator: { connectorsStatus: undefined, }, - getAutomaticTransactionGeneratorConfiguration: () => atgConfig, + getAutomaticTransactionGeneratorConfiguration: () => atgConfiguration, }) const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station) - assert.deepStrictEqual(result.automaticTransactionGenerator, atgConfig) + assert.deepStrictEqual(result.automaticTransactionGenerator, atgConfiguration) assert.strictEqual(result.automaticTransactionGeneratorStatuses, undefined) }) }) diff --git a/tests/utils/MessageChannelUtils.test.ts b/tests/utils/MessageChannelUtils.test.ts index 76f0b23c..7e2be5ae 100644 --- a/tests/utils/MessageChannelUtils.test.ts +++ b/tests/utils/MessageChannelUtils.test.ts @@ -160,10 +160,10 @@ await describe('MessageChannelUtils', async () => { assert.strictEqual(message.data.id, 'test-station-id') assert.strictEqual(message.data.name, 'test-station') - const heartbeatStats = message.data.statisticsData.get('Heartbeat') - assert.notStrictEqual(heartbeatStats, undefined) - assert.ok(Array.isArray(heartbeatStats?.measurementTimeSeries)) - const timeSeries = heartbeatStats.measurementTimeSeries + const heartbeatStatistics = message.data.statisticsData.get('Heartbeat') + assert.notStrictEqual(heartbeatStatistics, undefined) + assert.ok(Array.isArray(heartbeatStatistics?.measurementTimeSeries)) + const timeSeries = heartbeatStatistics.measurementTimeSeries assert.strictEqual(timeSeries.length, 2) assert.strictEqual(timeSeries[0].value, 42) assert.strictEqual(timeSeries[1].value, 84) diff --git a/ui/web/src/composables/UIClient.ts b/ui/web/src/composables/UIClient.ts index 1bb98ec7..fabef626 100644 --- a/ui/web/src/composables/UIClient.ts +++ b/ui/web/src/composables/UIClient.ts @@ -323,9 +323,9 @@ export class UIClient { if (isProtocolResponse) { const [uuid, responsePayload] = message as ProtocolResponse - if (this.responseHandlers.has(uuid)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { procedureName, reject, resolve } = this.responseHandlers.get(uuid)! + const responseHandler = this.responseHandlers.get(uuid) + if (responseHandler != null) { + const { procedureName, reject, resolve } = responseHandler switch (responsePayload.status) { case ResponseStatus.FAILURE: reject(responsePayload) diff --git a/ui/web/tests/unit/CSConnector.test.ts b/ui/web/tests/unit/CSConnector.test.ts index cfc3dca7..14e70ee3 100644 --- a/ui/web/tests/unit/CSConnector.test.ts +++ b/ui/web/tests/unit/CSConnector.test.ts @@ -116,8 +116,11 @@ describe('CSConnector', () => { describe('transaction actions', () => { it('should call stopTransaction with correct params', async () => { - const connector = createConnectorStatus({ transactionId: 12345, transactionStarted: true }) - const wrapper = mountCSConnector({ connector }) + const connectorStatus = createConnectorStatus({ + transactionId: 12345, + transactionStarted: true, + }) + const wrapper = mountCSConnector({ connector: connectorStatus }) const buttons = wrapper.findAll('button') const stopBtn = buttons.find(b => b.text() === 'Stop Transaction') await stopBtn?.trigger('click') @@ -129,11 +132,11 @@ describe('CSConnector', () => { }) it('should show error toast when no transaction to stop', async () => { - const connector = createConnectorStatus({ + const connectorStatus = createConnectorStatus({ transactionId: undefined, transactionStarted: true, }) - const wrapper = mountCSConnector({ connector }) + const wrapper = mountCSConnector({ connector: connectorStatus }) const buttons = wrapper.findAll('button') const stopBtn = buttons.find(b => b.text() === 'Stop Transaction') await stopBtn?.trigger('click') @@ -141,8 +144,8 @@ describe('CSConnector', () => { }) it('should show success toast after stopping transaction', async () => { - const connector = createConnectorStatus({ transactionId: 99, transactionStarted: true }) - const wrapper = mountCSConnector({ connector }) + const connectorStatus = createConnectorStatus({ transactionId: 99, transactionStarted: true }) + const wrapper = mountCSConnector({ connector: connectorStatus }) const buttons = wrapper.findAll('button') const stopBtn = buttons.find(b => b.text() === 'Stop Transaction') await stopBtn?.trigger('click') diff --git a/ui/web/tests/unit/ChargingStationsView.test.ts b/ui/web/tests/unit/ChargingStationsView.test.ts index 3990ccd8..50e411e8 100644 --- a/ui/web/tests/unit/ChargingStationsView.test.ts +++ b/ui/web/tests/unit/ChargingStationsView.test.ts @@ -24,11 +24,11 @@ vi.mock('@/composables', async importOriginal => { // ── Configuration fixtures ──────────────────────────────────────────────────── -const singleServerConfig = { +const singleServerConfiguration = { uiServer: [createUIServerConfig()], } -const multiServerConfig = { +const multiServerConfiguration = { uiServer: [ createUIServerConfig({ name: 'Server 1' }), createUIServerConfig({ host: 'server2', name: 'Server 2' }), @@ -63,11 +63,15 @@ function getWSHandler (eventType: string): ((...args: unknown[]) => void) | unde function mountView ( options: { chargingStations?: ReturnType[] - configuration?: typeof multiServerConfig | typeof singleServerConfig + configuration?: typeof multiServerConfiguration | typeof singleServerConfiguration templates?: string[] } = {} ) { - const { chargingStations = [], configuration = singleServerConfig, templates = [] } = options + const { + chargingStations = [], + configuration = singleServerConfiguration, + templates = [], + } = options return mount(ChargingStationsView, { global: { @@ -234,27 +238,27 @@ describe('ChargingStationsView', () => { describe('UI server selector', () => { it('should hide server selector for single server configuration', () => { - const wrapper = mountView({ configuration: singleServerConfig }) + const wrapper = mountView({ configuration: singleServerConfiguration }) const selectorContainer = wrapper.find('#ui-server-container') expect(selectorContainer.exists()).toBe(true) expect((selectorContainer.element as HTMLElement).style.display).toBe('none') }) it('should show server selector for multiple server configuration', () => { - const wrapper = mountView({ configuration: multiServerConfig }) + const wrapper = mountView({ configuration: multiServerConfiguration }) const selectorContainer = wrapper.find('#ui-server-container') expect(selectorContainer.exists()).toBe(true) expect((selectorContainer.element as HTMLElement).style.display).not.toBe('none') }) it('should render an option for each server', () => { - const wrapper = mountView({ configuration: multiServerConfig }) + const wrapper = mountView({ configuration: multiServerConfiguration }) const options = wrapper.findAll('#ui-server-selector option') expect(options).toHaveLength(2) }) it('should display server name in options', () => { - const wrapper = mountView({ configuration: multiServerConfig }) + const wrapper = mountView({ configuration: multiServerConfiguration }) const options = wrapper.findAll('#ui-server-selector option') expect(options[0].text()).toContain('Server 1') expect(options[1].text()).toContain('Server 2') @@ -347,14 +351,14 @@ describe('ChargingStationsView', () => { describe('server switching', () => { it('should call setConfiguration when server index changes', async () => { - const wrapper = mountView({ configuration: multiServerConfig }) + const wrapper = mountView({ configuration: multiServerConfiguration }) const selector = wrapper.find('#ui-server-selector') await selector.setValue(1) expect(mockClient.setConfiguration).toHaveBeenCalled() }) it('should register new WS event listeners after server switch', async () => { - const wrapper = mountView({ configuration: multiServerConfig }) + const wrapper = mountView({ configuration: multiServerConfiguration }) // Reset call count from initial mount registration vi.mocked(mockClient.registerWSEventListener).mockClear() const selector = wrapper.find('#ui-server-selector') @@ -364,7 +368,7 @@ describe('ChargingStationsView', () => { }) it('should save server index to localStorage on successful switch', async () => { - const wrapper = mountView({ configuration: multiServerConfig }) + const wrapper = mountView({ configuration: multiServerConfiguration }) const selector = wrapper.find('#ui-server-selector') await selector.setValue(1) // Simulate the WS open for the new connection (once-listener from server switching) @@ -386,7 +390,7 @@ describe('ChargingStationsView', () => { it('should revert server index on connection error', async () => { localStorage.setItem('uiServerConfigurationIndex', '0') - const wrapper = mountView({ configuration: multiServerConfig }) + const wrapper = mountView({ configuration: multiServerConfiguration }) const selector = wrapper.find('#ui-server-selector') await selector.setValue(1) // Find the once-error listener -- 2.53.0