From 60bf8d9306af169c6c48ff83676bc30fd6f0bfc5 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 27 Feb 2026 14:47:22 +0100 Subject: [PATCH] test(ocpp): add E03 IdToken-First pre-authorization transaction tests E03 IdToken-First Pre-Authorization Flow (16 tests): - E03.FR.01: IdToken inclusion in TransactionEvent (once per transaction) - E03.FR.05/06: EVConnectionTimeOut handling (authorization cancellation) - E03.FR.07/08: Sequence number continuity and unique transaction IDs - E03.FR.13: Authorized trigger reason for IdToken-first start - Authorization status handling (Deauthorized, StopAuthorized) - Full IdToken-first lifecycle (authorize -> cable -> charge -> end) - Differentiation from E02 Cable-First flow by trigger reason - Multiple connector independence verification Tests verify the IdToken-first flow where user presents their ID token BEFORE connecting the cable, as opposed to E02 Cable-First where the cable is connected first. All 1131 tests pass. --- ...tils-TransactionEvent-IdTokenFirst.test.ts | 679 ++++++++++++++++++ 1 file changed, 679 insertions(+) create mode 100644 tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-IdTokenFirst.test.ts diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-IdTokenFirst.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-IdTokenFirst.test.ts new file mode 100644 index 00000000..ea38f12b --- /dev/null +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent-IdTokenFirst.test.ts @@ -0,0 +1,679 @@ +import { expect } from '@std/expect' +import { afterEach, describe, it } from 'node:test' + +import type { EmptyObject } from '../../../../src/types/index.js' + +import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js' +import { + OCPP20TransactionEventEnumType, + OCPP20TriggerReasonEnumType, + OCPPVersion, +} from '../../../../src/types/index.js' +import { + OCPP20ChargingStateEnumType, + OCPP20IdTokenEnumType, + type OCPP20IdTokenType, + type OCPP20TransactionContext, +} from '../../../../src/types/ocpp/2.0/Transaction.js' +import { Constants, generateUUID } from '../../../../src/utils/index.js' +import { createChargingStation } from '../../../ChargingStationFactory.js' +import { TEST_CHARGING_STATION_BASE_NAME } from './OCPP20TestConstants.js' +import { resetConnectorTransactionState, resetLimits } from './OCPP20TestUtils.js' + +/** + * E03 IdToken-First Transaction Flow Tests (OCPP 2.0.1) + * + * Tests the IdToken-first pre-authorization flow where a user presents + * their ID token BEFORE connecting the cable. + * + * Key E03 Functional Requirements: + * - E03.FR.01: When IdToken presented first, CS SHALL verify with CSMS + * - E03.FR.02: CSMS SHALL verify IdToken validity + * - E03.FR.05: CS SHALL handle EVConnectionTimeOut + * - E03.FR.06: If cable not connected within timeout, CS SHALL cancel authorization + * - E03.FR.13: triggerReason SHALL be Authorized for IdToken-first + * + * Key Difference from E02 (Cable-First): + * - E03: Authorization -> Cable connection -> Charging + * - E02: Cable connection -> EV detection -> Authorization -> Charging + */ +await describe('E03 - IdToken-First Pre-Authorization Flow', async () => { + const mockChargingStation = createChargingStation({ + baseName: TEST_CHARGING_STATION_BASE_NAME, + connectorsCount: 3, + evseConfiguration: { evsesCount: 3 }, + heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL, + ocppRequestService: { + requestHandler: async () => { + return Promise.resolve({} as EmptyObject) + }, + }, + stationInfo: { + ocppStrictCompliance: true, + ocppVersion: OCPPVersion.VERSION_201, + }, + websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, + }) + + // Reset limits and state before tests + resetLimits(mockChargingStation) + + afterEach(() => { + resetConnectorTransactionState(mockChargingStation) + }) + + // ========================================================================= + // E03.FR.13: Trigger Reason Selection for IdToken-First + // ========================================================================= + await describe('E03.FR.13 - Trigger Reason Selection', async () => { + await it('Should select Authorized trigger for IdToken-first transaction start', () => { + // E03.FR.13: triggerReason SHALL be Authorized for IdToken-first + const context: OCPP20TransactionContext = { + authorizationMethod: 'idToken', + source: 'local_authorization', + } + + const triggerReason = OCPP20ServiceUtils.selectTriggerReason( + OCPP20TransactionEventEnumType.Started, + context + ) + + expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized) + }) + + await it('Should select groupIdToken trigger for group authorization', () => { + const context: OCPP20TransactionContext = { + authorizationMethod: 'groupIdToken', + source: 'local_authorization', + } + + const triggerReason = OCPP20ServiceUtils.selectTriggerReason( + OCPP20TransactionEventEnumType.Started, + context + ) + + expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized) + }) + + await it('Should differentiate IdToken-first from Cable-first by trigger reason', () => { + // IdToken-first: Authorized trigger + const idTokenFirstContext: OCPP20TransactionContext = { + authorizationMethod: 'idToken', + source: 'local_authorization', + } + + // Cable-first: CablePluggedIn trigger + const cableFirstContext: OCPP20TransactionContext = { + cableState: 'plugged_in', + source: 'cable_action', + } + + const idTokenTrigger = OCPP20ServiceUtils.selectTriggerReason( + OCPP20TransactionEventEnumType.Started, + idTokenFirstContext + ) + + const cableTrigger = OCPP20ServiceUtils.selectTriggerReason( + OCPP20TransactionEventEnumType.Started, + cableFirstContext + ) + + expect(idTokenTrigger).toBe(OCPP20TriggerReasonEnumType.Authorized) + expect(cableTrigger).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn) + expect(idTokenTrigger).not.toBe(cableTrigger) + }) + }) + + // ========================================================================= + // E03.FR.01: IdToken Inclusion in TransactionEvent + // ========================================================================= + await describe('E03.FR.01 - IdToken in TransactionEvent', async () => { + await it('Should include idToken in first TransactionEvent after authorization', () => { + const connectorId = 1 + const transactionId = generateUUID() + const idToken: OCPP20IdTokenType = { + idToken: 'VALID_TOKEN_E03_001', + type: OCPP20IdTokenEnumType.ISO14443, + } + + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId) + + // Build Started event with idToken (E03.FR.01: IdToken must be in first event) + const startedEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + transactionId, + { idToken } + ) + + expect(startedEvent.idToken).toBeDefined() + expect(startedEvent.idToken?.idToken).toBe('VALID_TOKEN_E03_001') + expect(startedEvent.idToken?.type).toBe(OCPP20IdTokenEnumType.ISO14443) + expect(startedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started) + expect(startedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized) + }) + + await it('Should not include idToken in subsequent events (E03.FR.01 compliance)', () => { + const connectorId = 1 + const transactionId = generateUUID() + const idToken: OCPP20IdTokenType = { + idToken: 'VALID_TOKEN_E03_002', + type: OCPP20IdTokenEnumType.ISO14443, + } + + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId) + + // First event includes idToken + const startedEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + transactionId, + { idToken } + ) + + // Second event should NOT include idToken (flag is set after first inclusion) + const updatedEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Updated, + OCPP20TriggerReasonEnumType.ChargingStateChanged, + connectorId, + transactionId, + { chargingState: OCPP20ChargingStateEnumType.Charging, idToken } + ) + + expect(startedEvent.idToken).toBeDefined() + expect(updatedEvent.idToken).toBeUndefined() + }) + + await it('Should support various IdToken types for E03 flow', () => { + const connectorId = 1 + const transactionId = generateUUID() + + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId) + + // Test ISO14443 (RFID) + const rfidToken: OCPP20IdTokenType = { + idToken: 'RFID_TAG_123456', + type: OCPP20IdTokenEnumType.ISO14443, + } + + const rfidEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + transactionId, + { idToken: rfidToken } + ) + + expect(rfidEvent.idToken?.type).toBe(OCPP20IdTokenEnumType.ISO14443) + + // Reset for eMAID test + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId) + const connectorStatus = mockChargingStation.getConnectorStatus(connectorId) + if (connectorStatus != null) { + connectorStatus.transactionIdTokenSent = undefined + } + + // Test eMAID (contract identifier) + const emaidToken: OCPP20IdTokenType = { + idToken: 'DE*ABC*E123456*1', + type: OCPP20IdTokenEnumType.eMAID, + } + + const emaidEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + generateUUID(), + { idToken: emaidToken } + ) + + expect(emaidEvent.idToken?.type).toBe(OCPP20IdTokenEnumType.eMAID) + expect(emaidEvent.idToken?.idToken).toBe('DE*ABC*E123456*1') + }) + }) + + // ========================================================================= + // Full E03 IdToken-First Transaction Lifecycle + // ========================================================================= + await describe('Full IdToken-First Transaction Lifecycle', async () => { + await it('Should support complete IdToken-first to cable to charging to end flow', () => { + const connectorId = 1 + const transactionId = generateUUID() + const idToken: OCPP20IdTokenType = { + idToken: 'LIFECYCLE_TOKEN_001', + type: OCPP20IdTokenEnumType.ISO14443, + } + + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId) + + // E03 Step 1: IdToken presented and authorized (Started with Authorized trigger) + const authorizedEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + transactionId, + { idToken } + ) + + // E03 Step 2: Cable connected (Updated event) + const cableConnectedEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Updated, + OCPP20TriggerReasonEnumType.CablePluggedIn, + connectorId, + transactionId + ) + + // E03 Step 3: Charging starts + const chargingEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Updated, + OCPP20TriggerReasonEnumType.ChargingStateChanged, + connectorId, + transactionId, + { chargingState: OCPP20ChargingStateEnumType.Charging } + ) + + // E03 Step 4: Transaction ends + const endedEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Ended, + OCPP20TriggerReasonEnumType.StopAuthorized, + connectorId, + transactionId + ) + + // Validate event sequence + expect(authorizedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started) + expect(authorizedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized) + expect(authorizedEvent.idToken).toBeDefined() + expect(authorizedEvent.seqNo).toBe(0) + + expect(cableConnectedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated) + expect(cableConnectedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn) + expect(cableConnectedEvent.idToken).toBeUndefined() // E03.FR.01: idToken only in first event + expect(cableConnectedEvent.seqNo).toBe(1) + + expect(chargingEvent.eventType).toBe(OCPP20TransactionEventEnumType.Updated) + expect(chargingEvent.seqNo).toBe(2) + + expect(endedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended) + expect(endedEvent.seqNo).toBe(3) + + // All events share same transaction ID + expect(authorizedEvent.transactionInfo.transactionId).toBe(transactionId) + expect(cableConnectedEvent.transactionInfo.transactionId).toBe(transactionId) + expect(chargingEvent.transactionInfo.transactionId).toBe(transactionId) + expect(endedEvent.transactionInfo.transactionId).toBe(transactionId) + }) + + await it('Should differentiate E03 lifecycle from E02 Cable-First lifecycle', () => { + const connectorId = 1 + const e03TransactionId = generateUUID() + const e02TransactionId = generateUUID() + const idToken: OCPP20IdTokenType = { + idToken: 'COMPARE_TOKEN_001', + type: OCPP20IdTokenEnumType.ISO14443, + } + + // E03 IdToken-First: Starts with Authorized trigger + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId) + const connectorStatus = mockChargingStation.getConnectorStatus(connectorId) + if (connectorStatus != null) { + connectorStatus.transactionIdTokenSent = undefined + } + + const e03Start = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + e03TransactionId, + { idToken } + ) + + // E02 Cable-First: Starts with CablePluggedIn trigger + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId) + if (connectorStatus != null) { + connectorStatus.transactionIdTokenSent = undefined + } + + const e02Start = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.CablePluggedIn, + connectorId, + e02TransactionId + ) + + // Key difference: E03 starts with Authorized, E02 starts with CablePluggedIn + expect(e03Start.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized) + expect(e02Start.triggerReason).toBe(OCPP20TriggerReasonEnumType.CablePluggedIn) + + // E03 includes idToken in first event, E02 may not + expect(e03Start.idToken).toBeDefined() + expect(e02Start.idToken).toBeUndefined() + }) + }) + + // ========================================================================= + // E03.FR.05/06: EVConnectionTimeOut Handling + // ========================================================================= + await describe('E03.FR.05/06 - EVConnectionTimeOut', async () => { + await it('Should support authorization cancellation event (cable not connected)', () => { + const connectorId = 1 + const transactionId = generateUUID() + const idToken: OCPP20IdTokenType = { + idToken: 'TIMEOUT_TOKEN_001', + type: OCPP20IdTokenEnumType.ISO14443, + } + + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId) + + // E03.FR.05: User authorizes with IdToken + const authorizedEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + transactionId, + { idToken } + ) + + // E03.FR.06: Cable not connected within timeout - transaction ends with Timeout + const timeoutEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Ended, + OCPP20TriggerReasonEnumType.EVConnectTimeout, + connectorId, + transactionId + ) + + expect(authorizedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started) + expect(authorizedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Authorized) + + expect(timeoutEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended) + expect(timeoutEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.EVConnectTimeout) + expect(timeoutEvent.seqNo).toBe(1) + + // Same transaction ID for both events + expect(authorizedEvent.transactionInfo.transactionId).toBe( + timeoutEvent.transactionInfo.transactionId + ) + }) + + await it('Should track sequence numbers correctly for timeout scenario', () => { + const connectorId = 1 + const transactionId = generateUUID() + + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId) + + // Started (seqNo: 0) + const startEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + transactionId + ) + + // Ended due to timeout (seqNo: 1) + const endEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Ended, + OCPP20TriggerReasonEnumType.EVConnectTimeout, + connectorId, + transactionId + ) + + expect(startEvent.seqNo).toBe(0) + expect(endEvent.seqNo).toBe(1) + }) + }) + + // ========================================================================= + // Authorization Status Handling + // ========================================================================= + await describe('Authorization Status in E03 Flow', async () => { + await it('Should support Deauthorized trigger for rejected authorization', () => { + const context: OCPP20TransactionContext = { + authorizationMethod: 'idToken', + isDeauthorized: true, + source: 'local_authorization', + } + + const triggerReason = OCPP20ServiceUtils.selectTriggerReason( + OCPP20TransactionEventEnumType.Ended, + context + ) + + expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.Deauthorized) + }) + + await it('Should handle transaction end after token revocation', () => { + const connectorId = 1 + const transactionId = generateUUID() + const idToken: OCPP20IdTokenType = { + idToken: 'REVOKED_TOKEN_001', + type: OCPP20IdTokenEnumType.ISO14443, + } + + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId) + + // Transaction started with authorization + const startEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + transactionId, + { idToken } + ) + + // Transaction ended due to deauthorization (e.g., token revoked mid-session) + const revokedEvent = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Ended, + OCPP20TriggerReasonEnumType.Deauthorized, + connectorId, + transactionId + ) + + expect(startEvent.eventType).toBe(OCPP20TransactionEventEnumType.Started) + expect(revokedEvent.eventType).toBe(OCPP20TransactionEventEnumType.Ended) + expect(revokedEvent.triggerReason).toBe(OCPP20TriggerReasonEnumType.Deauthorized) + }) + + await it('Should support StopAuthorized trigger for normal transaction end', () => { + const context: OCPP20TransactionContext = { + authorizationMethod: 'stopAuthorized', + source: 'local_authorization', + } + + const triggerReason = OCPP20ServiceUtils.selectTriggerReason( + OCPP20TransactionEventEnumType.Ended, + context + ) + + expect(triggerReason).toBe(OCPP20TriggerReasonEnumType.StopAuthorized) + }) + }) + + // ========================================================================= + // E03.FR.07/08: Sequence Numbers and Transaction ID + // ========================================================================= + await describe('E03.FR.07/08 - Sequence Numbers and Transaction ID', async () => { + await it('Should maintain continuous sequence numbers throughout E03 lifecycle', () => { + const connectorId = 1 + const transactionId = generateUUID() + const idToken: OCPP20IdTokenType = { + idToken: 'SEQ_TOKEN_001', + type: OCPP20IdTokenEnumType.ISO14443, + } + + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId) + + const events = [ + OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + transactionId, + { idToken } + ), + OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Updated, + OCPP20TriggerReasonEnumType.CablePluggedIn, + connectorId, + transactionId + ), + OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Updated, + OCPP20TriggerReasonEnumType.ChargingStateChanged, + connectorId, + transactionId + ), + OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Updated, + OCPP20TriggerReasonEnumType.MeterValuePeriodic, + connectorId, + transactionId + ), + OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Ended, + OCPP20TriggerReasonEnumType.StopAuthorized, + connectorId, + transactionId + ), + ] + + // E03.FR.07: Sequence numbers must be continuous + events.forEach((event, index) => { + expect(event.seqNo).toBe(index) + }) + }) + + await it('Should use unique transaction ID (E03.FR.08)', () => { + const connectorId = 1 + + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId) + + const transaction1Id = generateUUID() + const transaction2Id = generateUUID() + + // E03.FR.08: transactionId MUST be unique + expect(transaction1Id).not.toBe(transaction2Id) + + const event1 = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + transaction1Id + ) + + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connectorId) + + const event2 = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connectorId, + transaction2Id + ) + + expect(event1.transactionInfo.transactionId).toBe(transaction1Id) + expect(event2.transactionInfo.transactionId).toBe(transaction2Id) + expect(event1.transactionInfo.transactionId).not.toBe(event2.transactionInfo.transactionId) + }) + }) + + // ========================================================================= + // Multiple Connector Independence + // ========================================================================= + await describe('Multiple Connector Independence in E03 Flow', async () => { + await it('Should handle independent E03 transactions on different connectors', () => { + const connector1 = 1 + const connector2 = 2 + const transaction1Id = generateUUID() + const transaction2Id = generateUUID() + const token1: OCPP20IdTokenType = { + idToken: 'USER_A_TOKEN', + type: OCPP20IdTokenEnumType.ISO14443, + } + const token2: OCPP20IdTokenType = { + idToken: 'USER_B_TOKEN', + type: OCPP20IdTokenEnumType.eMAID, + } + + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connector1) + OCPP20ServiceUtils.resetTransactionSequenceNumber(mockChargingStation, connector2) + + // User A authorizes on connector 1 + const conn1Event1 = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connector1, + transaction1Id, + { idToken: token1 } + ) + + // User B authorizes on connector 2 + const conn2Event1 = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Started, + OCPP20TriggerReasonEnumType.Authorized, + connector2, + transaction2Id, + { idToken: token2 } + ) + + // User A plugs cable + const conn1Event2 = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Updated, + OCPP20TriggerReasonEnumType.CablePluggedIn, + connector1, + transaction1Id + ) + + // User B plugs cable + const conn2Event2 = OCPP20ServiceUtils.buildTransactionEvent( + mockChargingStation, + OCPP20TransactionEventEnumType.Updated, + OCPP20TriggerReasonEnumType.CablePluggedIn, + connector2, + transaction2Id + ) + + // Verify independent sequence numbers + expect(conn1Event1.seqNo).toBe(0) + expect(conn1Event2.seqNo).toBe(1) + expect(conn2Event1.seqNo).toBe(0) + expect(conn2Event2.seqNo).toBe(1) + + // Verify independent transaction IDs + expect(conn1Event1.transactionInfo.transactionId).toBe(transaction1Id) + expect(conn2Event1.transactionInfo.transactionId).toBe(transaction2Id) + expect(transaction1Id).not.toBe(transaction2Id) + + // Verify independent idTokens + expect(conn1Event1.idToken?.idToken).toBe('USER_A_TOKEN') + expect(conn2Event1.idToken?.idToken).toBe('USER_B_TOKEN') + }) + }) +}) -- 2.43.0