]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(tests): split RequestStopTransaction tests into handler and listener groups
authorJérôme Benoit <jerome.benoit@sap.com>
Mon, 16 Mar 2026 17:39:51 +0000 (18:39 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Mon, 16 Mar 2026 17:39:51 +0000 (18:39 +0100)
Remove emitStopEvent helper with fragile setTimeout(50) hack.
Handler tests call handleRequestStopTransaction directly.
Listener tests use emit() directly, matching the TriggerMessage
test pattern. 1856 tests (+3 new listener-specific tests).

tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts

index 23b868c6f9003e546db10d856f2f23d3980c2579..8052900f3b6924632c7fb23d1006cf7ccf56fabf 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'
 import type {
@@ -41,38 +41,39 @@ import {
   resetReportingValueSize,
 } from './OCPP20TestUtils.js'
 
+/**
+ * @param baseName
+ * @returns The mock station and its request handler spy
+ */
+function createListenerStation (baseName: string): {
+  requestHandlerMock: ReturnType<typeof mock.fn>
+  station: ChargingStation
+} {
+  const requestHandlerMock = mock.fn(async () => Promise.resolve({}))
+  const { station } = createMockChargingStation({
+    baseName,
+    connectorsCount: 3,
+    evseConfiguration: { evsesCount: 3 },
+    heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+    ocppRequestService: {
+      requestHandler: requestHandlerMock,
+    },
+    stationInfo: {
+      ocppStrictCompliance: false,
+      ocppVersion: OCPPVersion.VERSION_201,
+    },
+    websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+  })
+  return { requestHandlerMock, station }
+}
+
 await describe('F03 - Remote Stop Transaction', async () => {
-  let sentTransactionEvents: OCPP20TransactionEventRequest[] = []
   let mockStation: ChargingStation
   let incomingRequestService: OCPP20IncomingRequestService
   let testableService: ReturnType<typeof createTestableIncomingRequestService>
 
   beforeEach(() => {
-    sentTransactionEvents = []
-    const { station } = createMockChargingStation({
-      baseName: TEST_CHARGING_STATION_BASE_NAME,
-      connectorsCount: 3,
-      evseConfiguration: { evsesCount: 3 },
-      heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
-      ocppRequestService: {
-        requestHandler: async (
-          _chargingStation: unknown,
-          commandName: unknown,
-          commandPayload: unknown
-        ) => {
-          if (commandName === OCPP20RequestCommand.TRANSACTION_EVENT) {
-            sentTransactionEvents.push(commandPayload as OCPP20TransactionEventRequest)
-            return Promise.resolve({})
-          }
-          return Promise.resolve({})
-        },
-      },
-      stationInfo: {
-        ocppStrictCompliance: false,
-        ocppVersion: OCPPVersion.VERSION_201,
-      },
-      websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
-    })
+    const { station } = createListenerStation(TEST_CHARGING_STATION_BASE_NAME)
     mockStation = station
     incomingRequestService = new OCPP20IncomingRequestService()
     testableService = createTestableIncomingRequestService(incomingRequestService)
@@ -88,20 +89,20 @@ await describe('F03 - Remote Stop Transaction', async () => {
   })
 
   /**
-   * Helper function to start a transaction and return the transaction ID
-   * @param evseId - The EVSE ID to start transaction on
-   * @param remoteStartId - The remote start ID for the transaction
-   * @param skipReset - Whether to skip resetting connector states
+   * @param station
+   * @param evseId
+   * @param remoteStartId
+   * @param skipReset
    * @returns The transaction ID of the started transaction
    */
   async function startTransaction (
+    station: ChargingStation,
     evseId = 1,
     remoteStartId = 1,
     skipReset = false
   ): Promise<string> {
-    // Reset all connector states first to ensure clean state (unless skipped for multiple transactions)
     if (!skipReset) {
-      resetConnectorTransactionState(mockStation)
+      resetConnectorTransactionState(station)
     }
 
     const startRequest: OCPP20RequestStartTransactionRequest = {
@@ -113,425 +114,341 @@ await describe('F03 - Remote Stop Transaction', async () => {
       remoteStartId,
     }
 
-    const startResponse = await testableService.handleRequestStartTransaction(
-      mockStation,
-      startRequest
-    )
+    const startResponse = await testableService.handleRequestStartTransaction(station, startRequest)
 
     assert.strictEqual(startResponse.status, RequestStartStopStatusEnumType.Accepted)
     assert.notStrictEqual(startResponse.transactionId, undefined)
     return startResponse.transactionId as string
   }
 
-  /**
-   * Emits the REQUEST_STOP_TRANSACTION event on the service (mimicking
-   * the base class `handleIncomingRequest` post-response flow) and waits
-   * for any async listeners to settle.
-   * @param station - The charging station instance
-   * @param request - The stop transaction request payload
-   * @param response - The stop transaction response from the handler
-   */
-  async function emitStopEvent (
-    station: ChargingStation,
-    request: OCPP20RequestStopTransactionRequest,
-    response: OCPP20RequestStopTransactionResponse
-  ): Promise<void> {
-    incomingRequestService.emit(
-      OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION,
-      station,
-      request,
-      response
-    )
-    // Allow the async event listener (.catch) to complete
-    await new Promise(resolve => {
-      setTimeout(resolve, 50)
-    })
-  }
+  await describe('Handler validation', async () => {
+    // FR: F03.FR.02, F03.FR.03, F03.FR.07, F03.FR.09
+    await it('should return Accepted for valid active transaction', async () => {
+      const transactionId = await startTransaction(mockStation, 1, 100)
 
-  // FR: F03.FR.02, F03.FR.03, F03.FR.07, F03.FR.09
-  await it('should successfully stop an active transaction', async () => {
-    // Start a transaction first
-    const transactionId = await startTransaction(1, 100)
+      const response = testableService.handleRequestStopTransaction(mockStation, {
+        transactionId: transactionId as UUIDv4,
+      })
 
-    // Clear transaction events after starting, before testing stop transaction
-    sentTransactionEvents = []
-
-    // Create stop transaction request
-    const stopRequest: OCPP20RequestStopTransactionRequest = {
-      transactionId: transactionId as UUIDv4,
-    }
-
-    // Execute stop transaction handler (validates and returns Accepted)
-    const response = testableService.handleRequestStopTransaction(mockStation, stopRequest)
-
-    // Verify response
-    assert.notStrictEqual(response, undefined)
-    assert.strictEqual(response.status, RequestStartStopStatusEnumType.Accepted)
-
-    // Emit event to trigger the listener (mimics base class post-response flow)
-    await emitStopEvent(mockStation, stopRequest, response)
-
-    // Verify TransactionEvent was sent by the event listener
-    assert.strictEqual(sentTransactionEvents.length, 1)
-    const transactionEvent = sentTransactionEvents[0]
-
-    assert.strictEqual(transactionEvent.eventType, OCPP20TransactionEventEnumType.Ended)
-    assert.strictEqual(transactionEvent.triggerReason, OCPP20TriggerReasonEnumType.RemoteStop)
-    assert.strictEqual(transactionEvent.transactionInfo.transactionId, transactionId)
-    assert.strictEqual(transactionEvent.transactionInfo.stoppedReason, OCPP20ReasonEnumType.Remote)
-    assert.strictEqual(transactionEvent.evse?.id, 1)
-  })
-
-  // FR: F03.FR.02, F03.FR.03
-  await it('should handle multiple active transactions correctly', async () => {
-    // Reset once before starting multiple transactions
-    resetConnectorTransactionState(mockStation)
-
-    // Start transactions on different EVSEs (skip reset for subsequent transactions)
-    const transactionId1 = await startTransaction(1, 200, true) // Skip reset since we just did it
-    const transactionId2 = await startTransaction(2, 201, true) // Skip reset to keep transaction 1
-    const transactionId3 = await startTransaction(3, 202, true) // Skip reset to keep transactions 1 & 2
-
-    // Clear transaction events after starting, before testing stop transaction
-    sentTransactionEvents = []
-
-    // Stop the second transaction
-    const stopRequest: OCPP20RequestStopTransactionRequest = {
-      transactionId: transactionId2 as UUIDv4,
-    }
+      assert.notStrictEqual(response, undefined)
+      assert.strictEqual(response.status, RequestStartStopStatusEnumType.Accepted)
+    })
 
-    const response = testableService.handleRequestStopTransaction(mockStation, stopRequest)
+    // FR: F03.FR.02, F03.FR.03
+    await it('should handle multiple active transactions correctly', async () => {
+      resetConnectorTransactionState(mockStation)
 
-    // Verify response
-    assert.notStrictEqual(response, undefined)
-    assert.strictEqual(response.status, RequestStartStopStatusEnumType.Accepted)
+      const transactionId1 = await startTransaction(mockStation, 1, 200, true)
+      const transactionId2 = await startTransaction(mockStation, 2, 201, true)
+      const transactionId3 = await startTransaction(mockStation, 3, 202, true)
 
-    // Emit event to trigger the listener
-    await emitStopEvent(mockStation, stopRequest, response)
+      const response = testableService.handleRequestStopTransaction(mockStation, {
+        transactionId: transactionId2 as UUIDv4,
+      })
 
-    // Verify correct TransactionEvent was sent
-    assert.strictEqual(sentTransactionEvents.length, 1)
-    const transactionEvent = sentTransactionEvents[0]
+      assert.notStrictEqual(response, undefined)
+      assert.strictEqual(response.status, RequestStartStopStatusEnumType.Accepted)
+      assert.strictEqual(mockStation.getConnectorIdByTransactionId(transactionId1), 1)
+      assert.strictEqual(mockStation.getConnectorIdByTransactionId(transactionId3), 3)
+    })
 
-    assert.strictEqual(transactionEvent.transactionInfo.transactionId, transactionId2)
-    assert.strictEqual(transactionEvent.evse?.id, 2)
+    // FR: F03.FR.08
+    await it('should reject stop transaction for non-existent transaction ID', () => {
+      const response = testableService.handleRequestStopTransaction(mockStation, {
+        transactionId: 'non-existent-transaction-id' as UUIDv4,
+      })
 
-    // Verify other transactions are still active (test implementation dependent)
-    assert.strictEqual(mockStation.getConnectorIdByTransactionId(transactionId1), 1)
-    assert.strictEqual(mockStation.getConnectorIdByTransactionId(transactionId3), 3)
-  })
+      assert.notStrictEqual(response, undefined)
+      assert.strictEqual(response.status, RequestStartStopStatusEnumType.Rejected)
+    })
 
-  // FR: F03.FR.08
-  await it('should reject stop transaction for non-existent transaction ID', () => {
-    // Clear previous transaction events
-    sentTransactionEvents = []
+    // FR: F03.FR.08
+    await it('should reject stop transaction for invalid transaction ID format - empty string', () => {
+      const response = testableService.handleRequestStopTransaction(mockStation, {
+        transactionId: '' as UUIDv4,
+      })
 
-    const nonExistentTransactionId = 'non-existent-transaction-id'
-    const stopRequest: OCPP20RequestStopTransactionRequest = {
-      transactionId: nonExistentTransactionId as UUIDv4,
-    }
+      assert.notStrictEqual(response, undefined)
+      assert.strictEqual(response.status, RequestStartStopStatusEnumType.Rejected)
+    })
 
-    const response = testableService.handleRequestStopTransaction(mockStation, stopRequest)
+    // FR: F03.FR.08
+    await it('should reject stop transaction for invalid transaction ID format - too long', () => {
+      const response = testableService.handleRequestStopTransaction(mockStation, {
+        transactionId: 'a'.repeat(37) as UUIDv4,
+      })
 
-    // Verify rejection
-    assert.notStrictEqual(response, undefined)
-    assert.strictEqual(response.status, RequestStartStopStatusEnumType.Rejected)
+      assert.notStrictEqual(response, undefined)
+      assert.strictEqual(response.status, RequestStartStopStatusEnumType.Rejected)
+    })
 
-    // Verify no TransactionEvent was sent
-    assert.strictEqual(sentTransactionEvents.length, 0)
-  })
+    // FR: F03.FR.02
+    await it('should accept valid transaction ID format - exactly 36 characters', async () => {
+      const transactionId = await startTransaction(mockStation, 1, 300)
 
-  // FR: F03.FR.08
-  await it('should reject stop transaction for invalid transaction ID format - empty string', () => {
-    // Clear previous transaction events
-    sentTransactionEvents = []
+      let testTransactionId = transactionId
+      if (testTransactionId.length < 36) {
+        testTransactionId = testTransactionId.padEnd(36, '0')
+      } else if (testTransactionId.length > 36) {
+        testTransactionId = testTransactionId.substring(0, 36)
+      }
 
-    const invalidRequest: OCPP20RequestStopTransactionRequest = {
-      transactionId: '' as UUIDv4,
-    }
+      const connectorId = mockStation.getConnectorIdByTransactionId(transactionId)
+      if (connectorId != null) {
+        const connectorStatus = mockStation.getConnectorStatus(connectorId)
+        if (connectorStatus) {
+          connectorStatus.transactionId = testTransactionId
+        }
+      }
 
-    const response = testableService.handleRequestStopTransaction(mockStation, invalidRequest)
+      const response = testableService.handleRequestStopTransaction(mockStation, {
+        transactionId: testTransactionId as UUIDv4,
+      })
 
-    // Verify rejection
-    assert.notStrictEqual(response, undefined)
-    assert.strictEqual(response.status, RequestStartStopStatusEnumType.Rejected)
+      assert.notStrictEqual(response, undefined)
+      assert.strictEqual(response.status, RequestStartStopStatusEnumType.Accepted)
+    })
 
-    // Verify no TransactionEvent was sent
-    assert.strictEqual(sentTransactionEvents.length, 0)
-  })
+    // FR: F04.FR.01
+    await it('should return proper response structure', async () => {
+      const transactionId = await startTransaction(mockStation, 1, 400)
 
-  // FR: F03.FR.08
-  await it('should reject stop transaction for invalid transaction ID format - too long', () => {
-    // Clear previous transaction events
-    sentTransactionEvents = []
+      const response = testableService.handleRequestStopTransaction(mockStation, {
+        transactionId: transactionId as UUIDv4,
+      })
 
-    // Create a transaction ID longer than 36 characters
-    const tooLongTransactionId = 'a'.repeat(37)
-    const invalidRequest: OCPP20RequestStopTransactionRequest = {
-      transactionId: tooLongTransactionId as UUIDv4,
-    }
+      assert.notStrictEqual(response, undefined)
+      assert.strictEqual(typeof response, 'object')
+      assert.notStrictEqual(response.status, undefined)
+      assert.ok(Object.values(RequestStartStopStatusEnumType).includes(response.status))
+      assert.deepStrictEqual(Object.keys(response as object), ['status'])
+    })
 
-    const response = testableService.handleRequestStopTransaction(mockStation, invalidRequest)
+    await it('should accept request with custom data', async () => {
+      const transactionId = await startTransaction(mockStation, 1, 500)
 
-    // Verify rejection
-    assert.notStrictEqual(response, undefined)
-    assert.strictEqual(response.status, RequestStartStopStatusEnumType.Rejected)
+      const response = testableService.handleRequestStopTransaction(mockStation, {
+        customData: {
+          data: 'Custom stop transaction data',
+          vendorId: 'TestVendor',
+        },
+        transactionId: transactionId as UUIDv4,
+      })
 
-    // Verify no TransactionEvent was sent
-    assert.strictEqual(sentTransactionEvents.length, 0)
+      assert.notStrictEqual(response, undefined)
+      assert.strictEqual(response.status, RequestStartStopStatusEnumType.Accepted)
+    })
   })
 
-  // FR: F03.FR.02
-  await it('should accept valid transaction ID format - exactly 36 characters', async () => {
-    // Start a transaction first
-    const transactionId = await startTransaction(1, 300)
+  await describe('REQUEST_STOP_TRANSACTION event listener', async () => {
+    let listenerService: OCPP20IncomingRequestService
+    let requestHandlerMock: ReturnType<typeof mock.fn>
+    let listenerStation: ChargingStation
+
+    beforeEach(() => {
+      ;({ requestHandlerMock, station: listenerStation } = createListenerStation(
+        TEST_CHARGING_STATION_BASE_NAME + '-LISTENER'
+      ))
+      listenerService = new OCPP20IncomingRequestService()
+      testableService = createTestableIncomingRequestService(listenerService)
+      const stationId = listenerStation.stationInfo?.chargingStationId ?? 'unknown'
+      OCPPAuthServiceFactory.setInstanceForTesting(stationId, createMockAuthService())
+      resetLimits(listenerStation)
+      resetReportingValueSize(listenerStation)
+    })
 
-    // Clear transaction events after starting, before testing stop transaction
-    sentTransactionEvents = []
+    await it('should register REQUEST_STOP_TRANSACTION event listener in constructor', () => {
+      assert.strictEqual(
+        listenerService.listenerCount(OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION),
+        1
+      )
+    })
 
-    // Ensure the transaction ID is exactly 36 characters (pad if necessary for test)
-    let testTransactionId = transactionId
-    if (testTransactionId.length < 36) {
-      testTransactionId = testTransactionId.padEnd(36, '0')
-    } else if (testTransactionId.length > 36) {
-      testTransactionId = testTransactionId.substring(0, 36)
-    }
+    await it('should call requestStopTransaction when response is Accepted', async () => {
+      const transactionId = await startTransaction(listenerStation, 1, 100)
+      requestHandlerMock.mock.resetCalls()
 
-    // Update the connector's transaction ID for testing
-    const connectorId = mockStation.getConnectorIdByTransactionId(transactionId)
-    if (connectorId != null) {
-      const connectorStatus = mockStation.getConnectorStatus(connectorId)
-      if (connectorStatus) {
-        connectorStatus.transactionId = testTransactionId
+      const request: OCPP20RequestStopTransactionRequest = {
+        transactionId: transactionId as UUIDv4,
+      }
+      const response: OCPP20RequestStopTransactionResponse = {
+        status: RequestStartStopStatusEnumType.Accepted,
       }
-    }
-
-    const stopRequest: OCPP20RequestStopTransactionRequest = {
-      transactionId: testTransactionId as UUIDv4,
-    }
 
-    const response = testableService.handleRequestStopTransaction(mockStation, stopRequest)
+      listenerService.emit(
+        OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION,
+        listenerStation,
+        request,
+        response
+      )
+
+      assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+      const args = requestHandlerMock.mock.calls[0].arguments as [
+        unknown,
+        string,
+        OCPP20TransactionEventRequest
+      ]
+      assert.strictEqual(args[1], OCPP20RequestCommand.TRANSACTION_EVENT)
+    })
 
-    // Verify acceptance (format is valid)
-    assert.notStrictEqual(response, undefined)
-    assert.strictEqual(response.status, RequestStartStopStatusEnumType.Accepted)
+    await it('should NOT call requestStopTransaction when response is Rejected', () => {
+      const request: OCPP20RequestStopTransactionRequest = {
+        transactionId: 'any-transaction-id' as UUIDv4,
+      }
+      const response: OCPP20RequestStopTransactionResponse = {
+        status: RequestStartStopStatusEnumType.Rejected,
+      }
 
-    // Emit event to trigger the listener
-    await emitStopEvent(mockStation, stopRequest, response)
+      listenerService.emit(
+        OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION,
+        listenerStation,
+        request,
+        response
+      )
 
-    // Verify TransactionEvent was sent
-    assert.strictEqual(sentTransactionEvents.length, 1)
-  })
+      assert.strictEqual(requestHandlerMock.mock.callCount(), 0)
+    })
 
-  await it('should handle TransactionEvent request failure gracefully', async () => {
-    sentTransactionEvents = []
-
-    const { station: failingChargingStation } = createMockChargingStation({
-      baseName: TEST_CHARGING_STATION_BASE_NAME + '-FAIL',
-      connectorsCount: 1,
-      evseConfiguration: { evsesCount: 1 },
-      heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
-      ocppRequestService: {
-        requestHandler: async (
-          _chargingStation: unknown,
-          commandName: unknown,
-          commandPayload: unknown
-        ) => {
-          if (commandName === OCPP20RequestCommand.TRANSACTION_EVENT) {
-            throw new Error('TransactionEvent rejected by server')
-          }
-          return Promise.resolve({})
+    await it('should handle requestStopTransaction failure gracefully', async () => {
+      let transactionEventCallCount = 0
+      const { station: failStation } = createMockChargingStation({
+        baseName: TEST_CHARGING_STATION_BASE_NAME + '-FAIL',
+        connectorsCount: 1,
+        evseConfiguration: { evsesCount: 1 },
+        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+        ocppRequestService: {
+          requestHandler: async (_chargingStation: unknown, commandName: unknown) => {
+            if (commandName === OCPP20RequestCommand.TRANSACTION_EVENT) {
+              transactionEventCallCount++
+              throw new Error('TransactionEvent rejected by server')
+            }
+            return Promise.resolve({})
+          },
         },
-      },
-      stationInfo: {
-        ocppStrictCompliance: false,
-        ocppVersion: OCPPVersion.VERSION_201,
-      },
-      websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+        stationInfo: {
+          ocppStrictCompliance: false,
+          ocppVersion: OCPPVersion.VERSION_201,
+        },
+        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+      })
+
+      const failStationId = failStation.stationInfo?.chargingStationId ?? 'unknown'
+      OCPPAuthServiceFactory.setInstanceForTesting(failStationId, createMockAuthService())
+
+      resetConnectorTransactionState(failStation)
+      const startResponse = await testableService.handleRequestStartTransaction(failStation, {
+        evseId: 1,
+        idToken: {
+          idToken: 'FAIL_TEST_TOKEN',
+          type: OCPP20IdTokenEnumType.ISO14443,
+        },
+        remoteStartId: 999,
+      })
+      const transactionId = startResponse.transactionId as string
+
+      listenerService.emit(
+        OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION,
+        failStation,
+        { transactionId: transactionId as UUIDv4 } satisfies OCPP20RequestStopTransactionRequest,
+        {
+          status: RequestStartStopStatusEnumType.Accepted,
+        } satisfies OCPP20RequestStopTransactionResponse
+      )
+
+      // Flush microtask queue so .catch(errorHandler) executes
+      await Promise.resolve()
+
+      assert.strictEqual(transactionEventCallCount, 1)
     })
 
-    const failingStationId = failingChargingStation.stationInfo?.chargingStationId ?? 'unknown'
-    OCPPAuthServiceFactory.setInstanceForTesting(failingStationId, createMockAuthService())
-
-    const startRequest: OCPP20RequestStartTransactionRequest = {
-      evseId: 1,
-      idToken: {
-        idToken: 'FAIL_TEST_TOKEN',
-        type: OCPP20IdTokenEnumType.ISO14443,
-      },
-      remoteStartId: 999,
-    }
-
-    const startResponse = await testableService.handleRequestStartTransaction(
-      failingChargingStation,
-      startRequest
-    )
-
-    const transactionId = startResponse.transactionId as string
-
-    // Attempt to stop the transaction
-    const stopRequest: OCPP20RequestStopTransactionRequest = {
-      transactionId: transactionId as UUIDv4,
-    }
-
-    const response = testableService.handleRequestStopTransaction(
-      failingChargingStation,
-      stopRequest
-    )
-
-    // With event listener pattern, handler returns Accepted (validation passed).
-    // The TransactionEvent failure is handled asynchronously by the listener's .catch().
-    assert.notStrictEqual(response, undefined)
-    assert.strictEqual(response.status, RequestStartStopStatusEnumType.Accepted)
-
-    // Emit event — the listener will attempt requestStopTransaction which throws,
-    // but the error is caught and logged (no unhandled rejection).
-    await emitStopEvent(failingChargingStation, stopRequest, response)
-  })
-
-  // FR: F04.FR.01
-  await it('should return proper response structure', async () => {
-    // Clear previous transaction events
-    sentTransactionEvents = []
-
-    // Start a transaction first
-    const transactionId = await startTransaction(1, 400)
-
-    const stopRequest: OCPP20RequestStopTransactionRequest = {
-      transactionId: transactionId as UUIDv4,
-    }
-
-    const response = testableService.handleRequestStopTransaction(mockStation, stopRequest)
-
-    // Verify response structure
-    assert.notStrictEqual(response, undefined)
-    assert.strictEqual(typeof response, 'object')
-    assert.notStrictEqual(response.status, undefined)
-
-    // Verify status is valid enum value
-    assert.ok(Object.values(RequestStartStopStatusEnumType).includes(response.status))
-
-    // OCPP 2.0 RequestStopTransaction response should only contain status
-    assert.deepStrictEqual(Object.keys(response as object), ['status'])
-  })
-
-  await it('should handle custom data in request payload', async () => {
-    // Start a transaction first
-    const transactionId = await startTransaction(1, 500)
-
-    // Clear transaction events after starting, before testing stop transaction
-    sentTransactionEvents = []
-
-    const stopRequestWithCustomData: OCPP20RequestStopTransactionRequest = {
-      customData: {
-        data: 'Custom stop transaction data',
-        vendorId: 'TestVendor',
-      },
-      transactionId: transactionId as UUIDv4,
-    }
-
-    const response = testableService.handleRequestStopTransaction(
-      mockStation,
-      stopRequestWithCustomData
-    )
-
-    // Verify response
-    assert.notStrictEqual(response, undefined)
-    assert.strictEqual(response.status, RequestStartStopStatusEnumType.Accepted)
-
-    // Emit event to trigger the listener
-    await emitStopEvent(mockStation, stopRequestWithCustomData, response)
-
-    // Verify TransactionEvent was sent despite custom data
-    assert.strictEqual(sentTransactionEvents.length, 1)
-  })
-
-  // FR: F03.FR.07, F03.FR.09
-  await it('should validate TransactionEvent content correctly', async () => {
-    // Start a transaction first
-    const transactionId = await startTransaction(2, 600) // Use EVSE 2
-
-    // Clear transaction events after starting, before testing stop transaction
-    sentTransactionEvents = []
-
-    const stopRequest: OCPP20RequestStopTransactionRequest = {
-      transactionId: transactionId as UUIDv4,
-    }
-
-    const response = testableService.handleRequestStopTransaction(mockStation, stopRequest)
-
-    assert.strictEqual(response.status, RequestStartStopStatusEnumType.Accepted)
-
-    // Emit event to trigger the listener
-    await emitStopEvent(mockStation, stopRequest, response)
-
-    // Verify TransactionEvent structure and content
-    assert.strictEqual(sentTransactionEvents.length, 1)
-    const transactionEvent = sentTransactionEvents[0]
-
-    // Validate required fields
-    assert.strictEqual(transactionEvent.eventType, OCPP20TransactionEventEnumType.Ended)
-    assert.notStrictEqual(transactionEvent.timestamp, undefined)
-    assert.ok(transactionEvent.timestamp instanceof Date)
-    assert.strictEqual(transactionEvent.triggerReason, OCPP20TriggerReasonEnumType.RemoteStop)
-    assert.notStrictEqual(transactionEvent.seqNo, undefined)
-    assert.strictEqual(typeof transactionEvent.seqNo, 'number')
-
-    // Validate transaction info
-    assert.notStrictEqual(transactionEvent.transactionInfo, undefined)
-    assert.strictEqual(transactionEvent.transactionInfo.transactionId, transactionId)
-    assert.strictEqual(transactionEvent.transactionInfo.stoppedReason, OCPP20ReasonEnumType.Remote)
-
-    // Validate EVSE info
-    assert.notStrictEqual(transactionEvent.evse, undefined)
-    assert.strictEqual(transactionEvent.evse?.id, 2) // Should match the EVSE we used
-  })
-
-  // FR: F03.FR.09
-  await it('should include final meter values in TransactionEvent(Ended)', async () => {
-    resetConnectorTransactionState(mockStation)
-
-    const transactionId = await startTransaction(3, 700)
-
-    const connectorStatus = mockStation.getConnectorStatus(3)
-    assert.notStrictEqual(connectorStatus, undefined)
-    if (connectorStatus != null) {
-      connectorStatus.transactionEnergyActiveImportRegisterValue = 12345.67
-    }
-
-    sentTransactionEvents = []
-
-    const stopRequest: OCPP20RequestStopTransactionRequest = {
-      transactionId: transactionId as UUIDv4,
-    }
-
-    const response = testableService.handleRequestStopTransaction(mockStation, stopRequest)
-
-    assert.strictEqual(response.status, RequestStartStopStatusEnumType.Accepted)
-
-    // Emit event to trigger the listener
-    await emitStopEvent(mockStation, stopRequest, response)
+    // FR: F03.FR.07, F03.FR.09
+    await it('should send TransactionEvent(Ended) with correct content', async () => {
+      const transactionId = await startTransaction(listenerStation, 2, 600)
+      requestHandlerMock.mock.resetCalls()
+
+      listenerService.emit(
+        OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION,
+        listenerStation,
+        { transactionId: transactionId as UUIDv4 } satisfies OCPP20RequestStopTransactionRequest,
+        {
+          status: RequestStartStopStatusEnumType.Accepted,
+        } satisfies OCPP20RequestStopTransactionResponse
+      )
+
+      assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+      const args = requestHandlerMock.mock.calls[0].arguments as [
+        unknown,
+        string,
+        OCPP20TransactionEventRequest
+      ]
+      const transactionEvent = args[2]
+
+      assert.strictEqual(transactionEvent.eventType, OCPP20TransactionEventEnumType.Ended)
+      assert.notStrictEqual(transactionEvent.timestamp, undefined)
+      assert.ok(transactionEvent.timestamp instanceof Date)
+      assert.strictEqual(transactionEvent.triggerReason, OCPP20TriggerReasonEnumType.RemoteStop)
+      assert.notStrictEqual(transactionEvent.seqNo, undefined)
+      assert.strictEqual(typeof transactionEvent.seqNo, 'number')
+
+      assert.notStrictEqual(transactionEvent.transactionInfo, undefined)
+      assert.strictEqual(transactionEvent.transactionInfo.transactionId, transactionId)
+      assert.strictEqual(
+        transactionEvent.transactionInfo.stoppedReason,
+        OCPP20ReasonEnumType.Remote
+      )
+
+      assert.notStrictEqual(transactionEvent.evse, undefined)
+      assert.strictEqual(transactionEvent.evse?.id, 2)
+    })
 
-    assert.strictEqual(sentTransactionEvents.length, 1)
-    const transactionEvent = sentTransactionEvents[0]
+    // FR: F03.FR.09
+    await it('should include final meter values in TransactionEvent(Ended)', async () => {
+      resetConnectorTransactionState(listenerStation)
+      const transactionId = await startTransaction(listenerStation, 3, 700)
 
-    assert.strictEqual(transactionEvent.eventType, OCPP20TransactionEventEnumType.Ended)
+      const connectorStatus = listenerStation.getConnectorStatus(3)
+      assert.notStrictEqual(connectorStatus, undefined)
+      if (connectorStatus != null) {
+        connectorStatus.transactionEnergyActiveImportRegisterValue = 12345.67
+      }
 
-    assert.notStrictEqual(transactionEvent.meterValue, undefined)
-    if (transactionEvent.meterValue == null) {
-      assert.fail('Expected meterValue to be defined')
-    }
-    assert.strictEqual(transactionEvent.meterValue.length, 1)
-
-    const meterValue = transactionEvent.meterValue[0]
-    assert.notStrictEqual(meterValue, undefined)
-    assert.ok(meterValue.timestamp instanceof Date)
-    assert.notStrictEqual(meterValue.sampledValue, undefined)
-    assert.strictEqual(meterValue.sampledValue.length, 1)
-
-    const sampledValue = meterValue.sampledValue[0]
-    assert.strictEqual(sampledValue.value, 12345.67)
-    assert.strictEqual(sampledValue.context, 'Transaction.End')
-    assert.strictEqual(sampledValue.measurand, 'Energy.Active.Import.Register')
+      requestHandlerMock.mock.resetCalls()
+
+      listenerService.emit(
+        OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION,
+        listenerStation,
+        { transactionId: transactionId as UUIDv4 } satisfies OCPP20RequestStopTransactionRequest,
+        {
+          status: RequestStartStopStatusEnumType.Accepted,
+        } satisfies OCPP20RequestStopTransactionResponse
+      )
+
+      assert.strictEqual(requestHandlerMock.mock.callCount(), 1)
+      const args = requestHandlerMock.mock.calls[0].arguments as [
+        unknown,
+        string,
+        OCPP20TransactionEventRequest
+      ]
+      const transactionEvent = args[2]
+
+      assert.strictEqual(transactionEvent.eventType, OCPP20TransactionEventEnumType.Ended)
+
+      assert.notStrictEqual(transactionEvent.meterValue, undefined)
+      if (transactionEvent.meterValue == null) {
+        assert.fail('Expected meterValue to be defined')
+      }
+      assert.strictEqual(transactionEvent.meterValue.length, 1)
+
+      const meterValue = transactionEvent.meterValue[0]
+      assert.notStrictEqual(meterValue, undefined)
+      assert.ok(meterValue.timestamp instanceof Date)
+      assert.notStrictEqual(meterValue.sampledValue, undefined)
+      assert.strictEqual(meterValue.sampledValue.length, 1)
+
+      const sampledValue = meterValue.sampledValue[0]
+      assert.strictEqual(sampledValue.value, 12345.67)
+      assert.strictEqual(sampledValue.context, 'Transaction.End')
+      assert.strictEqual(sampledValue.measurand, 'Energy.Active.Import.Register')
+    })
   })
 })