]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(ocpp2): convert incoming request fire-and-forget patterns to event listeners...
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Thu, 12 Mar 2026 21:43:33 +0000 (22:43 +0100)
committerGitHub <noreply@github.com>
Thu, 12 Mar 2026 21:43:33 +0000 (22:43 +0100)
* refactor(ocpp2): convert UpdateFirmware fire-and-forget to event listener

* refactor(ocpp2): convert GetLog fire-and-forget to event listener

* refactor(ocpp2): convert CustomerInformation fire-and-forget to event listener

* refactor(ocpp2): convert TriggerMessage fire-and-forget to event listener

* [autofix.ci] apply automated fixes

* test(ocpp2): improve TRIGGER_MESSAGE listener test coverage for SonarCloud

* test(ocpp2): remove 10 redundant handler tests that add no branch coverage

* test(ocpp2): remove 5 project-wide redundant tests identified by cross-validated audit

* refactor(ocpp2): extract StatusNotification trigger methods to reduce cognitive complexity

* [autofix.ci] apply automated fixes

* test(ocpp2): strengthen StatusNotification trigger assertions with payload and options validation

* [autofix.ci] apply automated fixes

* fix(ocpp2): relocate B11.FR.01 spec reference to correct surviving test

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-CustomerInformation.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetLog.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-TriggerMessage.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UpdateFirmware.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts

index 3b01501e18736b411ae943e198315da375e7ba3d..89bc7e844306646b13becc140660eb8447f60be7 100644 (file)
@@ -266,6 +266,147 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
         }
       }
     )
+    this.on(
+      OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
+      (
+        chargingStation: ChargingStation,
+        request: OCPP20UpdateFirmwareRequest,
+        response: OCPP20UpdateFirmwareResponse
+      ) => {
+        if (response.status === UpdateFirmwareStatusEnumType.Accepted) {
+          this.simulateFirmwareUpdateLifecycle(
+            chargingStation,
+            request.requestId,
+            request.firmware.signature
+          ).catch((error: unknown) => {
+            logger.error(
+              `${chargingStation.logPrefix()} ${moduleName}.constructor: UpdateFirmware lifecycle error:`,
+              error
+            )
+          })
+        }
+      }
+    )
+    this.on(
+      OCPP20IncomingRequestCommand.GET_LOG,
+      (
+        chargingStation: ChargingStation,
+        request: OCPP20GetLogRequest,
+        response: OCPP20GetLogResponse
+      ) => {
+        if (response.status === LogStatusEnumType.Accepted) {
+          this.simulateLogUploadLifecycle(chargingStation, request.requestId).catch(
+            (error: unknown) => {
+              logger.error(
+                `${chargingStation.logPrefix()} ${moduleName}.constructor: GetLog lifecycle error:`,
+                error
+              )
+            }
+          )
+        }
+      }
+    )
+    this.on(
+      OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION,
+      (
+        chargingStation: ChargingStation,
+        request: OCPP20CustomerInformationRequest,
+        response: OCPP20CustomerInformationResponse
+      ) => {
+        if (response.status === CustomerInformationStatusEnumType.Accepted && request.report) {
+          this.sendNotifyCustomerInformation(chargingStation, request.requestId).catch(
+            (error: unknown) => {
+              logger.error(
+                `${chargingStation.logPrefix()} ${moduleName}.constructor: CustomerInformation notification error:`,
+                error
+              )
+            }
+          )
+        }
+      }
+    )
+    this.on(
+      OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
+      (
+        chargingStation: ChargingStation,
+        request: OCPP20TriggerMessageRequest,
+        response: OCPP20TriggerMessageResponse
+      ) => {
+        if (response.status !== TriggerMessageStatusEnumType.Accepted) {
+          return
+        }
+        const { evse, requestedMessage } = request
+        const errorHandler = (error: unknown): void => {
+          logger.error(
+            `${chargingStation.logPrefix()} ${moduleName}.constructor: Trigger ${requestedMessage} error:`,
+            error
+          )
+        }
+        switch (requestedMessage) {
+          case MessageTriggerEnumType.BootNotification:
+            chargingStation.ocppRequestService
+              .requestHandler<
+                OCPP20BootNotificationRequest,
+                OCPP20BootNotificationResponse
+              >(chargingStation, OCPP20RequestCommand.BOOT_NOTIFICATION, chargingStation.bootNotificationRequest as OCPP20BootNotificationRequest, { skipBufferingOnError: true, triggerMessage: true })
+              .catch(errorHandler)
+            break
+          case MessageTriggerEnumType.FirmwareStatusNotification:
+            chargingStation.ocppRequestService
+              .requestHandler<
+                OCPP20FirmwareStatusNotificationRequest,
+                OCPP20FirmwareStatusNotificationResponse
+              >(chargingStation, OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION, { status: FirmwareStatusEnumType.Idle }, { skipBufferingOnError: true, triggerMessage: true })
+              .catch(errorHandler)
+            break
+          case MessageTriggerEnumType.Heartbeat:
+            chargingStation.ocppRequestService
+              .requestHandler<
+                OCPP20HeartbeatRequest,
+                OCPP20HeartbeatResponse
+              >(chargingStation, OCPP20RequestCommand.HEARTBEAT, {}, { skipBufferingOnError: true, triggerMessage: true })
+              .catch(errorHandler)
+            break
+          case MessageTriggerEnumType.LogStatusNotification:
+            chargingStation.ocppRequestService
+              .requestHandler<
+                OCPP20LogStatusNotificationRequest,
+                OCPP20LogStatusNotificationResponse
+              >(chargingStation, OCPP20RequestCommand.LOG_STATUS_NOTIFICATION, { status: UploadLogStatusEnumType.Idle }, { skipBufferingOnError: true, triggerMessage: true })
+              .catch(errorHandler)
+            break
+          case MessageTriggerEnumType.MeterValues: {
+            const evseId = evse?.id ?? 0
+            chargingStation.ocppRequestService
+              .requestHandler<OCPP20MeterValuesRequest, OCPP20MeterValuesResponse>(
+                chargingStation,
+                OCPP20RequestCommand.METER_VALUES,
+                {
+                  evseId,
+                  meterValue: [
+                    {
+                      sampledValue: [
+                        {
+                          context: OCPP20ReadingContextEnumType.TRIGGER,
+                          measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
+                          value: 0,
+                        },
+                      ],
+                      timestamp: new Date(),
+                    },
+                  ],
+                },
+                { skipBufferingOnError: true, triggerMessage: true }
+              )
+              .catch(errorHandler)
+            break
+          }
+          case MessageTriggerEnumType.StatusNotification:
+            this.triggerStatusNotification(chargingStation, evse, errorHandler)
+            break
+        }
+      }
+    )
     this.validatePayload = this.validatePayload.bind(this)
   }
 
@@ -1191,17 +1332,6 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
       logger.info(
         `${chargingStation.logPrefix()} ${moduleName}.handleRequestCustomerInformation: Report request accepted, sending empty NotifyCustomerInformation`
       )
-      // Fire-and-forget NotifyCustomerInformation with empty data
-      setImmediate(() => {
-        this.sendNotifyCustomerInformation(chargingStation, commandPayload.requestId).catch(
-          (error: unknown) => {
-            logger.error(
-              `${chargingStation.logPrefix()} ${moduleName}.handleRequestCustomerInformation: Error sending NotifyCustomerInformation:`,
-              error
-            )
-          }
-        )
-      })
       return {
         status: CustomerInformationStatusEnumType.Accepted,
       }
@@ -1432,16 +1562,6 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
       `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetLog: Received GetLog request with requestId ${requestId.toString()} for logType '${logType}'`
     )
 
-    // Fire-and-forget log upload state machine after response is returned
-    setImmediate(() => {
-      this.simulateLogUploadLifecycle(chargingStation, requestId).catch((error: unknown) => {
-        logger.error(
-          `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetLog: Error during log upload simulation:`,
-          error
-        )
-      })
-    })
-
     return {
       filename: 'simulator-log.txt',
       status: LogStatusEnumType.Accepted,
@@ -2146,168 +2266,21 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
               },
             }
           }
-          chargingStation.ocppRequestService
-            .requestHandler<
-              OCPP20BootNotificationRequest,
-              OCPP20BootNotificationResponse
-            >(chargingStation, OCPP20RequestCommand.BOOT_NOTIFICATION, chargingStation.bootNotificationRequest as OCPP20BootNotificationRequest, { skipBufferingOnError: true, triggerMessage: true })
-            .catch((error: unknown) => {
-              logger.error(
-                `${chargingStation.logPrefix()} ${moduleName}.handleRequestTriggerMessage: Error sending BootNotification:`,
-                error
-              )
-            })
           return { status: TriggerMessageStatusEnumType.Accepted }
 
         case MessageTriggerEnumType.FirmwareStatusNotification:
-          chargingStation.ocppRequestService
-            .requestHandler<
-              OCPP20FirmwareStatusNotificationRequest,
-              OCPP20FirmwareStatusNotificationResponse
-            >(
-              chargingStation,
-              OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
-              {
-                status: FirmwareStatusEnumType.Idle,
-              },
-              { skipBufferingOnError: true, triggerMessage: true }
-            )
-            .catch((error: unknown) => {
-              logger.error(
-                `${chargingStation.logPrefix()} ${moduleName}.handleRequestTriggerMessage: Error sending FirmwareStatusNotification:`,
-                error
-              )
-            })
           return { status: TriggerMessageStatusEnumType.Accepted }
 
         case MessageTriggerEnumType.Heartbeat:
-          chargingStation.ocppRequestService
-            .requestHandler<
-              OCPP20HeartbeatRequest,
-              OCPP20HeartbeatResponse
-            >(chargingStation, OCPP20RequestCommand.HEARTBEAT, {}, { skipBufferingOnError: true, triggerMessage: true })
-            .catch((error: unknown) => {
-              logger.error(
-                `${chargingStation.logPrefix()} ${moduleName}.handleRequestTriggerMessage: Error sending Heartbeat:`,
-                error
-              )
-            })
           return { status: TriggerMessageStatusEnumType.Accepted }
 
         case MessageTriggerEnumType.LogStatusNotification:
-          chargingStation.ocppRequestService
-            .requestHandler<
-              OCPP20LogStatusNotificationRequest,
-              OCPP20LogStatusNotificationResponse
-            >(
-              chargingStation,
-              OCPP20RequestCommand.LOG_STATUS_NOTIFICATION,
-              {
-                status: UploadLogStatusEnumType.Idle,
-              },
-              { skipBufferingOnError: true, triggerMessage: true }
-            )
-            .catch((error: unknown) => {
-              logger.error(
-                `${chargingStation.logPrefix()} ${moduleName}.handleRequestTriggerMessage: Error sending LogStatusNotification:`,
-                error
-              )
-            })
           return { status: TriggerMessageStatusEnumType.Accepted }
 
-        case MessageTriggerEnumType.MeterValues: {
-          const evseId = evse?.id ?? 0
-          chargingStation.ocppRequestService
-            .requestHandler<OCPP20MeterValuesRequest, OCPP20MeterValuesResponse>(
-              chargingStation,
-              OCPP20RequestCommand.METER_VALUES,
-              {
-                evseId,
-                meterValue: [
-                  {
-                    sampledValue: [
-                      {
-                        context: OCPP20ReadingContextEnumType.TRIGGER,
-                        measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
-                        value: 0,
-                      },
-                    ],
-                    timestamp: new Date(),
-                  },
-                ],
-              },
-              { skipBufferingOnError: true, triggerMessage: true }
-            )
-            .catch((error: unknown) => {
-              logger.error(
-                `${chargingStation.logPrefix()} ${moduleName}.handleRequestTriggerMessage: Error sending MeterValues:`,
-                error
-              )
-            })
+        case MessageTriggerEnumType.MeterValues:
           return { status: TriggerMessageStatusEnumType.Accepted }
-        }
 
         case MessageTriggerEnumType.StatusNotification:
-          if (evse?.id !== undefined && evse.id > 0 && evse.connectorId !== undefined) {
-            const evseStatus = chargingStation.evses.get(evse.id)
-            const connectorStatus = evseStatus?.connectors.get(evse.connectorId)
-            const resolvedStatus =
-              connectorStatus?.status != null
-                ? (connectorStatus.status as unknown as OCPP20ConnectorStatusEnumType)
-                : OCPP20ConnectorStatusEnumType.Available
-            chargingStation.ocppRequestService
-              .requestHandler<OCPP20StatusNotificationRequest, OCPP20StatusNotificationResponse>(
-                chargingStation,
-                OCPP20RequestCommand.STATUS_NOTIFICATION,
-                {
-                  connectorId: evse.connectorId,
-                  connectorStatus: resolvedStatus,
-                  evseId: evse.id,
-                  timestamp: new Date(),
-                },
-                { skipBufferingOnError: true, triggerMessage: true }
-              )
-              .catch((error: unknown) => {
-                logger.error(
-                  `${chargingStation.logPrefix()} ${moduleName}.handleRequestTriggerMessage: Error sending StatusNotification:`,
-                  error
-                )
-              })
-          } else {
-            if (chargingStation.hasEvses) {
-              for (const [evseId, evseStatus] of chargingStation.evses) {
-                if (evseId > 0) {
-                  for (const [connectorId, connectorStatus] of evseStatus.connectors) {
-                    const resolvedConnectorStatus =
-                      connectorStatus.status != null
-                        ? (connectorStatus.status as unknown as OCPP20ConnectorStatusEnumType)
-                        : OCPP20ConnectorStatusEnumType.Available
-                    chargingStation.ocppRequestService
-                      .requestHandler<
-                        OCPP20StatusNotificationRequest,
-                        OCPP20StatusNotificationResponse
-                      >(
-                        chargingStation,
-                        OCPP20RequestCommand.STATUS_NOTIFICATION,
-                        {
-                          connectorId,
-                          connectorStatus: resolvedConnectorStatus,
-                          evseId,
-                          timestamp: new Date(),
-                        },
-                        { skipBufferingOnError: true, triggerMessage: true }
-                      )
-                      .catch((error: unknown) => {
-                        logger.error(
-                          `${chargingStation.logPrefix()} ${moduleName}.handleRequestTriggerMessage: Error sending StatusNotification:`,
-                          error
-                        )
-                      })
-                  }
-                }
-              }
-            }
-          }
           return { status: TriggerMessageStatusEnumType.Accepted }
 
         default:
@@ -2442,18 +2415,6 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
       `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Received UpdateFirmware request with requestId ${requestId.toString()} for location '${firmware.location}'`
     )
 
-    // Fire-and-forget firmware update state machine after response is returned
-    setImmediate(() => {
-      this.simulateFirmwareUpdateLifecycle(chargingStation, requestId, firmware.signature).catch(
-        (error: unknown) => {
-          logger.error(
-            `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Error during firmware update simulation:`,
-            error
-          )
-        }
-      )
-    })
-
     return {
       status: UpdateFirmwareStatusEnumType.Accepted,
     }
@@ -3049,6 +3010,65 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
     return handler as IncomingRequestHandler
   }
 
+  private triggerAllEvseStatusNotifications (
+    chargingStation: ChargingStation,
+    errorHandler: (error: unknown) => void
+  ): void {
+    for (const [evseId, evseStatus] of chargingStation.evses) {
+      if (evseId > 0) {
+        for (const [connectorId, connectorStatus] of evseStatus.connectors) {
+          const resolvedConnectorStatus =
+            connectorStatus.status != null
+              ? (connectorStatus.status as unknown as OCPP20ConnectorStatusEnumType)
+              : OCPP20ConnectorStatusEnumType.Available
+          chargingStation.ocppRequestService
+            .requestHandler<OCPP20StatusNotificationRequest, OCPP20StatusNotificationResponse>(
+              chargingStation,
+              OCPP20RequestCommand.STATUS_NOTIFICATION,
+              {
+                connectorId,
+                connectorStatus: resolvedConnectorStatus,
+                evseId,
+                timestamp: new Date(),
+              },
+              { skipBufferingOnError: true, triggerMessage: true }
+            )
+            .catch(errorHandler)
+        }
+      }
+    }
+  }
+
+  private triggerStatusNotification (
+    chargingStation: ChargingStation,
+    evse: OCPP20TriggerMessageRequest['evse'],
+    errorHandler: (error: unknown) => void
+  ): void {
+    if (evse?.id !== undefined && evse.id > 0 && evse.connectorId !== undefined) {
+      const evseStatus = chargingStation.evses.get(evse.id)
+      const connectorStatus = evseStatus?.connectors.get(evse.connectorId)
+      const resolvedStatus =
+        connectorStatus?.status != null
+          ? (connectorStatus.status as unknown as OCPP20ConnectorStatusEnumType)
+          : OCPP20ConnectorStatusEnumType.Available
+      chargingStation.ocppRequestService
+        .requestHandler<OCPP20StatusNotificationRequest, OCPP20StatusNotificationResponse>(
+          chargingStation,
+          OCPP20RequestCommand.STATUS_NOTIFICATION,
+          {
+            connectorId: evse.connectorId,
+            connectorStatus: resolvedStatus,
+            evseId: evse.id,
+            timestamp: new Date(),
+          },
+          { skipBufferingOnError: true, triggerMessage: true }
+        )
+        .catch(errorHandler)
+    } else if (chargingStation.hasEvses) {
+      this.triggerAllEvseStatusNotifications(chargingStation, errorHandler)
+    }
+  }
+
   private validateChargingProfile (
     chargingStation: ChargingStation,
     chargingProfile: OCPP20ChargingProfileType,
index bd802e13ff5a322bfaf781a4f45147f1318eb92c..d53ffcb0d293c9396374d8a979fcfcd1e0fab19f 100644 (file)
@@ -4,13 +4,19 @@
  */
 
 import assert from 'node:assert/strict'
-import { afterEach, beforeEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
 
 import type { ChargingStation } from '../../../../src/charging-station/index.js'
 
 import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
 import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
-import { CustomerInformationStatusEnumType, OCPPVersion } from '../../../../src/types/index.js'
+import {
+  CustomerInformationStatusEnumType,
+  type OCPP20CustomerInformationRequest,
+  type OCPP20CustomerInformationResponse,
+  OCPP20IncomingRequestCommand,
+  OCPPVersion,
+} from '../../../../src/types/index.js'
 import { Constants } from '../../../../src/utils/index.js'
 import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
 import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
@@ -82,25 +88,118 @@ await describe('N32 - CustomerInformation', async () => {
     assert.strictEqual(response.status, CustomerInformationStatusEnumType.Rejected)
   })
 
-  // Verify clear request with explicit false report flag
-  await it('should respond with Accepted for clear=true and report=false', () => {
-    const response = testableService.handleRequestCustomerInformation(station, {
+  await it('should register CUSTOMER_INFORMATION event listener in constructor', () => {
+    const service = new OCPP20IncomingRequestService()
+    assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION), 1)
+  })
+
+  await it('should call sendNotifyCustomerInformation when CUSTOMER_INFORMATION event emitted with Accepted + report=true', () => {
+    const service = new OCPP20IncomingRequestService()
+    const notifyMock = mock.method(
+      service as unknown as {
+        sendNotifyCustomerInformation: (
+          chargingStation: ChargingStation,
+          requestId: number
+        ) => Promise<void>
+      },
+      'sendNotifyCustomerInformation',
+      () => Promise.resolve()
+    )
+
+    const request: OCPP20CustomerInformationRequest = {
+      clear: false,
+      report: true,
+      requestId: 20,
+    }
+    const response: OCPP20CustomerInformationResponse = {
+      status: CustomerInformationStatusEnumType.Accepted,
+    }
+
+    service.emit(OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION, station, request, response)
+
+    assert.strictEqual(notifyMock.mock.callCount(), 1)
+    assert.strictEqual(notifyMock.mock.calls[0].arguments[1], 20)
+  })
+
+  await it('should NOT call sendNotifyCustomerInformation when CUSTOMER_INFORMATION event emitted with Accepted + clear=true only', () => {
+    // CRITICAL: clear=true also returns Accepted — listener must NOT fire notification
+    const service = new OCPP20IncomingRequestService()
+    const notifyMock = mock.method(
+      service as unknown as {
+        sendNotifyCustomerInformation: (
+          chargingStation: ChargingStation,
+          requestId: number
+        ) => Promise<void>
+      },
+      'sendNotifyCustomerInformation',
+      () => Promise.resolve()
+    )
+
+    const request: OCPP20CustomerInformationRequest = {
       clear: true,
       report: false,
-      requestId: 4,
-    })
+      requestId: 21,
+    }
+    const response: OCPP20CustomerInformationResponse = {
+      status: CustomerInformationStatusEnumType.Accepted,
+    }
 
-    assert.strictEqual(response.status, CustomerInformationStatusEnumType.Accepted)
+    service.emit(OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION, station, request, response)
+
+    assert.strictEqual(notifyMock.mock.callCount(), 0)
   })
 
-  // Verify report request with explicit false clear flag
-  await it('should respond with Accepted for clear=false and report=true', () => {
-    const response = testableService.handleRequestCustomerInformation(station, {
+  await it('should NOT call sendNotifyCustomerInformation when CUSTOMER_INFORMATION event emitted with Rejected', () => {
+    const service = new OCPP20IncomingRequestService()
+    const notifyMock = mock.method(
+      service as unknown as {
+        sendNotifyCustomerInformation: (
+          chargingStation: ChargingStation,
+          requestId: number
+        ) => Promise<void>
+      },
+      'sendNotifyCustomerInformation',
+      () => Promise.resolve()
+    )
+
+    const request: OCPP20CustomerInformationRequest = {
+      clear: false,
+      report: false,
+      requestId: 22,
+    }
+    const response: OCPP20CustomerInformationResponse = {
+      status: CustomerInformationStatusEnumType.Rejected,
+    }
+
+    service.emit(OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION, station, request, response)
+
+    assert.strictEqual(notifyMock.mock.callCount(), 0)
+  })
+
+  await it('should handle sendNotifyCustomerInformation rejection gracefully', async () => {
+    const service = new OCPP20IncomingRequestService()
+    mock.method(
+      service as unknown as {
+        sendNotifyCustomerInformation: (
+          chargingStation: ChargingStation,
+          requestId: number
+        ) => Promise<void>
+      },
+      'sendNotifyCustomerInformation',
+      () => Promise.reject(new Error('notification error'))
+    )
+
+    const request: OCPP20CustomerInformationRequest = {
       clear: false,
       report: true,
-      requestId: 5,
-    })
+      requestId: 99,
+    }
+    const response: OCPP20CustomerInformationResponse = {
+      status: CustomerInformationStatusEnumType.Accepted,
+    }
 
-    assert.strictEqual(response.status, CustomerInformationStatusEnumType.Accepted)
+    service.emit(OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION, station, request, response)
+
+    await Promise.resolve()
   })
 })
index bc8670648b923c9b5d09b297352c282e277e6765..5bacfe0950bdf87abed1101d6140a6866624d226 100644 (file)
@@ -4,7 +4,7 @@
  */
 
 import assert from 'node:assert/strict'
-import { afterEach, beforeEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
 
 import type { ChargingStation } from '../../../../src/charging-station/index.js'
 
@@ -14,6 +14,8 @@ import {
   LogEnumType,
   LogStatusEnumType,
   type OCPP20GetLogRequest,
+  type OCPP20GetLogResponse,
+  OCPP20IncomingRequestCommand,
   OCPPVersion,
 } from '../../../../src/types/index.js'
 import { Constants } from '../../../../src/utils/index.js'
@@ -77,39 +79,98 @@ await describe('K01 - GetLog', async () => {
     assert.strictEqual(response.filename, 'simulator-log.txt')
   })
 
-  await it('should pass through requestId correctly across different values', () => {
-    const testRequestId = 42
+  await it('should register GET_LOG event listener in constructor', () => {
+    const service = new OCPP20IncomingRequestService()
+    assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.GET_LOG), 1)
+  })
+
+  await it('should call simulateLogUploadLifecycle when GET_LOG event emitted with Accepted response', () => {
+    const service = new OCPP20IncomingRequestService()
+    const simulateMock = mock.method(
+      service as unknown as {
+        simulateLogUploadLifecycle: (
+          chargingStation: ChargingStation,
+          requestId: number
+        ) => Promise<void>
+      },
+      'simulateLogUploadLifecycle',
+      () => Promise.resolve()
+    )
+
     const request: OCPP20GetLogRequest = {
       log: {
-        remoteLocation: 'ftp://logs.example.com/uploads/',
+        remoteLocation: 'https://csms.example.com/logs',
       },
       logType: LogEnumType.DiagnosticsLog,
-      requestId: testRequestId,
+      requestId: 10,
+    }
+    const response: OCPP20GetLogResponse = {
+      filename: 'simulator-log.txt',
+      status: LogStatusEnumType.Accepted,
     }
 
-    const response = testableService.handleRequestGetLog(station, request)
+    service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
 
-    assert.strictEqual(response.status, LogStatusEnumType.Accepted)
-    assert.strictEqual(typeof response.status, 'string')
-    assert.strictEqual(response.filename, 'simulator-log.txt')
+    assert.strictEqual(simulateMock.mock.callCount(), 1)
+    assert.strictEqual(simulateMock.mock.calls[0].arguments[1], 10)
   })
 
-  await it('should return Accepted for request with retries and retryInterval', () => {
+  await it('should NOT call simulateLogUploadLifecycle when GET_LOG event emitted with Rejected response', () => {
+    const service = new OCPP20IncomingRequestService()
+    const simulateMock = mock.method(
+      service as unknown as {
+        simulateLogUploadLifecycle: (
+          chargingStation: ChargingStation,
+          requestId: number
+        ) => Promise<void>
+      },
+      'simulateLogUploadLifecycle',
+      () => Promise.resolve()
+    )
+
     const request: OCPP20GetLogRequest = {
       log: {
-        latestTimestamp: new Date('2025-01-15T23:59:59.000Z'),
-        oldestTimestamp: new Date('2025-01-01T00:00:00.000Z'),
-        remoteLocation: 'ftp://logs.example.com/uploads/',
+        remoteLocation: 'https://csms.example.com/logs',
       },
       logType: LogEnumType.DiagnosticsLog,
-      requestId: 5,
-      retries: 3,
-      retryInterval: 60,
+      requestId: 11,
+    }
+    const response: OCPP20GetLogResponse = {
+      status: LogStatusEnumType.Rejected,
     }
 
-    const response = testableService.handleRequestGetLog(station, request)
+    service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
 
-    assert.strictEqual(response.status, LogStatusEnumType.Accepted)
-    assert.strictEqual(response.filename, 'simulator-log.txt')
+    assert.strictEqual(simulateMock.mock.callCount(), 0)
+  })
+
+  await it('should handle simulateLogUploadLifecycle rejection gracefully', async () => {
+    const service = new OCPP20IncomingRequestService()
+    mock.method(
+      service as unknown as {
+        simulateLogUploadLifecycle: (
+          chargingStation: ChargingStation,
+          requestId: number
+        ) => Promise<void>
+      },
+      'simulateLogUploadLifecycle',
+      () => Promise.reject(new Error('log upload error'))
+    )
+
+    const request: OCPP20GetLogRequest = {
+      log: {
+        remoteLocation: 'https://csms.example.com/logs',
+      },
+      logType: LogEnumType.DiagnosticsLog,
+      requestId: 99,
+    }
+    const response: OCPP20GetLogResponse = {
+      filename: 'simulator-log.txt',
+      status: LogStatusEnumType.Accepted,
+    }
+
+    service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
+
+    await Promise.resolve()
   })
 })
index 9c1fb96b2fc6807e8884762152e44e71c2f90828..a888f5bd7b7deb12204e86e0e91372e4006fb706 100644 (file)
@@ -49,51 +49,6 @@ await describe('B11 & B12 - Reset', async () => {
       mockStation = ResetTestFixtures.createStandardStation()
     })
 
-    // FR: B11.FR.01
-    await it('should handle Reset request with Immediate type when no transactions', async () => {
-      const resetRequest: OCPP20ResetRequest = {
-        type: ResetEnumType.Immediate,
-      }
-
-      const response: OCPP20ResetResponse = await testableService.handleRequestReset(
-        mockStation,
-        resetRequest
-      )
-
-      assert.notStrictEqual(response, undefined)
-      assert.strictEqual(typeof response, 'object')
-      assert.notStrictEqual(response.status, undefined)
-      assert.strictEqual(typeof response.status, 'string')
-      assert.ok(
-        [
-          ResetStatusEnumType.Accepted,
-          ResetStatusEnumType.Rejected,
-          ResetStatusEnumType.Scheduled,
-        ].includes(response.status)
-      )
-    })
-
-    await it('should handle Reset request with OnIdle type when no transactions', async () => {
-      const resetRequest: OCPP20ResetRequest = {
-        type: ResetEnumType.OnIdle,
-      }
-
-      const response: OCPP20ResetResponse = await testableService.handleRequestReset(
-        mockStation,
-        resetRequest
-      )
-
-      assert.notStrictEqual(response, undefined)
-      assert.notStrictEqual(response.status, undefined)
-      assert.ok(
-        [
-          ResetStatusEnumType.Accepted,
-          ResetStatusEnumType.Rejected,
-          ResetStatusEnumType.Scheduled,
-        ].includes(response.status)
-      )
-    })
-
     // FR: B11.FR.03
     await it('should handle EVSE-specific reset request when no transactions', async () => {
       const resetRequest: OCPP20ResetRequest = {
@@ -140,6 +95,7 @@ await describe('B11 & B12 - Reset', async () => {
       assert.ok(response.statusInfo.additionalInfo.includes('EVSE 999'))
     })
 
+    // FR: B11.FR.01
     await it('should return proper response structure for immediate reset without transactions', async () => {
       const resetRequest: OCPP20ResetRequest = {
         type: ResetEnumType.Immediate,
index 93dc0e7922eac037c2da963131e2142a6bf977e2..3199af0784145a8c695afa47f30313b268090ba1 100644 (file)
@@ -16,6 +16,8 @@ import { createTestableIncomingRequestService } from '../../../../src/charging-s
 import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
 import {
   MessageTriggerEnumType,
+  OCPP20IncomingRequestCommand,
+  OCPP20RequestCommand,
   OCPPVersion,
   ReasonCodeEnumType,
   RegistrationStatusEnumType,
@@ -382,34 +384,256 @@ await describe('F06 - TriggerMessage', async () => {
     })
   })
 
-  await describe('F06 - Response structure', async () => {
+  await describe('F06 - TRIGGER_MESSAGE event listener', async () => {
+    let incomingRequestServiceForListener: OCPP20IncomingRequestService
     let mockStation: MockChargingStation
+    let requestHandlerMock: ReturnType<typeof mock.fn>
 
     beforeEach(() => {
-      ;({ mockStation } = createTriggerMessageStation())
+      ;({ mockStation, requestHandlerMock } = createTriggerMessageStation())
+      incomingRequestServiceForListener = new OCPP20IncomingRequestService()
+    })
+
+    await it('should register TRIGGER_MESSAGE event listener in constructor', () => {
+      assert.strictEqual(
+        incomingRequestServiceForListener.listenerCount(
+          OCPP20IncomingRequestCommand.TRIGGER_MESSAGE
+        ),
+        1
+      )
     })
 
-    await it('should return a plain object with a string status field', () => {
+    await it('should NOT fire requestHandler when response status is Rejected', () => {
       const request: OCPP20TriggerMessageRequest = {
-        requestedMessage: MessageTriggerEnumType.BootNotification,
+        requestedMessage: MessageTriggerEnumType.Heartbeat,
+      }
+      const response: OCPP20TriggerMessageResponse = {
+        status: TriggerMessageStatusEnumType.Rejected,
+      }
+
+      incomingRequestServiceForListener.emit(
+        OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
+        mockStation,
+        request,
+        response
+      )
+
+      assert.strictEqual(requestHandlerMock.mock.callCount(), 0)
+    })
+
+    await it('should NOT fire requestHandler when response status is NotImplemented', () => {
+      const request: OCPP20TriggerMessageRequest = {
+        requestedMessage: MessageTriggerEnumType.Heartbeat,
+      }
+      const response: OCPP20TriggerMessageResponse = {
+        status: TriggerMessageStatusEnumType.NotImplemented,
       }
 
-      const response = testableService.handleRequestTriggerMessage(mockStation, request)
+      incomingRequestServiceForListener.emit(
+        OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
+        mockStation,
+        request,
+        response
+      )
 
-      assert.notStrictEqual(response, undefined)
-      assert.strictEqual(typeof response, 'object')
-      assert.strictEqual(typeof response.status, 'string')
+      assert.strictEqual(requestHandlerMock.mock.callCount(), 0)
     })
 
-    await it('should not return a Promise from synchronous handler', () => {
+    await it('should fire BootNotification requestHandler on Accepted', () => {
       const request: OCPP20TriggerMessageRequest = {
         requestedMessage: MessageTriggerEnumType.BootNotification,
       }
+      const response: OCPP20TriggerMessageResponse = {
+        status: TriggerMessageStatusEnumType.Accepted,
+      }
+
+      incomingRequestServiceForListener.emit(
+        OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
+        mockStation,
+        request,
+        response
+      )
+
+      assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+    })
+
+    await it('should fire Heartbeat requestHandler on Accepted', () => {
+      const request: OCPP20TriggerMessageRequest = {
+        requestedMessage: MessageTriggerEnumType.Heartbeat,
+      }
+      const response: OCPP20TriggerMessageResponse = {
+        status: TriggerMessageStatusEnumType.Accepted,
+      }
+
+      incomingRequestServiceForListener.emit(
+        OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
+        mockStation,
+        request,
+        response
+      )
+
+      assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+    })
+
+    await it('should fire FirmwareStatusNotification requestHandler on Accepted', () => {
+      const request: OCPP20TriggerMessageRequest = {
+        requestedMessage: MessageTriggerEnumType.FirmwareStatusNotification,
+      }
+      const response: OCPP20TriggerMessageResponse = {
+        status: TriggerMessageStatusEnumType.Accepted,
+      }
+
+      incomingRequestServiceForListener.emit(
+        OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
+        mockStation,
+        request,
+        response
+      )
+
+      assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+    })
+
+    await it('should fire LogStatusNotification requestHandler on Accepted', () => {
+      const request: OCPP20TriggerMessageRequest = {
+        requestedMessage: MessageTriggerEnumType.LogStatusNotification,
+      }
+      const response: OCPP20TriggerMessageResponse = {
+        status: TriggerMessageStatusEnumType.Accepted,
+      }
+
+      incomingRequestServiceForListener.emit(
+        OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
+        mockStation,
+        request,
+        response
+      )
+
+      assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+    })
+
+    await it('should fire MeterValues requestHandler on Accepted', () => {
+      const request: OCPP20TriggerMessageRequest = {
+        requestedMessage: MessageTriggerEnumType.MeterValues,
+      }
+      const response: OCPP20TriggerMessageResponse = {
+        status: TriggerMessageStatusEnumType.Accepted,
+      }
+
+      incomingRequestServiceForListener.emit(
+        OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
+        mockStation,
+        request,
+        response
+      )
+
+      assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+    })
+
+    await it('should broadcast StatusNotification for all EVSEs on Accepted without specific EVSE', () => {
+      const request: OCPP20TriggerMessageRequest = {
+        requestedMessage: MessageTriggerEnumType.StatusNotification,
+      }
+      const response: OCPP20TriggerMessageResponse = {
+        status: TriggerMessageStatusEnumType.Accepted,
+      }
+
+      incomingRequestServiceForListener.emit(
+        OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
+        mockStation,
+        request,
+        response
+      )
+
+      // 3 EVSEs (1, 2, 3) × 1 connector each = 3 StatusNotification calls
+      const callCount = requestHandlerMock.mock.callCount()
+      assert.strictEqual(callCount, 3)
+      for (const call of requestHandlerMock.mock.calls) {
+        const args = call.arguments as [
+          unknown,
+          string,
+          Record<string, unknown>,
+          Record<string, unknown>
+        ]
+        const [, command, payload, options] = args
+        assert.strictEqual(command, OCPP20RequestCommand.STATUS_NOTIFICATION)
+        assert.notStrictEqual(payload, undefined)
+        assert.ok('evseId' in payload, 'Expected payload to include evseId')
+        assert.ok('connectorId' in payload, 'Expected payload to include connectorId')
+        assert.ok('connectorStatus' in payload, 'Expected payload to include connectorStatus')
+        assert.ok('timestamp' in payload, 'Expected payload to include timestamp')
+        assert.ok((payload.evseId as number) > 0, 'Expected evseId > 0 (EVSE 0 excluded)')
+        assert.strictEqual(options.skipBufferingOnError, true)
+        assert.strictEqual(options.triggerMessage, true)
+      }
+    })
+
+    await it('should fire StatusNotification for specific EVSE and connector via listener', () => {
+      const request: OCPP20TriggerMessageRequest = {
+        evse: { connectorId: 1, id: 1 },
+        requestedMessage: MessageTriggerEnumType.StatusNotification,
+      }
+      const response: OCPP20TriggerMessageResponse = {
+        status: TriggerMessageStatusEnumType.Accepted,
+      }
+
+      incomingRequestServiceForListener.emit(
+        OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
+        mockStation,
+        request,
+        response
+      )
+
+      assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+      const args = requestHandlerMock.mock.calls[0].arguments as [
+        unknown,
+        string,
+        Record<string, unknown>,
+        Record<string, unknown>
+      ]
+      const [, command, payload, options] = args
+      assert.strictEqual(command, OCPP20RequestCommand.STATUS_NOTIFICATION)
+      assert.strictEqual(payload.evseId, 1)
+      assert.strictEqual(payload.connectorId, 1)
+      assert.ok('connectorStatus' in payload)
+      assert.ok(payload.timestamp instanceof Date)
+      assert.strictEqual(options.skipBufferingOnError, true)
+      assert.strictEqual(options.triggerMessage, true)
+    })
+
+    await it('should handle requestHandler rejection gracefully via errorHandler', async () => {
+      const rejectingMock = mock.fn(async () => Promise.reject(new Error('test error')))
+      const { station: rejectStation } = createMockChargingStation({
+        baseName: TEST_CHARGING_STATION_BASE_NAME,
+        connectorsCount: 3,
+        evseConfiguration: { evsesCount: 3 },
+        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+        ocppRequestService: {
+          requestHandler: rejectingMock,
+        },
+        stationInfo: {
+          ocppVersion: OCPPVersion.VERSION_201,
+        },
+        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+      })
+
+      const request: OCPP20TriggerMessageRequest = {
+        requestedMessage: MessageTriggerEnumType.Heartbeat,
+      }
+      const response: OCPP20TriggerMessageResponse = {
+        status: TriggerMessageStatusEnumType.Accepted,
+      }
+
+      incomingRequestServiceForListener.emit(
+        OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
+        rejectStation,
+        request,
+        response
+      )
 
-      const result = testableService.handleRequestTriggerMessage(mockStation, request)
+      // Flush microtask queue so .catch(errorHandler) executes
+      await Promise.resolve()
 
-      // A Promise would have a `then` property that is a function
-      assert.notStrictEqual(typeof (result as unknown as Promise<unknown>).then, 'function')
+      assert.strictEqual(rejectingMock.mock.callCount(), 1)
     })
   })
 })
index c9b5bb78bd97c4dd36f3fa9f86f7ca4bc2f31aa4..24213025f01e55042cae3c6b0d78a563a21a0ee4 100644 (file)
@@ -4,14 +4,16 @@
  */
 
 import assert from 'node:assert/strict'
-import { afterEach, beforeEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
 
 import type { ChargingStation } from '../../../../src/charging-station/index.js'
 
 import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
 import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
 import {
+  OCPP20IncomingRequestCommand,
   type OCPP20UpdateFirmwareRequest,
+  type OCPP20UpdateFirmwareResponse,
   OCPPVersion,
   UpdateFirmwareStatusEnumType,
 } from '../../../../src/types/index.js'
@@ -60,66 +62,101 @@ await describe('J02 - UpdateFirmware', async () => {
     assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
   })
 
-  await it('should return Accepted for request with signature field', () => {
+  await it('should register UPDATE_FIRMWARE event listener in constructor', () => {
+    const service = new OCPP20IncomingRequestService()
+    assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE), 1)
+  })
+
+  await it('should call simulateFirmwareUpdateLifecycle when UPDATE_FIRMWARE event emitted with Accepted response', () => {
+    const service = new OCPP20IncomingRequestService()
+    const simulateMock = mock.method(
+      service as unknown as {
+        simulateFirmwareUpdateLifecycle: (
+          chargingStation: ChargingStation,
+          requestId: number,
+          signature?: string
+        ) => Promise<void>
+      },
+      'simulateFirmwareUpdateLifecycle',
+      () => Promise.resolve()
+    )
+
     const request: OCPP20UpdateFirmwareRequest = {
       firmware: {
-        location: 'https://firmware.example.com/signed-update.bin',
+        location: 'https://firmware.example.com/update.bin',
         retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
-        signature: 'dGVzdC1zaWduYXR1cmU=',
-        signingCertificate: '-----BEGIN CERTIFICATE-----\nMIIBkTCB...',
+        signature: 'dGVzdA==',
       },
-      requestId: 2,
+      requestId: 42,
+    }
+    const response: OCPP20UpdateFirmwareResponse = {
+      status: UpdateFirmwareStatusEnumType.Accepted,
     }
 
-    const response = testableService.handleRequestUpdateFirmware(station, request)
+    service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
 
-    assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
+    assert.strictEqual(simulateMock.mock.callCount(), 1)
+    assert.strictEqual(simulateMock.mock.calls[0].arguments[1], 42)
+    assert.strictEqual(simulateMock.mock.calls[0].arguments[2], 'dGVzdA==')
   })
 
-  await it('should return Accepted for request without signature', () => {
-    const request: OCPP20UpdateFirmwareRequest = {
-      firmware: {
-        location: 'https://firmware.example.com/unsigned-update.bin',
-        retrieveDateTime: new Date('2025-01-15T12:00:00.000Z'),
+  await it('should NOT call simulateFirmwareUpdateLifecycle when UPDATE_FIRMWARE event emitted with Rejected response', () => {
+    const service = new OCPP20IncomingRequestService()
+    const simulateMock = mock.method(
+      service as unknown as {
+        simulateFirmwareUpdateLifecycle: (
+          chargingStation: ChargingStation,
+          requestId: number,
+          signature?: string
+        ) => Promise<void>
       },
-      requestId: 3,
-    }
-
-    const response = testableService.handleRequestUpdateFirmware(station, request)
+      'simulateFirmwareUpdateLifecycle',
+      () => Promise.resolve()
+    )
 
-    assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
-  })
-
-  await it('should pass through requestId correctly in the response', () => {
-    const testRequestId = 42
     const request: OCPP20UpdateFirmwareRequest = {
       firmware: {
         location: 'https://firmware.example.com/update.bin',
-        retrieveDateTime: new Date('2025-01-15T14:00:00.000Z'),
+        retrieveDateTime: new Date(),
       },
-      requestId: testRequestId,
+      requestId: 43,
+    }
+    const response: OCPP20UpdateFirmwareResponse = {
+      status: UpdateFirmwareStatusEnumType.Rejected,
     }
 
-    const response = testableService.handleRequestUpdateFirmware(station, request)
+    service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
 
-    assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
-    assert.strictEqual(typeof response.status, 'string')
+    assert.strictEqual(simulateMock.mock.callCount(), 0)
   })
 
-  await it('should return Accepted for request with retries and retryInterval', () => {
+  await it('should handle simulateFirmwareUpdateLifecycle rejection gracefully', async () => {
+    const service = new OCPP20IncomingRequestService()
+    mock.method(
+      service as unknown as {
+        simulateFirmwareUpdateLifecycle: (
+          chargingStation: ChargingStation,
+          requestId: number,
+          signature?: string
+        ) => Promise<void>
+      },
+      'simulateFirmwareUpdateLifecycle',
+      () => Promise.reject(new Error('firmware lifecycle error'))
+    )
+
     const request: OCPP20UpdateFirmwareRequest = {
       firmware: {
-        installDateTime: new Date('2025-01-15T16:00:00.000Z'),
-        location: 'https://firmware.example.com/update-retry.bin',
-        retrieveDateTime: new Date('2025-01-15T15:00:00.000Z'),
+        location: 'https://firmware.example.com/update.bin',
+        retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
       },
-      requestId: 5,
-      retries: 3,
-      retryInterval: 60,
+      requestId: 99,
+    }
+    const response: OCPP20UpdateFirmwareResponse = {
+      status: UpdateFirmwareStatusEnumType.Accepted,
     }
 
-    const response = testableService.handleRequestUpdateFirmware(station, request)
+    service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
 
-    assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
+    await Promise.resolve()
   })
 })
index bb367feb6a330fb7dc5d885eb8f095a1afd52107..5e98605f69d84c34c97a075679a4d550bd1c3830 100644 (file)
@@ -593,48 +593,6 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
           assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.Deauthorized)
         })
 
-        await it('should select CablePluggedIn for cable_action context with plugged_in', () => {
-          const context: OCPP20TransactionContext = {
-            cableState: 'plugged_in',
-            source: 'cable_action',
-          }
-
-          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-            OCPP20TransactionEventEnumType.Started,
-            context
-          )
-
-          assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.CablePluggedIn)
-        })
-
-        await it('should select EVDetected for cable_action context with detected', () => {
-          const context: OCPP20TransactionContext = {
-            cableState: 'detected',
-            source: 'cable_action',
-          }
-
-          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-            OCPP20TransactionEventEnumType.Updated,
-            context
-          )
-
-          assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.EVDetected)
-        })
-
-        await it('should select EVDeparted for cable_action context with unplugged', () => {
-          const context: OCPP20TransactionContext = {
-            cableState: 'unplugged',
-            source: 'cable_action',
-          }
-
-          const triggerReason = OCPP20ServiceUtils.selectTriggerReason(
-            OCPP20TransactionEventEnumType.Ended,
-            context
-          )
-
-          assert.strictEqual(triggerReason, OCPP20TriggerReasonEnumType.EVDeparted)
-        })
-
         await it('should select ChargingStateChanged for charging_state context', () => {
           const context: OCPP20TransactionContext = {
             chargingStateChange: {