]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(ocpp): align builder naming and harmonize test structure
authorJérôme Benoit <jerome.benoit@sap.com>
Wed, 1 Apr 2026 16:45:50 +0000 (18:45 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Wed, 1 Apr 2026 16:45:50 +0000 (18:45 +0200)
Rename buildMeterValueForOCPP16/20 and buildSampledValueForOCPP16/20
to buildOCPP16/20MeterValue and buildOCPP16/20SampledValue matching
the established buildOCPP{version}{Thing} convention.

Merge IdTagAuthorization.test.ts into OCPPServiceOperations.test.ts
(source file was deleted). Move buildBootNotificationRequest tests
from OCPPServiceOperations.test.ts to OCPPServiceUtils-pure.test.ts
(function was moved to Utils).

src/charging-station/ocpp/1.6/OCPP16RequestBuilders.ts
src/charging-station/ocpp/2.0/OCPP20RequestBuilders.ts
src/charging-station/ocpp/OCPPServiceUtils.ts
tests/charging-station/ocpp/IdTagAuthorization.test.ts [deleted file]
tests/charging-station/ocpp/OCPPServiceOperations.test.ts
tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts

index 6f6894faf26efff9fc1f5da1ed7f4857ce089951..13812bd3c42bc39b82ddc05f696febf48198a9fd 100644 (file)
@@ -62,7 +62,7 @@ export const buildOCPP16BootNotificationRequest = (
   }),
 })
 
-export const buildMeterValueForOCPP16 = (
+export const buildOCPP16MeterValue = (
   chargingStation: ChargingStation,
   transactionId: number | string,
   interval: number,
@@ -86,7 +86,7 @@ export const buildMeterValueForOCPP16 = (
     context?: MeterValueContext,
     phase?: MeterValuePhase
   ): OCPP16SampledValue => {
-    return buildSampledValueForOCPP16(sampledValueTemplate, value, context, phase)
+    return buildOCPP16SampledValue(sampledValueTemplate, value, context, phase)
   }
   // SoC measurand
   const socMeasurand = buildSocMeasurandValue(
@@ -313,7 +313,7 @@ export const buildMeterValueForOCPP16 = (
  * @param phase - The phase of the measurement.
  * @returns The built OCPP 1.6 sampled value.
  */
-export function buildSampledValueForOCPP16 (
+export function buildOCPP16SampledValue (
   sampledValueTemplate: SampledValueTemplate,
   value: number,
   context?: MeterValueContext,
index 5457e870217801e68028180f0e86dd3e03b7b6af..6289bf8438b949d01adcf356714b86bf93291f28 100644 (file)
@@ -56,7 +56,7 @@ export const buildOCPP20BootNotificationRequest = (
   reason: bootReason,
 })
 
-export const buildMeterValueForOCPP20 = (
+export const buildOCPP20MeterValue = (
   chargingStation: ChargingStation,
   transactionId: number | string,
   interval: number,
@@ -81,7 +81,7 @@ export const buildMeterValueForOCPP20 = (
     context?: MeterValueContext,
     phase?: MeterValuePhase
   ): OCPP20SampledValue => {
-    return buildSampledValueForOCPP20(sampledValueTemplate, value, context, phase)
+    return buildOCPP20SampledValue(sampledValueTemplate, value, context, phase)
   }
   // SoC measurand
   const socMeasurand = buildSocMeasurandValue(chargingStation, connectorId, evseId, measurandsKey)
@@ -222,7 +222,7 @@ export const buildMeterValueForOCPP20 = (
  * @param phase - The phase of the measurement.
  * @returns The built OCPP 2.0 sampled value.
  */
-export function buildSampledValueForOCPP20 (
+export function buildOCPP20SampledValue (
   sampledValueTemplate: SampledValueTemplate,
   value: number,
   context?: MeterValueContext,
index e277e39f1838afa003e06d563e1d0deb369d8a6b..a9c76f6bbaca5a423e023b3bb9aa8a9da1f18dca 100644 (file)
@@ -58,12 +58,12 @@ import {
   roundTo,
 } from '../../utils/index.js'
 import {
-  buildMeterValueForOCPP16,
   buildOCPP16BootNotificationRequest,
+  buildOCPP16MeterValue,
 } from './1.6/OCPP16RequestBuilders.js'
 import {
-  buildMeterValueForOCPP20,
   buildOCPP20BootNotificationRequest,
+  buildOCPP20MeterValue,
 } from './2.0/OCPP20RequestBuilders.js'
 import { OCPPConstants } from './OCPPConstants.js'
 
@@ -1068,7 +1068,7 @@ export const buildMeterValue = (
   }
   switch (chargingStation.stationInfo?.ocppVersion) {
     case OCPPVersion.VERSION_16:
-      return buildMeterValueForOCPP16(
+      return buildOCPP16MeterValue(
         chargingStation,
         transactionId,
         interval,
@@ -1078,7 +1078,7 @@ export const buildMeterValue = (
       )
     case OCPPVersion.VERSION_20:
     case OCPPVersion.VERSION_201:
-      return buildMeterValueForOCPP20(
+      return buildOCPP20MeterValue(
         chargingStation,
         transactionId,
         interval,
diff --git a/tests/charging-station/ocpp/IdTagAuthorization.test.ts b/tests/charging-station/ocpp/IdTagAuthorization.test.ts
deleted file mode 100644 (file)
index c556dab..0000000
+++ /dev/null
@@ -1,298 +0,0 @@
-/**
- * @file Tests for IdTagAuthorization
- * @description Verifies isIdTagAuthorized authorization function
- *
- * Covers:
- * - isIdTagAuthorized — auth system for all OCPP versions
- * - Connector state management based on authentication method
- *
- * Note: The auth subsystem (OCPPAuthService, strategies, adapters) has its own
- * dedicated test suite in tests/charging-station/ocpp/auth/. These tests verify the
- * wrapper/dispatch layer only — no overlap.
- */
-
-import assert from 'node:assert/strict'
-import { afterEach, describe, it } from 'node:test'
-
-import {
-  AuthContext,
-  AuthenticationMethod,
-  AuthorizationStatus,
-  OCPPAuthServiceFactory,
-} from '../../../src/charging-station/ocpp/auth/index.js'
-import { isIdTagAuthorized } from '../../../src/charging-station/ocpp/OCPPServiceOperations.js'
-import { OCPPVersion } from '../../../src/types/index.js'
-import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
-import { createMockChargingStation } from '../ChargingStationTestUtils.js'
-import {
-  createMockAuthorizationResult,
-  createMockAuthService,
-} from './auth/helpers/MockFactories.js'
-
-/**
- * Registers a mock auth service for the given station in OCPPAuthServiceFactory.
- * @param station - Mock charging station instance
- * @param overrides - Partial overrides for the mock auth service methods
- * @returns The created mock auth service
- */
-function injectMockAuthService (
-  station: ReturnType<typeof createMockChargingStation>['station'],
-  overrides?: Parameters<typeof createMockAuthService>[0]
-): ReturnType<typeof createMockAuthService> {
-  const stationId = station.stationInfo?.chargingStationId ?? 'unknown'
-  const mockService = createMockAuthService(overrides)
-  OCPPAuthServiceFactory.setInstanceForTesting(stationId, mockService)
-  return mockService
-}
-
-await describe('IdTagAuthorization', async () => {
-  afterEach(() => {
-    OCPPAuthServiceFactory.clearAllInstances()
-    standardCleanup()
-  })
-
-  await describe('isIdTagAuthorized', async () => {
-    await it('should return false when auth service rejects the tag', async () => {
-      // Arrange
-      const { station } = createMockChargingStation({
-        stationInfo: { remoteAuthorization: false },
-      })
-      injectMockAuthService(station, {
-        authorize: () =>
-          Promise.resolve(createMockAuthorizationResult({ status: AuthorizationStatus.INVALID })),
-      })
-
-      // Act
-      const result = await isIdTagAuthorized(station, 1, 'TAG-001')
-
-      // Assert
-      assert.strictEqual(result, false)
-    })
-
-    await it('should return true when auth service returns LOCAL_LIST accepted', async () => {
-      // Arrange
-      const { station } = createMockChargingStation()
-      injectMockAuthService(station, {
-        authorize: () =>
-          Promise.resolve(
-            createMockAuthorizationResult({
-              method: AuthenticationMethod.LOCAL_LIST,
-              status: AuthorizationStatus.ACCEPTED,
-            })
-          ),
-      })
-
-      // Act
-      const result = await isIdTagAuthorized(station, 1, 'TAG-001')
-
-      // Assert
-      assert.strictEqual(result, true)
-    })
-
-    await it('should set localAuthorizeIdTag when auth returns LOCAL_LIST method', async () => {
-      // Arrange
-      const { station } = createMockChargingStation()
-      injectMockAuthService(station, {
-        authorize: () =>
-          Promise.resolve(
-            createMockAuthorizationResult({
-              method: AuthenticationMethod.LOCAL_LIST,
-              status: AuthorizationStatus.ACCEPTED,
-            })
-          ),
-      })
-
-      // Act
-      await isIdTagAuthorized(station, 1, 'TAG-001')
-
-      // Assert
-      const connectorStatus = station.getConnectorStatus(1)
-      assert.ok(connectorStatus != null)
-      assert.strictEqual(connectorStatus.localAuthorizeIdTag, 'TAG-001')
-      assert.strictEqual(connectorStatus.idTagLocalAuthorized, true)
-    })
-
-    await it('should set idTagLocalAuthorized when auth returns CACHE method', async () => {
-      // Arrange
-      const { station } = createMockChargingStation()
-      injectMockAuthService(station, {
-        authorize: () =>
-          Promise.resolve(
-            createMockAuthorizationResult({
-              method: AuthenticationMethod.CACHE,
-              status: AuthorizationStatus.ACCEPTED,
-            })
-          ),
-      })
-
-      // Act
-      await isIdTagAuthorized(station, 1, 'TAG-CACHED')
-
-      // Assert
-      const connectorStatus = station.getConnectorStatus(1)
-      assert.ok(connectorStatus != null)
-      assert.strictEqual(connectorStatus.localAuthorizeIdTag, 'TAG-CACHED')
-      assert.strictEqual(connectorStatus.idTagLocalAuthorized, true)
-    })
-
-    await it('should authorize remotely when auth service returns REMOTE_AUTHORIZATION accepted', async () => {
-      // Arrange
-      const { station } = createMockChargingStation({
-        stationInfo: { remoteAuthorization: true },
-      })
-      injectMockAuthService(station, {
-        authorize: () =>
-          Promise.resolve(
-            createMockAuthorizationResult({
-              method: AuthenticationMethod.REMOTE_AUTHORIZATION,
-              status: AuthorizationStatus.ACCEPTED,
-            })
-          ),
-      })
-
-      // Act
-      const result = await isIdTagAuthorized(station, 1, 'TAG-001')
-
-      // Assert
-      assert.strictEqual(result, true)
-    })
-
-    await it('should not set localAuthorizeIdTag when REMOTE_AUTHORIZATION method', async () => {
-      // Arrange
-      const { station } = createMockChargingStation({
-        stationInfo: { remoteAuthorization: true },
-      })
-      injectMockAuthService(station, {
-        authorize: () =>
-          Promise.resolve(
-            createMockAuthorizationResult({
-              method: AuthenticationMethod.REMOTE_AUTHORIZATION,
-              status: AuthorizationStatus.ACCEPTED,
-            })
-          ),
-      })
-
-      // Act
-      await isIdTagAuthorized(station, 1, 'TAG-001')
-
-      // Assert
-      const connectorStatus = station.getConnectorStatus(1)
-      assert.ok(connectorStatus != null)
-      assert.strictEqual(connectorStatus.localAuthorizeIdTag, undefined)
-      assert.notStrictEqual(connectorStatus.idTagLocalAuthorized, true)
-    })
-
-    await it('should return false when remote authorization rejects the tag', async () => {
-      // Arrange
-      const { station } = createMockChargingStation({
-        stationInfo: { remoteAuthorization: true },
-      })
-      injectMockAuthService(station, {
-        authorize: () =>
-          Promise.resolve(
-            createMockAuthorizationResult({
-              method: AuthenticationMethod.REMOTE_AUTHORIZATION,
-              status: AuthorizationStatus.BLOCKED,
-            })
-          ),
-      })
-
-      // Act
-      const result = await isIdTagAuthorized(station, 1, 'TAG-999')
-
-      // Assert
-      assert.strictEqual(result, false)
-    })
-
-    await it('should return true but not set connector state for non-existent connector', async () => {
-      // Arrange
-      const { station } = createMockChargingStation()
-      injectMockAuthService(station, {
-        authorize: () =>
-          Promise.resolve(
-            createMockAuthorizationResult({
-              method: AuthenticationMethod.LOCAL_LIST,
-              status: AuthorizationStatus.ACCEPTED,
-            })
-          ),
-      })
-
-      // Act
-      const result = await isIdTagAuthorized(station, 99, 'TAG-001')
-
-      // Assert
-      assert.strictEqual(result, true)
-      const connectorStatus = station.getConnectorStatus(99)
-      assert.strictEqual(connectorStatus, undefined)
-    })
-
-    await it('should set localAuthorizeIdTag when auth returns OFFLINE_FALLBACK method', async () => {
-      // Arrange
-      const { station } = createMockChargingStation()
-      injectMockAuthService(station, {
-        authorize: () =>
-          Promise.resolve(
-            createMockAuthorizationResult({
-              method: AuthenticationMethod.OFFLINE_FALLBACK,
-              status: AuthorizationStatus.ACCEPTED,
-            })
-          ),
-      })
-
-      // Act
-      await isIdTagAuthorized(station, 1, 'TAG-OFFLINE')
-
-      // Assert
-      const connectorStatus = station.getConnectorStatus(1)
-      assert.ok(connectorStatus != null)
-      assert.strictEqual(connectorStatus.localAuthorizeIdTag, 'TAG-OFFLINE')
-      assert.strictEqual(connectorStatus.idTagLocalAuthorized, true)
-    })
-
-    await it('should return false when auth service throws an error', async () => {
-      // Arrange
-      const { station } = createMockChargingStation()
-      injectMockAuthService(station, {
-        authorize: () => Promise.reject(new Error('Test auth service error')),
-      })
-
-      // Act
-      const result = await isIdTagAuthorized(station, 1, 'TAG-ERROR')
-
-      // Assert
-      assert.strictEqual(result, false)
-    })
-
-    await it('should accept explicit auth context parameter', async () => {
-      // Arrange
-      const { station } = createMockChargingStation()
-      let capturedContext: string | undefined
-      injectMockAuthService(station, {
-        authorize: (request: { context?: string }) => {
-          capturedContext = request.context
-          return Promise.resolve(
-            createMockAuthorizationResult({
-              method: AuthenticationMethod.LOCAL_LIST,
-              status: AuthorizationStatus.ACCEPTED,
-            })
-          )
-        },
-      })
-
-      // Act
-      await isIdTagAuthorized(station, 1, 'TAG-001', AuthContext.REMOTE_START)
-
-      // Assert
-      assert.strictEqual(capturedContext, 'RemoteStart')
-    })
-
-    await it('should return false when no auth service is registered for station', async () => {
-      const { station } = createMockChargingStation({
-        ocppVersion: OCPPVersion.VERSION_20,
-      })
-
-      const result = await isIdTagAuthorized(station, 1, 'TAG-001')
-      assert.strictEqual(result, false)
-    })
-  })
-})
index ccbd486ead8527ddd93f672c489e436057e81876..74434bc8e9d4f479634cdbc5779bb0de21423757 100644 (file)
@@ -2,30 +2,35 @@
  * @file Tests for OCPPServiceOperations version-dispatching functions
  * @description Verifies startTransactionOnConnector, stopTransactionOnConnector,
  *              stopRunningTransactions, flushQueuedTransactionMessages, and
- *              buildBootNotificationRequest cross-version dispatchers
+ *              isIdTagAuthorized cross-version dispatchers
  */
 
 import assert from 'node:assert/strict'
 import { afterEach, describe, it, mock } from 'node:test'
 
 import type { ChargingStation } from '../../../src/charging-station/index.js'
-import type { ChargingStationInfo } from '../../../src/types/index.js'
 import type { MockChargingStationOptions } from '../helpers/StationHelpers.js'
 
+import {
+  AuthContext,
+  AuthenticationMethod,
+  AuthorizationStatus,
+  OCPPAuthServiceFactory,
+} from '../../../src/charging-station/ocpp/auth/index.js'
 import {
   flushQueuedTransactionMessages,
+  isIdTagAuthorized,
   startTransactionOnConnector,
   stopRunningTransactions,
   stopTransactionOnConnector,
 } from '../../../src/charging-station/ocpp/OCPPServiceOperations.js'
-import { buildBootNotificationRequest } from '../../../src/charging-station/ocpp/OCPPServiceUtils.js'
-import {
-  BootReasonEnumType,
-  type OCPP20TransactionEventRequest,
-  OCPPVersion,
-} from '../../../src/types/index.js'
+import { type OCPP20TransactionEventRequest, OCPPVersion } from '../../../src/types/index.js'
 import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
 import { createMockChargingStation } from '../ChargingStationTestUtils.js'
+import {
+  createMockAuthorizationResult,
+  createMockAuthService,
+} from './auth/helpers/MockFactories.js'
 
 /**
  * Creates a mock charging station with a tracked request handler for testing
@@ -44,6 +49,22 @@ function createStationWithRequestHandler (opts?: Partial<MockChargingStationOpti
   return { requestHandler, station }
 }
 
+/**
+ * Registers a mock auth service for the given station in OCPPAuthServiceFactory.
+ * @param station - Mock charging station instance
+ * @param overrides - Partial overrides for the mock auth service methods
+ * @returns The created mock auth service
+ */
+function injectMockAuthService (
+  station: ReturnType<typeof createMockChargingStation>['station'],
+  overrides?: Parameters<typeof createMockAuthService>[0]
+): ReturnType<typeof createMockAuthService> {
+  const stationId = station.stationInfo?.chargingStationId ?? 'unknown'
+  const mockService = createMockAuthService(overrides)
+  OCPPAuthServiceFactory.setInstanceForTesting(stationId, mockService)
+  return mockService
+}
+
 /**
  * Configures a connector with a pending (not started) transaction for testing
  * @param station - the charging station mock
@@ -89,6 +110,7 @@ function setupTransaction (
 
 await describe('OCPPServiceOperations', async () => {
   afterEach(() => {
+    OCPPAuthServiceFactory.clearAllInstances()
     standardCleanup()
   })
 
@@ -313,138 +335,248 @@ await describe('OCPPServiceOperations', async () => {
     })
   })
 
-  await describe('buildBootNotificationRequest', async () => {
-    await describe('OCPP 1.6', async () => {
-      await it('should build OCPP 1.6 boot notification with required fields', () => {
-        const stationInfo = {
-          chargePointModel: 'TestModel',
-          chargePointVendor: 'TestVendor',
-          ocppVersion: OCPPVersion.VERSION_16,
-        } as unknown as ChargingStationInfo
-
-        const result = buildBootNotificationRequest(stationInfo)
-
-        assert.notStrictEqual(result, undefined)
-        assert.deepStrictEqual(result, {
-          chargePointModel: 'TestModel',
-          chargePointVendor: 'TestVendor',
-        })
+  await describe('isIdTagAuthorized', async () => {
+    await it('should return false when auth service rejects the tag', async () => {
+      // Arrange
+      const { station } = createMockChargingStation({
+        stationInfo: { remoteAuthorization: false },
+      })
+      injectMockAuthService(station, {
+        authorize: () =>
+          Promise.resolve(createMockAuthorizationResult({ status: AuthorizationStatus.INVALID })),
       })
 
-      await it('should build OCPP 1.6 boot notification with optional fields', () => {
-        // Arrange
-        const stationInfo = {
-          chargeBoxSerialNumber: 'CB-001',
-          chargePointModel: 'TestModel',
-          chargePointSerialNumber: 'CP-001',
-          chargePointVendor: 'TestVendor',
-          firmwareVersion: '1.0.0',
-          iccid: '8901234567890',
-          imsi: '310150123456789',
-          meterSerialNumber: 'M-001',
-          meterType: 'ACMeter',
-          ocppVersion: OCPPVersion.VERSION_16,
-        } as unknown as ChargingStationInfo
-
-        // Act
-        const result = buildBootNotificationRequest(stationInfo)
-
-        // Assert
-        assert.deepStrictEqual(result, {
-          chargeBoxSerialNumber: 'CB-001',
-          chargePointModel: 'TestModel',
-          chargePointSerialNumber: 'CP-001',
-          chargePointVendor: 'TestVendor',
-          firmwareVersion: '1.0.0',
-          iccid: '8901234567890',
-          imsi: '310150123456789',
-          meterSerialNumber: 'M-001',
-          meterType: 'ACMeter',
-        })
+      // Act
+      const result = await isIdTagAuthorized(station, 1, 'TAG-001')
+
+      // Assert
+      assert.strictEqual(result, false)
+    })
+
+    await it('should return true when auth service returns LOCAL_LIST accepted', async () => {
+      // Arrange
+      const { station } = createMockChargingStation()
+      injectMockAuthService(station, {
+        authorize: () =>
+          Promise.resolve(
+            createMockAuthorizationResult({
+              method: AuthenticationMethod.LOCAL_LIST,
+              status: AuthorizationStatus.ACCEPTED,
+            })
+          ),
       })
+
+      // Act
+      const result = await isIdTagAuthorized(station, 1, 'TAG-001')
+
+      // Assert
+      assert.strictEqual(result, true)
     })
 
-    await describe('OCPP 2.0', async () => {
-      await it('should build OCPP 2.0 boot notification with required fields', () => {
-        const stationInfo = {
-          chargePointModel: 'TestModel',
-          chargePointVendor: 'TestVendor',
-          ocppVersion: OCPPVersion.VERSION_20,
-        } as unknown as ChargingStationInfo
-
-        const result = buildBootNotificationRequest(stationInfo)
-
-        assert.notStrictEqual(result, undefined)
-        assert.deepStrictEqual(result, {
-          chargingStation: {
-            model: 'TestModel',
-            vendorName: 'TestVendor',
-          },
-          reason: BootReasonEnumType.PowerUp,
-        })
+    await it('should set localAuthorizeIdTag when auth returns LOCAL_LIST method', async () => {
+      // Arrange
+      const { station } = createMockChargingStation()
+      injectMockAuthService(station, {
+        authorize: () =>
+          Promise.resolve(
+            createMockAuthorizationResult({
+              method: AuthenticationMethod.LOCAL_LIST,
+              status: AuthorizationStatus.ACCEPTED,
+            })
+          ),
       })
 
-      await it('should build OCPP 2.0 boot notification with optional fields and modem', () => {
-        // Arrange
-        const stationInfo = {
-          chargeBoxSerialNumber: 'CB-001',
-          chargePointModel: 'TestModel',
-          chargePointVendor: 'TestVendor',
-          firmwareVersion: '2.0.0',
-          iccid: '8901234567890',
-          imsi: '310150123456789',
-          ocppVersion: OCPPVersion.VERSION_201,
-        } as unknown as ChargingStationInfo
-
-        // Act
-        const result = buildBootNotificationRequest(stationInfo)
-
-        // Assert
-        assert.deepStrictEqual(result, {
-          chargingStation: {
-            firmwareVersion: '2.0.0',
-            model: 'TestModel',
-            modem: {
-              iccid: '8901234567890',
-              imsi: '310150123456789',
-            },
-            serialNumber: 'CB-001',
-            vendorName: 'TestVendor',
-          },
-          reason: BootReasonEnumType.PowerUp,
-        })
+      // Act
+      await isIdTagAuthorized(station, 1, 'TAG-001')
+
+      // Assert
+      const connectorStatus = station.getConnectorStatus(1)
+      assert.ok(connectorStatus != null)
+      assert.strictEqual(connectorStatus.localAuthorizeIdTag, 'TAG-001')
+      assert.strictEqual(connectorStatus.idTagLocalAuthorized, true)
+    })
+
+    await it('should set idTagLocalAuthorized when auth returns CACHE method', async () => {
+      // Arrange
+      const { station } = createMockChargingStation()
+      injectMockAuthService(station, {
+        authorize: () =>
+          Promise.resolve(
+            createMockAuthorizationResult({
+              method: AuthenticationMethod.CACHE,
+              status: AuthorizationStatus.ACCEPTED,
+            })
+          ),
       })
 
-      await it('should build OCPP 2.0 boot notification with custom boot reason', () => {
-        const stationInfo = {
-          chargePointModel: 'TestModel',
-          chargePointVendor: 'TestVendor',
-          ocppVersion: OCPPVersion.VERSION_20,
-        } as unknown as ChargingStationInfo
-
-        const result = buildBootNotificationRequest(stationInfo, BootReasonEnumType.RemoteReset)
-
-        assert.notStrictEqual(result, undefined)
-        assert.deepStrictEqual(result, {
-          chargingStation: {
-            model: 'TestModel',
-            vendorName: 'TestVendor',
-          },
-          reason: BootReasonEnumType.RemoteReset,
-        })
+      // Act
+      await isIdTagAuthorized(station, 1, 'TAG-CACHED')
+
+      // Assert
+      const connectorStatus = station.getConnectorStatus(1)
+      assert.ok(connectorStatus != null)
+      assert.strictEqual(connectorStatus.localAuthorizeIdTag, 'TAG-CACHED')
+      assert.strictEqual(connectorStatus.idTagLocalAuthorized, true)
+    })
+
+    await it('should authorize remotely when auth service returns REMOTE_AUTHORIZATION accepted', async () => {
+      // Arrange
+      const { station } = createMockChargingStation({
+        stationInfo: { remoteAuthorization: true },
+      })
+      injectMockAuthService(station, {
+        authorize: () =>
+          Promise.resolve(
+            createMockAuthorizationResult({
+              method: AuthenticationMethod.REMOTE_AUTHORIZATION,
+              status: AuthorizationStatus.ACCEPTED,
+            })
+          ),
       })
+
+      // Act
+      const result = await isIdTagAuthorized(station, 1, 'TAG-001')
+
+      // Assert
+      assert.strictEqual(result, true)
     })
 
-    await it('should return undefined for unsupported version', () => {
-      const stationInfo = {
-        chargePointModel: 'TestModel',
-        chargePointVendor: 'TestVendor',
-        ocppVersion: '3.0',
-      } as unknown as ChargingStationInfo
+    await it('should not set localAuthorizeIdTag when REMOTE_AUTHORIZATION method', async () => {
+      // Arrange
+      const { station } = createMockChargingStation({
+        stationInfo: { remoteAuthorization: true },
+      })
+      injectMockAuthService(station, {
+        authorize: () =>
+          Promise.resolve(
+            createMockAuthorizationResult({
+              method: AuthenticationMethod.REMOTE_AUTHORIZATION,
+              status: AuthorizationStatus.ACCEPTED,
+            })
+          ),
+      })
 
-      const result = buildBootNotificationRequest(stationInfo)
+      // Act
+      await isIdTagAuthorized(station, 1, 'TAG-001')
+
+      // Assert
+      const connectorStatus = station.getConnectorStatus(1)
+      assert.ok(connectorStatus != null)
+      assert.strictEqual(connectorStatus.localAuthorizeIdTag, undefined)
+      assert.notStrictEqual(connectorStatus.idTagLocalAuthorized, true)
+    })
+
+    await it('should return false when remote authorization rejects the tag', async () => {
+      // Arrange
+      const { station } = createMockChargingStation({
+        stationInfo: { remoteAuthorization: true },
+      })
+      injectMockAuthService(station, {
+        authorize: () =>
+          Promise.resolve(
+            createMockAuthorizationResult({
+              method: AuthenticationMethod.REMOTE_AUTHORIZATION,
+              status: AuthorizationStatus.BLOCKED,
+            })
+          ),
+      })
+
+      // Act
+      const result = await isIdTagAuthorized(station, 1, 'TAG-999')
+
+      // Assert
+      assert.strictEqual(result, false)
+    })
+
+    await it('should return true but not set connector state for non-existent connector', async () => {
+      // Arrange
+      const { station } = createMockChargingStation()
+      injectMockAuthService(station, {
+        authorize: () =>
+          Promise.resolve(
+            createMockAuthorizationResult({
+              method: AuthenticationMethod.LOCAL_LIST,
+              status: AuthorizationStatus.ACCEPTED,
+            })
+          ),
+      })
+
+      // Act
+      const result = await isIdTagAuthorized(station, 99, 'TAG-001')
+
+      // Assert
+      assert.strictEqual(result, true)
+      const connectorStatus = station.getConnectorStatus(99)
+      assert.strictEqual(connectorStatus, undefined)
+    })
+
+    await it('should set localAuthorizeIdTag when auth returns OFFLINE_FALLBACK method', async () => {
+      // Arrange
+      const { station } = createMockChargingStation()
+      injectMockAuthService(station, {
+        authorize: () =>
+          Promise.resolve(
+            createMockAuthorizationResult({
+              method: AuthenticationMethod.OFFLINE_FALLBACK,
+              status: AuthorizationStatus.ACCEPTED,
+            })
+          ),
+      })
+
+      // Act
+      await isIdTagAuthorized(station, 1, 'TAG-OFFLINE')
+
+      // Assert
+      const connectorStatus = station.getConnectorStatus(1)
+      assert.ok(connectorStatus != null)
+      assert.strictEqual(connectorStatus.localAuthorizeIdTag, 'TAG-OFFLINE')
+      assert.strictEqual(connectorStatus.idTagLocalAuthorized, true)
+    })
+
+    await it('should return false when auth service throws an error', async () => {
+      // Arrange
+      const { station } = createMockChargingStation()
+      injectMockAuthService(station, {
+        authorize: () => Promise.reject(new Error('Test auth service error')),
+      })
+
+      // Act
+      const result = await isIdTagAuthorized(station, 1, 'TAG-ERROR')
+
+      // Assert
+      assert.strictEqual(result, false)
+    })
+
+    await it('should accept explicit auth context parameter', async () => {
+      // Arrange
+      const { station } = createMockChargingStation()
+      let capturedContext: string | undefined
+      injectMockAuthService(station, {
+        authorize: (request: { context?: string }) => {
+          capturedContext = request.context
+          return Promise.resolve(
+            createMockAuthorizationResult({
+              method: AuthenticationMethod.LOCAL_LIST,
+              status: AuthorizationStatus.ACCEPTED,
+            })
+          )
+        },
+      })
+
+      // Act
+      await isIdTagAuthorized(station, 1, 'TAG-001', AuthContext.REMOTE_START)
+
+      // Assert
+      assert.strictEqual(capturedContext, 'RemoteStart')
+    })
+
+    await it('should return false when no auth service is registered for station', async () => {
+      const { station } = createMockChargingStation({
+        ocppVersion: OCPPVersion.VERSION_20,
+      })
 
-      assert.strictEqual(result, undefined)
+      const result = await isIdTagAuthorized(station, 1, 'TAG-001')
+      assert.strictEqual(result, false)
     })
   })
 })
index 15b3eeb37ddac52ec4c7695d71be19e9153463c9..97ddf96e4ddf23302b1233c8f9b4d8227df4eaca 100644 (file)
@@ -4,6 +4,7 @@
  *
  * Covers:
  * - ajvErrorsToErrorType — maps AJV validation errors to OCPP ErrorType
+ * - buildBootNotificationRequest — builds version-specific boot notification payloads
  * - convertDateToISOString — recursively converts Date objects to ISO strings in-place
  * - isConnectorIdValid — validates connector ID ranges
  * - mapStopReasonToOCPP20 — maps OCPP 1.6 stop reasons to OCPP 2.0 equivalents
@@ -18,14 +19,18 @@ import type { ChargingStation } from '../../../src/charging-station/index.js'
 
 import {
   ajvErrorsToErrorType,
+  buildBootNotificationRequest,
   convertDateToISOString,
   isConnectorIdValid,
   mapStopReasonToOCPP20,
 } from '../../../src/charging-station/ocpp/OCPPServiceUtils.js'
 import {
+  BootReasonEnumType,
+  type ChargingStationInfo,
   ErrorType,
   IncomingRequestCommand,
   type JsonType,
+  OCPPVersion,
   type StopTransactionReason,
 } from '../../../src/types/index.js'
 import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
@@ -192,4 +197,139 @@ await describe('OCPPServiceUtils — pure functions', async () => {
       assert.strictEqual(result.triggerReason, 'RemoteStop')
     })
   })
+
+  await describe('buildBootNotificationRequest', async () => {
+    await describe('OCPP 1.6', async () => {
+      await it('should build OCPP 1.6 boot notification with required fields', () => {
+        const stationInfo = {
+          chargePointModel: 'TestModel',
+          chargePointVendor: 'TestVendor',
+          ocppVersion: OCPPVersion.VERSION_16,
+        } as unknown as ChargingStationInfo
+
+        const result = buildBootNotificationRequest(stationInfo)
+
+        assert.notStrictEqual(result, undefined)
+        assert.deepStrictEqual(result, {
+          chargePointModel: 'TestModel',
+          chargePointVendor: 'TestVendor',
+        })
+      })
+
+      await it('should build OCPP 1.6 boot notification with optional fields', () => {
+        // Arrange
+        const stationInfo = {
+          chargeBoxSerialNumber: 'CB-001',
+          chargePointModel: 'TestModel',
+          chargePointSerialNumber: 'CP-001',
+          chargePointVendor: 'TestVendor',
+          firmwareVersion: '1.0.0',
+          iccid: '8901234567890',
+          imsi: '310150123456789',
+          meterSerialNumber: 'M-001',
+          meterType: 'ACMeter',
+          ocppVersion: OCPPVersion.VERSION_16,
+        } as unknown as ChargingStationInfo
+
+        // Act
+        const result = buildBootNotificationRequest(stationInfo)
+
+        // Assert
+        assert.deepStrictEqual(result, {
+          chargeBoxSerialNumber: 'CB-001',
+          chargePointModel: 'TestModel',
+          chargePointSerialNumber: 'CP-001',
+          chargePointVendor: 'TestVendor',
+          firmwareVersion: '1.0.0',
+          iccid: '8901234567890',
+          imsi: '310150123456789',
+          meterSerialNumber: 'M-001',
+          meterType: 'ACMeter',
+        })
+      })
+    })
+
+    await describe('OCPP 2.0', async () => {
+      await it('should build OCPP 2.0 boot notification with required fields', () => {
+        const stationInfo = {
+          chargePointModel: 'TestModel',
+          chargePointVendor: 'TestVendor',
+          ocppVersion: OCPPVersion.VERSION_20,
+        } as unknown as ChargingStationInfo
+
+        const result = buildBootNotificationRequest(stationInfo)
+
+        assert.notStrictEqual(result, undefined)
+        assert.deepStrictEqual(result, {
+          chargingStation: {
+            model: 'TestModel',
+            vendorName: 'TestVendor',
+          },
+          reason: BootReasonEnumType.PowerUp,
+        })
+      })
+
+      await it('should build OCPP 2.0 boot notification with optional fields and modem', () => {
+        // Arrange
+        const stationInfo = {
+          chargeBoxSerialNumber: 'CB-001',
+          chargePointModel: 'TestModel',
+          chargePointVendor: 'TestVendor',
+          firmwareVersion: '2.0.0',
+          iccid: '8901234567890',
+          imsi: '310150123456789',
+          ocppVersion: OCPPVersion.VERSION_201,
+        } as unknown as ChargingStationInfo
+
+        // Act
+        const result = buildBootNotificationRequest(stationInfo)
+
+        // Assert
+        assert.deepStrictEqual(result, {
+          chargingStation: {
+            firmwareVersion: '2.0.0',
+            model: 'TestModel',
+            modem: {
+              iccid: '8901234567890',
+              imsi: '310150123456789',
+            },
+            serialNumber: 'CB-001',
+            vendorName: 'TestVendor',
+          },
+          reason: BootReasonEnumType.PowerUp,
+        })
+      })
+
+      await it('should build OCPP 2.0 boot notification with custom boot reason', () => {
+        const stationInfo = {
+          chargePointModel: 'TestModel',
+          chargePointVendor: 'TestVendor',
+          ocppVersion: OCPPVersion.VERSION_20,
+        } as unknown as ChargingStationInfo
+
+        const result = buildBootNotificationRequest(stationInfo, BootReasonEnumType.RemoteReset)
+
+        assert.notStrictEqual(result, undefined)
+        assert.deepStrictEqual(result, {
+          chargingStation: {
+            model: 'TestModel',
+            vendorName: 'TestVendor',
+          },
+          reason: BootReasonEnumType.RemoteReset,
+        })
+      })
+    })
+
+    await it('should return undefined for unsupported version', () => {
+      const stationInfo = {
+        chargePointModel: 'TestModel',
+        chargePointVendor: 'TestVendor',
+        ocppVersion: '3.0',
+      } as unknown as ChargingStationInfo
+
+      const result = buildBootNotificationRequest(stationInfo)
+
+      assert.strictEqual(result, undefined)
+    })
+  })
 })