From 64b8f8583a21569d84a67b367ed07bc4972976c9 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Mon, 16 Mar 2026 18:39:51 +0100 Subject: [PATCH] refactor(tests): split RequestStopTransaction tests into handler and listener groups 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). --- ...uestService-RequestStopTransaction.test.ts | 731 ++++++++---------- 1 file changed, 324 insertions(+), 407 deletions(-) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts index 23b868c6..8052900f 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts @@ -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 + 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 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 { - // 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 { - 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 + 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') + }) }) }) -- 2.43.0