]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(tests): separate handler/listener tests and remove setTimeout hacks
authorJérôme Benoit <jerome.benoit@sap.com>
Mon, 16 Mar 2026 17:58:26 +0000 (18:58 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Mon, 16 Mar 2026 17:59:00 +0000 (18:59 +0100)
Align TriggerMessage, UpdateFirmware, and GetLog test files with the
RequestStopTransaction reference pattern:
- Split into 'Handler validation' + 'event listener' describe groups
- Replace raw setTimeout delays with withMockTimers where needed
- Use emit() directly for listener tests (no wrapper helpers)
- Follows TEST_STYLE_GUIDE.md conventions

tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetLog.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-TriggerMessage.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UpdateFirmware.test.ts

index 2e02c056dcf554a6bc399333fbfa8a222c8eaebd..316e93fd322d84686268feb73696b38094cb0661 100644 (file)
@@ -54,236 +54,236 @@ await describe('K01 - GetLog', async () => {
     standardCleanup()
   })
 
-  await it('should return Accepted with filename for DiagnosticsLog request', () => {
-    const request: OCPP20GetLogRequest = {
-      log: {
-        remoteLocation: 'ftp://logs.example.com/uploads/',
-      },
-      logType: LogEnumType.DiagnosticsLog,
-      requestId: 1,
-    }
-
-    const response = testableService.handleRequestGetLog(station, request)
-
-    assert.notStrictEqual(response, undefined)
-    assert.strictEqual(typeof response, 'object')
-    assert.strictEqual(response.status, LogStatusEnumType.Accepted)
-    assert.strictEqual(response.filename, 'simulator-log.txt')
-  })
-
-  await it('should return Accepted with filename for SecurityLog request', () => {
-    const request: OCPP20GetLogRequest = {
-      log: {
-        remoteLocation: 'https://logs.example.com/security/',
-      },
-      logType: LogEnumType.SecurityLog,
-      requestId: 2,
-    }
-
-    const response = testableService.handleRequestGetLog(station, request)
-
-    assert.strictEqual(response.status, LogStatusEnumType.Accepted)
-    assert.strictEqual(response.filename, 'simulator-log.txt')
-  })
-
-  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: 'https://csms.example.com/logs',
-      },
-      logType: LogEnumType.DiagnosticsLog,
-      requestId: 10,
-    }
-    const response: OCPP20GetLogResponse = {
-      filename: 'simulator-log.txt',
-      status: LogStatusEnumType.Accepted,
-    }
-
-    service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
-
-    assert.strictEqual(simulateMock.mock.callCount(), 1)
-    assert.strictEqual(simulateMock.mock.calls[0].arguments[1], 10)
-  })
-
-  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()
-    )
+  await describe('Handler validation', async () => {
+    await it('should return Accepted with filename for DiagnosticsLog request', () => {
+      const request: OCPP20GetLogRequest = {
+        log: {
+          remoteLocation: 'ftp://logs.example.com/uploads/',
+        },
+        logType: LogEnumType.DiagnosticsLog,
+        requestId: 1,
+      }
+
+      const response = testableService.handleRequestGetLog(station, request)
+
+      assert.notStrictEqual(response, undefined)
+      assert.strictEqual(typeof response, 'object')
+      assert.strictEqual(response.status, LogStatusEnumType.Accepted)
+      assert.strictEqual(response.filename, 'simulator-log.txt')
+    })
 
-    const request: OCPP20GetLogRequest = {
-      log: {
-        remoteLocation: 'https://csms.example.com/logs',
-      },
-      logType: LogEnumType.DiagnosticsLog,
-      requestId: 11,
-    }
-    const response: OCPP20GetLogResponse = {
-      status: LogStatusEnumType.Rejected,
-    }
+    await it('should return Accepted with filename for SecurityLog request', () => {
+      const request: OCPP20GetLogRequest = {
+        log: {
+          remoteLocation: 'https://logs.example.com/security/',
+        },
+        logType: LogEnumType.SecurityLog,
+        requestId: 2,
+      }
 
-    service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
+      const response = testableService.handleRequestGetLog(station, request)
 
-    assert.strictEqual(simulateMock.mock.callCount(), 0)
+      assert.strictEqual(response.status, LogStatusEnumType.Accepted)
+      assert.strictEqual(response.filename, 'simulator-log.txt')
+    })
   })
 
-  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,
-    }
+  await describe('GET_LOG event listener', async () => {
+    await it('should register GET_LOG event listener in constructor', () => {
+      const service = new OCPP20IncomingRequestService()
+      assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.GET_LOG), 1)
+    })
 
-    service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
+    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: 'https://csms.example.com/logs',
+        },
+        logType: LogEnumType.DiagnosticsLog,
+        requestId: 10,
+      }
+      const response: OCPP20GetLogResponse = {
+        filename: 'simulator-log.txt',
+        status: LogStatusEnumType.Accepted,
+      }
+
+      service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
+
+      assert.strictEqual(simulateMock.mock.callCount(), 1)
+      assert.strictEqual(simulateMock.mock.calls[0].arguments[1], 10)
+    })
 
-    await Promise.resolve()
-  })
+    await it('should NOT call simulateLogUploadLifecycle when GET_LOG event emitted with Rejected response', () => {
+      const service = new OCPP20IncomingRequestService()
+      const simulateMock = mock.method(
+        service as unknown as {
+          simulateLogUploadLifecycle: (
+            chargingStation: ChargingStation,
+            requestId: number
+          ) => Promise<void>
+        },
+        'simulateLogUploadLifecycle',
+        () => Promise.resolve()
+      )
+
+      const request: OCPP20GetLogRequest = {
+        log: {
+          remoteLocation: 'https://csms.example.com/logs',
+        },
+        logType: LogEnumType.DiagnosticsLog,
+        requestId: 11,
+      }
+      const response: OCPP20GetLogResponse = {
+        status: LogStatusEnumType.Rejected,
+      }
+
+      service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
+
+      assert.strictEqual(simulateMock.mock.callCount(), 0)
+    })
 
-  await describe('N01 - LogStatusNotification lifecycle', async () => {
-    afterEach(() => {
-      standardCleanup()
+    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()
     })
 
-    await it('should send Uploading notification with correct requestId', async t => {
-      await withMockTimers(t, ['setTimeout'], async () => {
-        // Arrange
-        const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
-        const service = new OCPP20IncomingRequestService()
-        const request: OCPP20GetLogRequest = {
-          log: { remoteLocation: 'ftp://logs.example.com/' },
-          logType: LogEnumType.DiagnosticsLog,
-          requestId: 42,
-        }
-        const response: OCPP20GetLogResponse = {
-          filename: 'simulator-log.txt',
-          status: LogStatusEnumType.Accepted,
-        }
-
-        // Act
-        service.emit(OCPP20IncomingRequestCommand.GET_LOG, trackingStation, request, response)
-        await flushMicrotasks()
-
-        // Assert
-        assert.ok(sentRequests.length >= 1, 'Expected at least one notification')
-        assert.deepStrictEqual(sentRequests[0].payload, {
-          requestId: 42,
-          status: UploadLogStatusEnumType.Uploading,
+    await describe('N01 - LogStatusNotification lifecycle', async () => {
+      await it('should send Uploading notification with correct requestId', async t => {
+        await withMockTimers(t, ['setTimeout'], async () => {
+          // Arrange
+          const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+          const service = new OCPP20IncomingRequestService()
+          const request: OCPP20GetLogRequest = {
+            log: { remoteLocation: 'ftp://logs.example.com/' },
+            logType: LogEnumType.DiagnosticsLog,
+            requestId: 42,
+          }
+          const response: OCPP20GetLogResponse = {
+            filename: 'simulator-log.txt',
+            status: LogStatusEnumType.Accepted,
+          }
+
+          // Act
+          service.emit(OCPP20IncomingRequestCommand.GET_LOG, trackingStation, request, response)
+          await flushMicrotasks()
+
+          // Assert
+          assert.ok(sentRequests.length >= 1, 'Expected at least one notification')
+          assert.deepStrictEqual(sentRequests[0].payload, {
+            requestId: 42,
+            status: UploadLogStatusEnumType.Uploading,
+          })
         })
       })
-    })
 
-    await it('should send Uploaded notification with correct requestId after delay', async t => {
-      await withMockTimers(t, ['setTimeout'], async () => {
-        // Arrange
-        const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
-        const service = new OCPP20IncomingRequestService()
-        const request: OCPP20GetLogRequest = {
-          log: { remoteLocation: 'ftp://logs.example.com/' },
-          logType: LogEnumType.DiagnosticsLog,
-          requestId: 42,
-        }
-        const response: OCPP20GetLogResponse = {
-          filename: 'simulator-log.txt',
-          status: LogStatusEnumType.Accepted,
-        }
-
-        // Act
-        service.emit(OCPP20IncomingRequestCommand.GET_LOG, trackingStation, request, response)
-        await flushMicrotasks()
-
-        // Only Uploading should be sent before the timer fires
-        assert.strictEqual(sentRequests.length, 1)
-
-        // Advance past the simulated upload delay
-        t.mock.timers.tick(1000)
-        await flushMicrotasks()
-
-        // Assert
-        assert.strictEqual(sentRequests.length, 2)
-        assert.deepStrictEqual(sentRequests[1].payload, {
-          requestId: 42,
-          status: UploadLogStatusEnumType.Uploaded,
+      await it('should send Uploaded notification with correct requestId after delay', async t => {
+        await withMockTimers(t, ['setTimeout'], async () => {
+          // Arrange
+          const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+          const service = new OCPP20IncomingRequestService()
+          const request: OCPP20GetLogRequest = {
+            log: { remoteLocation: 'ftp://logs.example.com/' },
+            logType: LogEnumType.DiagnosticsLog,
+            requestId: 42,
+          }
+          const response: OCPP20GetLogResponse = {
+            filename: 'simulator-log.txt',
+            status: LogStatusEnumType.Accepted,
+          }
+
+          // Act
+          service.emit(OCPP20IncomingRequestCommand.GET_LOG, trackingStation, request, response)
+          await flushMicrotasks()
+
+          // Only Uploading should be sent before the timer fires
+          assert.strictEqual(sentRequests.length, 1)
+
+          // Advance past the simulated upload delay
+          t.mock.timers.tick(1000)
+          await flushMicrotasks()
+
+          // Assert
+          assert.strictEqual(sentRequests.length, 2)
+          assert.deepStrictEqual(sentRequests[1].payload, {
+            requestId: 42,
+            status: UploadLogStatusEnumType.Uploaded,
+          })
         })
       })
-    })
 
-    await it('should send Uploading before Uploaded in correct sequence', async t => {
-      await withMockTimers(t, ['setTimeout'], async () => {
-        // Arrange
-        const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
-        const service = new OCPP20IncomingRequestService()
-        const request: OCPP20GetLogRequest = {
-          log: { remoteLocation: 'ftp://logs.example.com/' },
-          logType: LogEnumType.DiagnosticsLog,
-          requestId: 7,
-        }
-        const response: OCPP20GetLogResponse = {
-          filename: 'simulator-log.txt',
-          status: LogStatusEnumType.Accepted,
-        }
-
-        // Act - complete the full lifecycle
-        service.emit(OCPP20IncomingRequestCommand.GET_LOG, trackingStation, request, response)
-        await flushMicrotasks()
-        t.mock.timers.tick(1000)
-        await flushMicrotasks()
-
-        // Assert - verify sequence and requestId propagation
-        assert.strictEqual(sentRequests.length, 2)
-        assert.strictEqual(
-          sentRequests[0].payload.status,
-          UploadLogStatusEnumType.Uploading,
-          'First notification should be Uploading'
-        )
-        assert.strictEqual(
-          sentRequests[1].payload.status,
-          UploadLogStatusEnumType.Uploaded,
-          'Second notification should be Uploaded'
-        )
-        assert.strictEqual(sentRequests[0].payload.requestId, 7)
-        assert.strictEqual(sentRequests[1].payload.requestId, 7)
+      await it('should send Uploading before Uploaded in correct sequence', async t => {
+        await withMockTimers(t, ['setTimeout'], async () => {
+          // Arrange
+          const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+          const service = new OCPP20IncomingRequestService()
+          const request: OCPP20GetLogRequest = {
+            log: { remoteLocation: 'ftp://logs.example.com/' },
+            logType: LogEnumType.DiagnosticsLog,
+            requestId: 7,
+          }
+          const response: OCPP20GetLogResponse = {
+            filename: 'simulator-log.txt',
+            status: LogStatusEnumType.Accepted,
+          }
+
+          // Act - complete the full lifecycle
+          service.emit(OCPP20IncomingRequestCommand.GET_LOG, trackingStation, request, response)
+          await flushMicrotasks()
+          t.mock.timers.tick(1000)
+          await flushMicrotasks()
+
+          // Assert - verify sequence and requestId propagation
+          assert.strictEqual(sentRequests.length, 2)
+          assert.strictEqual(
+            sentRequests[0].payload.status,
+            UploadLogStatusEnumType.Uploading,
+            'First notification should be Uploading'
+          )
+          assert.strictEqual(
+            sentRequests[1].payload.status,
+            UploadLogStatusEnumType.Uploaded,
+            'Second notification should be Uploaded'
+          )
+          assert.strictEqual(sentRequests[0].payload.requestId, 7)
+          assert.strictEqual(sentRequests[1].payload.requestId, 7)
+        })
       })
     })
   })
index 8570e1d433e1ff9d9967495181c56d015645e384..02b100d62e7ed1e6b0f7f69017a8e405f2b90587 100644 (file)
@@ -60,7 +60,6 @@ await describe('F06 - TriggerMessage', async () => {
   let testableService: ReturnType<typeof createTestableIncomingRequestService>
 
   beforeEach(() => {
-    mock.timers.enable({ apis: ['setInterval', 'setTimeout'] })
     incomingRequestService = new OCPP20IncomingRequestService()
     testableService = createTestableIncomingRequestService(incomingRequestService)
   })
index f4b60c802155c54b960bee030d8f7557b98a2bfe..f3830d69d6d27a628588323882f5f023204ebc60 100644 (file)
@@ -58,121 +58,23 @@ await describe('L01/L02 - UpdateFirmware', async () => {
     standardCleanup()
   })
 
-  await it('should return Accepted for valid firmware update request', () => {
-    const request: OCPP20UpdateFirmwareRequest = {
-      firmware: {
-        location: 'https://firmware.example.com/update-v2.0.bin',
-        retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
-      },
-      requestId: 1,
-    }
-
-    const response = testableService.handleRequestUpdateFirmware(station, request)
-
-    assert.notStrictEqual(response, undefined)
-    assert.strictEqual(typeof response, 'object')
-    assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
-  })
-
-  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,
-          firmware: FirmwareType
-        ) => Promise<void>
-      },
-      'simulateFirmwareUpdateLifecycle',
-      () => Promise.resolve()
-    )
-
-    const request: OCPP20UpdateFirmwareRequest = {
-      firmware: {
-        location: 'https://firmware.example.com/update.bin',
-        retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
-        signature: 'dGVzdA==',
-      },
-      requestId: 42,
-    }
-    const response: OCPP20UpdateFirmwareResponse = {
-      status: UpdateFirmwareStatusEnumType.Accepted,
-    }
-
-    service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
-
-    assert.strictEqual(simulateMock.mock.callCount(), 1)
-    assert.strictEqual(simulateMock.mock.calls[0].arguments[1], 42)
-    assert.deepStrictEqual(simulateMock.mock.calls[0].arguments[2], request.firmware)
-  })
-
-  await it('should NOT call simulateFirmwareUpdateLifecycle when UPDATE_FIRMWARE event emitted with Rejected response', () => {
-    const service = new OCPP20IncomingRequestService()
-    const simulateMock = mock.method(
-      service as unknown as {
-        simulateFirmwareUpdateLifecycle: (
-          chargingStation: ChargingStation,
-          requestId: number,
-          firmware: FirmwareType
-        ) => Promise<void>
-      },
-      'simulateFirmwareUpdateLifecycle',
-      () => Promise.resolve()
-    )
-
-    const request: OCPP20UpdateFirmwareRequest = {
-      firmware: {
-        location: 'https://firmware.example.com/update.bin',
-        retrieveDateTime: new Date(),
-      },
-      requestId: 43,
-    }
-    const response: OCPP20UpdateFirmwareResponse = {
-      status: UpdateFirmwareStatusEnumType.Rejected,
-    }
-
-    service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
-
-    assert.strictEqual(simulateMock.mock.callCount(), 0)
-  })
-
-  await it('should handle simulateFirmwareUpdateLifecycle rejection gracefully', async () => {
-    const service = new OCPP20IncomingRequestService()
-    mock.method(
-      service as unknown as {
-        simulateFirmwareUpdateLifecycle: (
-          chargingStation: ChargingStation,
-          requestId: number,
-          firmware: FirmwareType
-        ) => Promise<void>
-      },
-      'simulateFirmwareUpdateLifecycle',
-      () => Promise.reject(new Error('firmware lifecycle error'))
-    )
-
-    const request: OCPP20UpdateFirmwareRequest = {
-      firmware: {
-        location: 'https://firmware.example.com/update.bin',
-        retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
-      },
-      requestId: 99,
-    }
-    const response: OCPP20UpdateFirmwareResponse = {
-      status: UpdateFirmwareStatusEnumType.Accepted,
-    }
+  await describe('Handler validation', async () => {
+    await it('should return Accepted for valid firmware update request', () => {
+      const request: OCPP20UpdateFirmwareRequest = {
+        firmware: {
+          location: 'https://firmware.example.com/update-v2.0.bin',
+          retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
+        },
+        requestId: 1,
+      }
 
-    service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
+      const response = testableService.handleRequestUpdateFirmware(station, request)
 
-    await Promise.resolve()
-  })
+      assert.notStrictEqual(response, undefined)
+      assert.strictEqual(typeof response, 'object')
+      assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
+    })
 
-  await describe('Security Features', async () => {
     await it('should return InvalidCertificate for invalid signing certificate PEM', () => {
       // Arrange
       const certManager = createMockCertificateManager()
@@ -322,191 +224,120 @@ await describe('L01/L02 - UpdateFirmware', async () => {
       // Assert
       assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
     })
+  })
 
-    await it('should cancel previous firmware update when new one arrives', async t => {
-      const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+  await describe('UPDATE_FIRMWARE event listener', async () => {
+    await it('should register UPDATE_FIRMWARE event listener in constructor', () => {
       const service = new OCPP20IncomingRequestService()
-      const testable = createTestableIncomingRequestService(service)
-
-      const firstRequest: OCPP20UpdateFirmwareRequest = {
-        firmware: {
-          location: 'https://firmware.example.com/v1.bin',
-          retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
-        },
-        requestId: 100,
-      }
-      const firstResponse: OCPP20UpdateFirmwareResponse = {
-        status: UpdateFirmwareStatusEnumType.Accepted,
-      }
-
-      await withMockTimers(t, ['setTimeout'], async () => {
-        service.emit(
-          OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
-          trackingStation,
-          firstRequest,
-          firstResponse
-        )
-
-        await flushMicrotasks()
-        assert.strictEqual(sentRequests.length, 1)
-        assert.strictEqual(sentRequests[0].payload.status, OCPP20FirmwareStatusEnumType.Downloading)
-
-        const secondRequest: OCPP20UpdateFirmwareRequest = {
-          firmware: {
-            location: 'https://firmware.example.com/v2.bin',
-            retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
-          },
-          requestId: 101,
-        }
-
-        const secondResponse = testable.handleRequestUpdateFirmware(trackingStation, secondRequest)
-        assert.strictEqual(secondResponse.status, UpdateFirmwareStatusEnumType.AcceptedCanceled)
-
-        await flushMicrotasks()
-      })
+      assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE), 1)
     })
-  })
 
-  await describe('Firmware Update Lifecycle', async () => {
-    await it('should send full lifecycle Downloading→Downloaded→Installing→Installed + SecurityEvent', async t => {
-      const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+    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,
+            firmware: FirmwareType
+          ) => Promise<void>
+        },
+        'simulateFirmwareUpdateLifecycle',
+        () => Promise.resolve()
+      )
 
       const request: OCPP20UpdateFirmwareRequest = {
         firmware: {
           location: 'https://firmware.example.com/update.bin',
-          retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+          retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
+          signature: 'dGVzdA==',
         },
-        requestId: 1,
+        requestId: 42,
       }
       const response: OCPP20UpdateFirmwareResponse = {
         status: UpdateFirmwareStatusEnumType.Accepted,
       }
 
-      await withMockTimers(t, ['setTimeout'], async () => {
-        service.emit(
-          OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
-          trackingStation,
-          request,
-          response
-        )
+      service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
 
-        await flushMicrotasks()
-        assert.strictEqual(sentRequests.length, 1)
-        assert.strictEqual(
-          sentRequests[0].command,
-          OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION
-        )
-        assert.strictEqual(sentRequests[0].payload.status, OCPP20FirmwareStatusEnumType.Downloading)
-        assert.strictEqual(sentRequests[0].payload.requestId, 1)
-
-        t.mock.timers.tick(2000)
-        await flushMicrotasks()
-        assert.strictEqual(sentRequests.length, 3)
-        assert.strictEqual(sentRequests[1].payload.status, OCPP20FirmwareStatusEnumType.Downloaded)
-        assert.strictEqual(sentRequests[2].payload.status, OCPP20FirmwareStatusEnumType.Installing)
-
-        t.mock.timers.tick(1000)
-        await flushMicrotasks()
-        assert.strictEqual(sentRequests[3].payload.status, OCPP20FirmwareStatusEnumType.Installed)
-
-        // H11: SecurityEventNotification for FirmwareUpdated
-        assert.strictEqual(sentRequests.length, 5)
-        assert.strictEqual(
-          sentRequests[4].command,
-          OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION
-        )
-        assert.strictEqual(sentRequests[4].payload.type, 'FirmwareUpdated')
-      })
+      assert.strictEqual(simulateMock.mock.callCount(), 1)
+      assert.strictEqual(simulateMock.mock.calls[0].arguments[1], 42)
+      assert.deepStrictEqual(simulateMock.mock.calls[0].arguments[2], request.firmware)
     })
 
-    await it('should send DownloadFailed for empty firmware location', async t => {
-      const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+    await it('should NOT call simulateFirmwareUpdateLifecycle when UPDATE_FIRMWARE event emitted with Rejected response', () => {
       const service = new OCPP20IncomingRequestService()
+      const simulateMock = mock.method(
+        service as unknown as {
+          simulateFirmwareUpdateLifecycle: (
+            chargingStation: ChargingStation,
+            requestId: number,
+            firmware: FirmwareType
+          ) => Promise<void>
+        },
+        'simulateFirmwareUpdateLifecycle',
+        () => Promise.resolve()
+      )
 
       const request: OCPP20UpdateFirmwareRequest = {
         firmware: {
-          location: '',
-          retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+          location: 'https://firmware.example.com/update.bin',
+          retrieveDateTime: new Date(),
         },
-        requestId: 7,
+        requestId: 43,
       }
       const response: OCPP20UpdateFirmwareResponse = {
-        status: UpdateFirmwareStatusEnumType.Accepted,
+        status: UpdateFirmwareStatusEnumType.Rejected,
       }
 
-      await withMockTimers(t, ['setTimeout'], async () => {
-        service.emit(
-          OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
-          trackingStation,
-          request,
-          response
-        )
-
-        await flushMicrotasks()
-        assert.strictEqual(sentRequests.length, 1)
-        assert.strictEqual(sentRequests[0].payload.status, OCPP20FirmwareStatusEnumType.Downloading)
+      service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
 
-        t.mock.timers.tick(2000)
-        await flushMicrotasks()
-        assert.strictEqual(sentRequests.length, 2)
-        assert.strictEqual(
-          sentRequests[1].payload.status,
-          OCPP20FirmwareStatusEnumType.DownloadFailed
-        )
-        assert.strictEqual(sentRequests[1].payload.requestId, 7)
-      })
+      assert.strictEqual(simulateMock.mock.callCount(), 0)
     })
 
-    await it('should send DownloadFailed for malformed firmware location', async t => {
-      const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+    await it('should handle simulateFirmwareUpdateLifecycle rejection gracefully', async () => {
       const service = new OCPP20IncomingRequestService()
+      mock.method(
+        service as unknown as {
+          simulateFirmwareUpdateLifecycle: (
+            chargingStation: ChargingStation,
+            requestId: number,
+            firmware: FirmwareType
+          ) => Promise<void>
+        },
+        'simulateFirmwareUpdateLifecycle',
+        () => Promise.reject(new Error('firmware lifecycle error'))
+      )
 
       const request: OCPP20UpdateFirmwareRequest = {
         firmware: {
-          location: 'not-a-valid-url',
-          retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+          location: 'https://firmware.example.com/update.bin',
+          retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
         },
-        requestId: 8,
+        requestId: 99,
       }
       const response: OCPP20UpdateFirmwareResponse = {
         status: UpdateFirmwareStatusEnumType.Accepted,
       }
 
-      await withMockTimers(t, ['setTimeout'], async () => {
-        service.emit(
-          OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
-          trackingStation,
-          request,
-          response
-        )
-
-        await flushMicrotasks()
-        t.mock.timers.tick(2000)
-        await flushMicrotasks()
+      service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
 
-        assert.strictEqual(sentRequests.length, 2)
-        assert.strictEqual(
-          sentRequests[1].payload.status,
-          OCPP20FirmwareStatusEnumType.DownloadFailed
-        )
-        assert.strictEqual(sentRequests[1].payload.requestId, 8)
-      })
+      await Promise.resolve()
     })
 
-    await it('should include requestId in all firmware status notifications', async t => {
+    await it('should cancel previous firmware update when new one arrives', async t => {
       const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
       const service = new OCPP20IncomingRequestService()
-      const expectedRequestId = 42
+      const testable = createTestableIncomingRequestService(service)
 
-      const request: OCPP20UpdateFirmwareRequest = {
+      const firstRequest: OCPP20UpdateFirmwareRequest = {
         firmware: {
-          location: 'https://firmware.example.com/update.bin',
+          location: 'https://firmware.example.com/v1.bin',
           retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
         },
-        requestId: expectedRequestId,
+        requestId: 100,
       }
-      const response: OCPP20UpdateFirmwareResponse = {
+      const firstResponse: OCPP20UpdateFirmwareResponse = {
         status: UpdateFirmwareStatusEnumType.Accepted,
       }
 
@@ -514,79 +345,268 @@ await describe('L01/L02 - UpdateFirmware', async () => {
         service.emit(
           OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
           trackingStation,
-          request,
-          response
+          firstRequest,
+          firstResponse
         )
 
         await flushMicrotasks()
-        t.mock.timers.tick(2000)
-        await flushMicrotasks()
-        t.mock.timers.tick(1000)
+        assert.strictEqual(sentRequests.length, 1)
+        assert.strictEqual(sentRequests[0].payload.status, OCPP20FirmwareStatusEnumType.Downloading)
+
+        const secondRequest: OCPP20UpdateFirmwareRequest = {
+          firmware: {
+            location: 'https://firmware.example.com/v2.bin',
+            retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+          },
+          requestId: 101,
+        }
+
+        const secondResponse = testable.handleRequestUpdateFirmware(trackingStation, secondRequest)
+        assert.strictEqual(secondResponse.status, UpdateFirmwareStatusEnumType.AcceptedCanceled)
+
         await flushMicrotasks()
+      })
+    })
 
-        const firmwareNotifications = sentRequests.filter(
-          r =>
-            (r.command as OCPP20RequestCommand) ===
+    await describe('Firmware Update Lifecycle', async () => {
+      await it('should send full lifecycle Downloading→Downloaded→Installing→Installed + SecurityEvent', async t => {
+        const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+        const service = new OCPP20IncomingRequestService()
+
+        const request: OCPP20UpdateFirmwareRequest = {
+          firmware: {
+            location: 'https://firmware.example.com/update.bin',
+            retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+          },
+          requestId: 1,
+        }
+        const response: OCPP20UpdateFirmwareResponse = {
+          status: UpdateFirmwareStatusEnumType.Accepted,
+        }
+
+        await withMockTimers(t, ['setTimeout'], async () => {
+          service.emit(
+            OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
+            trackingStation,
+            request,
+            response
+          )
+
+          await flushMicrotasks()
+          assert.strictEqual(sentRequests.length, 1)
+          assert.strictEqual(
+            sentRequests[0].command,
             OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION
-        )
-        assert.strictEqual(firmwareNotifications.length, 4)
-        for (const req of firmwareNotifications) {
-          assert.strictEqual(req.payload.requestId, expectedRequestId)
+          )
+          assert.strictEqual(
+            sentRequests[0].payload.status,
+            OCPP20FirmwareStatusEnumType.Downloading
+          )
+          assert.strictEqual(sentRequests[0].payload.requestId, 1)
+
+          t.mock.timers.tick(2000)
+          await flushMicrotasks()
+          assert.strictEqual(sentRequests.length, 3)
+          assert.strictEqual(
+            sentRequests[1].payload.status,
+            OCPP20FirmwareStatusEnumType.Downloaded
+          )
+          assert.strictEqual(
+            sentRequests[2].payload.status,
+            OCPP20FirmwareStatusEnumType.Installing
+          )
+
+          t.mock.timers.tick(1000)
+          await flushMicrotasks()
+          assert.strictEqual(sentRequests[3].payload.status, OCPP20FirmwareStatusEnumType.Installed)
+
+          // H11: SecurityEventNotification for FirmwareUpdated
+          assert.strictEqual(sentRequests.length, 5)
+          assert.strictEqual(
+            sentRequests[4].command,
+            OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION
+          )
+          assert.strictEqual(sentRequests[4].payload.type, 'FirmwareUpdated')
+        })
+      })
+
+      await it('should send DownloadFailed for empty firmware location', async t => {
+        const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+        const service = new OCPP20IncomingRequestService()
+
+        const request: OCPP20UpdateFirmwareRequest = {
+          firmware: {
+            location: '',
+            retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+          },
+          requestId: 7,
+        }
+        const response: OCPP20UpdateFirmwareResponse = {
+          status: UpdateFirmwareStatusEnumType.Accepted,
         }
+
+        await withMockTimers(t, ['setTimeout'], async () => {
+          service.emit(
+            OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
+            trackingStation,
+            request,
+            response
+          )
+
+          await flushMicrotasks()
+          assert.strictEqual(sentRequests.length, 1)
+          assert.strictEqual(
+            sentRequests[0].payload.status,
+            OCPP20FirmwareStatusEnumType.Downloading
+          )
+
+          t.mock.timers.tick(2000)
+          await flushMicrotasks()
+          assert.strictEqual(sentRequests.length, 2)
+          assert.strictEqual(
+            sentRequests[1].payload.status,
+            OCPP20FirmwareStatusEnumType.DownloadFailed
+          )
+          assert.strictEqual(sentRequests[1].payload.requestId, 7)
+        })
       })
-    })
 
-    await it('should include SignatureVerified when firmware has signature', async t => {
-      const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
-      const service = new OCPP20IncomingRequestService()
+      await it('should send DownloadFailed for malformed firmware location', async t => {
+        const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+        const service = new OCPP20IncomingRequestService()
 
-      const request: OCPP20UpdateFirmwareRequest = {
-        firmware: {
-          location: 'https://firmware.example.com/update.bin',
-          retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
-          signature: 'dGVzdA==',
-        },
-        requestId: 5,
-      }
-      const response: OCPP20UpdateFirmwareResponse = {
-        status: UpdateFirmwareStatusEnumType.Accepted,
-      }
+        const request: OCPP20UpdateFirmwareRequest = {
+          firmware: {
+            location: 'not-a-valid-url',
+            retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+          },
+          requestId: 8,
+        }
+        const response: OCPP20UpdateFirmwareResponse = {
+          status: UpdateFirmwareStatusEnumType.Accepted,
+        }
 
-      await withMockTimers(t, ['setTimeout'], async () => {
-        service.emit(
-          OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
-          trackingStation,
-          request,
-          response
-        )
+        await withMockTimers(t, ['setTimeout'], async () => {
+          service.emit(
+            OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
+            trackingStation,
+            request,
+            response
+          )
+
+          await flushMicrotasks()
+          t.mock.timers.tick(2000)
+          await flushMicrotasks()
+
+          assert.strictEqual(sentRequests.length, 2)
+          assert.strictEqual(
+            sentRequests[1].payload.status,
+            OCPP20FirmwareStatusEnumType.DownloadFailed
+          )
+          assert.strictEqual(sentRequests[1].payload.requestId, 8)
+        })
+      })
 
-        await flushMicrotasks()
-        assert.strictEqual(sentRequests.length, 1)
+      await it('should include requestId in all firmware status notifications', async t => {
+        const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+        const service = new OCPP20IncomingRequestService()
+        const expectedRequestId = 42
 
-        t.mock.timers.tick(2000)
-        await flushMicrotasks()
-        assert.strictEqual(sentRequests.length, 2)
-        assert.strictEqual(sentRequests[1].payload.status, OCPP20FirmwareStatusEnumType.Downloaded)
+        const request: OCPP20UpdateFirmwareRequest = {
+          firmware: {
+            location: 'https://firmware.example.com/update.bin',
+            retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+          },
+          requestId: expectedRequestId,
+        }
+        const response: OCPP20UpdateFirmwareResponse = {
+          status: UpdateFirmwareStatusEnumType.Accepted,
+        }
 
-        t.mock.timers.tick(500)
-        await flushMicrotasks()
-        assert.strictEqual(
-          sentRequests[2].payload.status,
-          OCPP20FirmwareStatusEnumType.SignatureVerified
-        )
-        assert.strictEqual(sentRequests[3].payload.status, OCPP20FirmwareStatusEnumType.Installing)
+        await withMockTimers(t, ['setTimeout'], async () => {
+          service.emit(
+            OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
+            trackingStation,
+            request,
+            response
+          )
+
+          await flushMicrotasks()
+          t.mock.timers.tick(2000)
+          await flushMicrotasks()
+          t.mock.timers.tick(1000)
+          await flushMicrotasks()
+
+          const firmwareNotifications = sentRequests.filter(
+            r =>
+              (r.command as OCPP20RequestCommand) ===
+              OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION
+          )
+          assert.strictEqual(firmwareNotifications.length, 4)
+          for (const req of firmwareNotifications) {
+            assert.strictEqual(req.payload.requestId, expectedRequestId)
+          }
+        })
+      })
 
-        t.mock.timers.tick(1000)
-        await flushMicrotasks()
-        assert.strictEqual(sentRequests[4].payload.status, OCPP20FirmwareStatusEnumType.Installed)
+      await it('should include SignatureVerified when firmware has signature', async t => {
+        const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+        const service = new OCPP20IncomingRequestService()
 
-        // H11: SecurityEventNotification after Installed
-        assert.strictEqual(sentRequests.length, 6)
-        assert.strictEqual(
-          sentRequests[5].command,
-          OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION
-        )
-        assert.strictEqual(sentRequests[5].payload.type, 'FirmwareUpdated')
+        const request: OCPP20UpdateFirmwareRequest = {
+          firmware: {
+            location: 'https://firmware.example.com/update.bin',
+            retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+            signature: 'dGVzdA==',
+          },
+          requestId: 5,
+        }
+        const response: OCPP20UpdateFirmwareResponse = {
+          status: UpdateFirmwareStatusEnumType.Accepted,
+        }
+
+        await withMockTimers(t, ['setTimeout'], async () => {
+          service.emit(
+            OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
+            trackingStation,
+            request,
+            response
+          )
+
+          await flushMicrotasks()
+          assert.strictEqual(sentRequests.length, 1)
+
+          t.mock.timers.tick(2000)
+          await flushMicrotasks()
+          assert.strictEqual(sentRequests.length, 2)
+          assert.strictEqual(
+            sentRequests[1].payload.status,
+            OCPP20FirmwareStatusEnumType.Downloaded
+          )
+
+          t.mock.timers.tick(500)
+          await flushMicrotasks()
+          assert.strictEqual(
+            sentRequests[2].payload.status,
+            OCPP20FirmwareStatusEnumType.SignatureVerified
+          )
+          assert.strictEqual(
+            sentRequests[3].payload.status,
+            OCPP20FirmwareStatusEnumType.Installing
+          )
+
+          t.mock.timers.tick(1000)
+          await flushMicrotasks()
+          assert.strictEqual(sentRequests[4].payload.status, OCPP20FirmwareStatusEnumType.Installed)
+
+          // H11: SecurityEventNotification after Installed
+          assert.strictEqual(sentRequests.length, 6)
+          assert.strictEqual(
+            sentRequests[5].command,
+            OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION
+          )
+          assert.strictEqual(sentRequests[5].payload.type, 'FirmwareUpdated')
+        })
       })
     })
   })