From 6019cb28512c77a30a8cbb02a6b33040173740b4 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 1 Apr 2026 18:45:50 +0200 Subject: [PATCH] refactor(ocpp): align builder naming and harmonize test structure 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). --- .../ocpp/1.6/OCPP16RequestBuilders.ts | 6 +- .../ocpp/2.0/OCPP20RequestBuilders.ts | 6 +- src/charging-station/ocpp/OCPPServiceUtils.ts | 8 +- .../ocpp/IdTagAuthorization.test.ts | 298 -------------- .../ocpp/OCPPServiceOperations.test.ts | 384 ++++++++++++------ .../ocpp/OCPPServiceUtils-pure.test.ts | 140 +++++++ 6 files changed, 408 insertions(+), 434 deletions(-) delete mode 100644 tests/charging-station/ocpp/IdTagAuthorization.test.ts diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestBuilders.ts b/src/charging-station/ocpp/1.6/OCPP16RequestBuilders.ts index 6f6894fa..13812bd3 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestBuilders.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestBuilders.ts @@ -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, diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestBuilders.ts b/src/charging-station/ocpp/2.0/OCPP20RequestBuilders.ts index 5457e870..6289bf84 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestBuilders.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestBuilders.ts @@ -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, diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index e277e39f..a9c76f6b 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -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 index c556dab8..00000000 --- a/tests/charging-station/ocpp/IdTagAuthorization.test.ts +++ /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['station'], - overrides?: Parameters[0] -): ReturnType { - 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) - }) - }) -}) diff --git a/tests/charging-station/ocpp/OCPPServiceOperations.test.ts b/tests/charging-station/ocpp/OCPPServiceOperations.test.ts index ccbd486e..74434bc8 100644 --- a/tests/charging-station/ocpp/OCPPServiceOperations.test.ts +++ b/tests/charging-station/ocpp/OCPPServiceOperations.test.ts @@ -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['station'], + overrides?: Parameters[0] +): ReturnType { + 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) }) }) }) diff --git a/tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts b/tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts index 15b3eeb3..97ddf96e 100644 --- a/tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts +++ b/tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts @@ -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) + }) + }) }) -- 2.53.0