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)
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)
// 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 = {
// 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 = {
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')
})
})
})
// 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' }]
}
}
}
)
// 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 ───────────────────────────────────────────────
// 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' }]
}
}
}
)
// 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
)
// 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
// 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',
)
// 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 () => {
)
// 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)
})
})
)
// 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
)
// 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 () => {
)
// 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 () => {
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,
})
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,
})
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,
})
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,
})
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,
})
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
}
}
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
}
}
// 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)
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
}
}
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 }
// 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)
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()
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 () => {
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
)
})
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 () => {
)
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 () => {
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)
})
})
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 () => {
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)
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()
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)
})
})
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', () => {
}
// 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 () => {
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()
}
/**
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 () => {
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')
})
})
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',
await flushQueuedTransactionMessages(station)
- assert.strictEqual(connector.transactionEventQueue.length, 0)
+ assert.strictEqual(connectorStatus.transactionEventQueue.length, 0)
})
})
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)
})
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)
})
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)
}
private testConfigurationManagement (): void {
- const originalConfig = this.authService.getConfiguration()
+ const originalConfiguration = this.authService.getConfiguration()
const updates: Partial<AuthConfiguration> = {
authorizationTimeout: 60,
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`)
}
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')
}
* @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
}
}
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
}
/**
--- /dev/null
+# 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).
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', () => {
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)
})
})
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)
})
})
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)
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)
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')
})
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')
})
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')
// ── Configuration fixtures ────────────────────────────────────────────────────
-const singleServerConfig = {
+const singleServerConfiguration = {
uiServer: [createUIServerConfig()],
}
-const multiServerConfig = {
+const multiServerConfiguration = {
uiServer: [
createUIServerConfig({ name: 'Server 1' }),
createUIServerConfig({ host: 'server2', name: 'Server 2' }),
function mountView (
options: {
chargingStations?: ReturnType<typeof createChargingStationData>[]
- 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: {
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')
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')
})
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)
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