From c2e5cc55e03988b2009b2a51111ecf0f02ec9cad Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 3 Apr 2026 21:23:45 +0200 Subject: [PATCH] refactor: improve helper/utility usage consistency across codebase - Make worker module standalone by inlining mergeDeepRight in WorkerUtils - Replace console/chalk with structured logger calls in Bootstrap - Extract magic numbers to named constants (auth cache, WS reconnect) - Use setupConnectorWithTransaction helper in transaction tests - Remove dead test helper OCPPAuthIntegrationTest - Fix 'shutdowning' typo in graceful shutdown log message --- src/charging-station/Bootstrap.ts | 107 ++--- src/charging-station/ChargingStation.ts | 2 +- .../ocpp/auth/cache/InMemoryAuthCache.ts | 10 +- .../auth/factories/AuthComponentFactory.ts | 2 +- src/utils/Constants.ts | 8 + src/worker/WorkerFactory.ts | 2 +- src/worker/WorkerUtils.ts | 23 ++ .../ChargingStation-Transactions.test.ts | 96 ++--- tests/helpers/OCPPAuthIntegrationTest.ts | 382 ------------------ 9 files changed, 129 insertions(+), 503 deletions(-) delete mode 100644 tests/helpers/OCPPAuthIntegrationTest.ts diff --git a/src/charging-station/Bootstrap.ts b/src/charging-station/Bootstrap.ts index acc6388d..d82574c6 100644 --- a/src/charging-station/Bootstrap.ts +++ b/src/charging-station/Bootstrap.ts @@ -2,7 +2,6 @@ import type { Worker } from 'node:worker_threads' -import chalk from 'chalk' import { EventEmitter } from 'node:events' import { dirname, extname, join } from 'node:path' import process, { exit } from 'node:process' @@ -255,10 +254,8 @@ export class Bootstrap extends EventEmitter implements IBootstrap { try { await this.addChargingStation(index, stationTemplateUrl.file) } catch (error) { - console.error( - chalk.red( - `Error at starting charging station with template file ${stationTemplateUrl.file}: ` - ), + logger.error( + `${this.logPrefix()} ${moduleName}.start: Error at starting charging station with template file ${stationTemplateUrl.file}:`, error ) } @@ -271,10 +268,8 @@ export class Bootstrap extends EventEmitter implements IBootstrap { ) for (const result of results) { if (result.status === 'rejected') { - console.error( - chalk.red( - `Error at starting charging station with template file ${stationTemplateUrl.file}: ` - ), + logger.error( + `${this.logPrefix()} ${moduleName}.start: Error at starting charging station with template file ${stationTemplateUrl.file}:`, result.reason ) } @@ -284,43 +279,46 @@ export class Bootstrap extends EventEmitter implements IBootstrap { const workerConfiguration = Configuration.getConfigurationSection( ConfigurationSection.worker ) - console.info( - chalk.green( - `Charging stations simulator ${this.version} started with ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s) from ${this.numberOfChargingStationTemplates.toString()} charging station template(s) and ${ - Configuration.workerDynamicPoolInUse() - ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${workerConfiguration.poolMinSize?.toString()}/` - : '' - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - }${this.workerImplementation?.size.toString()}${ - Configuration.workerPoolInUse() - ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `/${workerConfiguration.poolMaxSize?.toString()}` - : '' - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - } worker(s) concurrently running in '${workerConfiguration.processType}' mode${ - this.workerImplementation?.maxElementsPerWorker != null - ? ` (${this.workerImplementation.maxElementsPerWorker.toString()} charging station(s) per worker)` - : '' - }` - ) + logger.info( + `${this.logPrefix()} ${moduleName}.start: Charging stations simulator ${this.version} started with ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s) from ${this.numberOfChargingStationTemplates.toString()} charging station template(s) and ${ + Configuration.workerDynamicPoolInUse() + ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${workerConfiguration.poolMinSize?.toString()}/` + : '' + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + }${this.workerImplementation?.size.toString()}${ + Configuration.workerPoolInUse() + ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `/${workerConfiguration.poolMaxSize?.toString()}` + : '' + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + } worker(s) concurrently running in '${workerConfiguration.processType}' mode${ + this.workerImplementation?.maxElementsPerWorker != null + ? ` (${this.workerImplementation.maxElementsPerWorker.toString()} charging station(s) per worker)` + : '' + }` ) Configuration.workerDynamicPoolInUse() && - console.warn( - chalk.yellow( - 'Charging stations simulator is using dynamic pool mode. This is an experimental feature with known issues.\nPlease consider using fixed pool or worker set mode instead' - ) + logger.warn( + `${this.logPrefix()} ${moduleName}.start: Charging stations simulator is using dynamic pool mode. This is an experimental feature with known issues.\nPlease consider using fixed pool or worker set mode instead` ) - console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info) + logger.info( + `${this.logPrefix()} ${moduleName}.start: Worker set/pool information:`, + this.workerImplementation?.info + ) this.started = true } finally { this.starting = false } } else { - console.error(chalk.red('Cannot start an already starting charging stations simulator')) + logger.error( + `${this.logPrefix()} ${moduleName}.start: Cannot start an already starting charging stations simulator` + ) } } else { - console.error(chalk.red('Cannot start an already started charging stations simulator')) + logger.error( + `${this.logPrefix()} ${moduleName}.start: Cannot start an already started charging stations simulator` + ) } } @@ -347,17 +345,21 @@ export class Bootstrap extends EventEmitter implements IBootstrap { this.stopping = false } } else { - console.error(chalk.red('Cannot stop an already stopping charging stations simulator')) + logger.error( + `${this.logPrefix()} ${moduleName}.stop: Cannot stop an already stopping charging stations simulator` + ) } } else { - console.error(chalk.red('Cannot stop an already stopped charging stations simulator')) + logger.error( + `${this.logPrefix()} ${moduleName}.stop: Cannot stop an already stopped charging stations simulator` + ) } } private gracefulShutdown (): void { this.stop() .then(() => { - console.info(chalk.green('Graceful shutdown')) + logger.info(`${this.logPrefix()} ${moduleName}.gracefulShutdown: Graceful shutdown`) if (this.uiServerStarted) { this.uiServer.stop() this.uiServerStarted = false @@ -365,7 +367,10 @@ export class Bootstrap extends EventEmitter implements IBootstrap { return exit(exitCodes.succeeded) }) .catch((error: unknown) => { - console.error(chalk.red('Error while shutdowning charging stations simulator: '), error) + logger.error( + `${this.logPrefix()} ${moduleName}.gracefulShutdown: Error while shutting down charging stations simulator:`, + error + ) exit(exitCodes.gracefulShutdownError) }) } @@ -384,16 +389,14 @@ export class Bootstrap extends EventEmitter implements IBootstrap { }) } if (this.templateStatistics.size !== stationTemplateUrls.length) { - console.error( - chalk.red( - "'stationTemplateUrls' contains duplicate entries, please check your configuration" - ) + logger.error( + `${this.logPrefix()} ${moduleName}.initializeCounters: 'stationTemplateUrls' contains duplicate entries, please check your configuration` ) exit(exitCodes.duplicateChargingStationTemplateUrls) } } else { - console.error( - chalk.red("'stationTemplateUrls' not defined or empty, please check your configuration") + logger.error( + `${this.logPrefix()} ${moduleName}.initializeCounters: 'stationTemplateUrls' not defined or empty, please check your configuration` ) exit(exitCodes.missingChargingStationsConfiguration) } @@ -402,10 +405,8 @@ export class Bootstrap extends EventEmitter implements IBootstrap { Configuration.getConfigurationSection(ConfigurationSection.uiServer) .enabled !== true ) { - console.error( - chalk.red( - "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration" - ) + logger.error( + `${this.logPrefix()} ${moduleName}.initializeCounters: 'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration` ) exit(exitCodes.noChargingStationTemplates) } @@ -587,8 +588,10 @@ export class Bootstrap extends EventEmitter implements IBootstrap { const timeoutMessage = `Timeout ${formatDurationMilliSeconds( Constants.STOP_CHARGING_STATIONS_TIMEOUT )} reached at stopping charging stations` - console.warn(chalk.yellow(timeoutMessage)) - reject(new Error(timeoutMessage)) + logger.warn( + `${this.logPrefix()} ${moduleName}.waitChargingStationsStopped: ${timeoutMessage}` + ) + reject(new BaseError(timeoutMessage)) }, Constants.STOP_CHARGING_STATIONS_TIMEOUT) waitChargingStationEvents( this, diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index fc44f304..d98164b2 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -2332,7 +2332,7 @@ export class ChargingStation extends EventEmitter { this.bootNotificationResponse?.interval != null ? secondsToMilliseconds(this.bootNotificationResponse.interval) : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL, - jitterMs: 1000, + jitterMs: Constants.DEFAULT_WS_RECONNECT_TIMEOUT_OFFSET, retryNumber: registrationRetryCount, }) ) diff --git a/src/charging-station/ocpp/auth/cache/InMemoryAuthCache.ts b/src/charging-station/ocpp/auth/cache/InMemoryAuthCache.ts index a0693cf0..039be166 100644 --- a/src/charging-station/ocpp/auth/cache/InMemoryAuthCache.ts +++ b/src/charging-station/ocpp/auth/cache/InMemoryAuthCache.ts @@ -121,17 +121,19 @@ export class InMemoryAuthCache implements AuthCache { maxEntries?: number rateLimit?: { enabled?: boolean; maxRequests?: number; windowMs?: number } }) { - this.defaultTtl = options?.defaultTtl ?? 3600 // 1 hour default + this.defaultTtl = options?.defaultTtl ?? Constants.DEFAULT_AUTH_CACHE_TTL_SECONDS this.maxAbsoluteLifetimeMs = options?.maxAbsoluteLifetimeMs ?? InMemoryAuthCache.DEFAULT_MAX_ABSOLUTE_LIFETIME_MS this.maxEntries = Math.max(1, options?.maxEntries ?? Constants.DEFAULT_AUTH_CACHE_MAX_ENTRIES) this.rateLimit = { enabled: options?.rateLimit?.enabled ?? false, - maxRequests: options?.rateLimit?.maxRequests ?? 10, // 10 requests per window - windowMs: options?.rateLimit?.windowMs ?? 60000, // 1 minute window + maxRequests: + options?.rateLimit?.maxRequests ?? Constants.DEFAULT_AUTH_CACHE_RATE_LIMIT_MAX_REQUESTS, + windowMs: options?.rateLimit?.windowMs ?? Constants.DEFAULT_AUTH_CACHE_RATE_LIMIT_WINDOW_MS, } - const cleanupSeconds = options?.cleanupIntervalSeconds ?? 300 + const cleanupSeconds = + options?.cleanupIntervalSeconds ?? Constants.DEFAULT_AUTH_CACHE_CLEANUP_INTERVAL_SECONDS if (cleanupSeconds > 0) { const intervalMs = secondsToMilliseconds(cleanupSeconds) this.cleanupInterval = setInterval(() => { diff --git a/src/charging-station/ocpp/auth/factories/AuthComponentFactory.ts b/src/charging-station/ocpp/auth/factories/AuthComponentFactory.ts index fa938a36..81f57e16 100644 --- a/src/charging-station/ocpp/auth/factories/AuthComponentFactory.ts +++ b/src/charging-station/ocpp/auth/factories/AuthComponentFactory.ts @@ -70,7 +70,7 @@ export class AuthComponentFactory { */ static createAuthCache (config: AuthConfiguration): AuthCache { return new InMemoryAuthCache({ - defaultTtl: config.authorizationCacheLifetime ?? 3600, + defaultTtl: config.authorizationCacheLifetime ?? Constants.DEFAULT_AUTH_CACHE_TTL_SECONDS, maxEntries: config.maxCacheEntries ?? Constants.DEFAULT_AUTH_CACHE_MAX_ENTRIES, }) } diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index 306ebcb5..1dbde98f 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -22,8 +22,16 @@ export class Constants { static readonly DEFAULT_ATG_WAIT_TIME = 1000 // Ms + static readonly DEFAULT_AUTH_CACHE_CLEANUP_INTERVAL_SECONDS = 300 + static readonly DEFAULT_AUTH_CACHE_MAX_ENTRIES = 1000 + static readonly DEFAULT_AUTH_CACHE_RATE_LIMIT_MAX_REQUESTS = 10 + + static readonly DEFAULT_AUTH_CACHE_RATE_LIMIT_WINDOW_MS = 60000 + + static readonly DEFAULT_AUTH_CACHE_TTL_SECONDS = 3600 + static readonly DEFAULT_BOOT_NOTIFICATION_INTERVAL = 60000 // Ms static readonly DEFAULT_CIRCULAR_BUFFER_CAPACITY = 386 diff --git a/src/worker/WorkerFactory.ts b/src/worker/WorkerFactory.ts index f48b91ee..4807c88d 100644 --- a/src/worker/WorkerFactory.ts +++ b/src/worker/WorkerFactory.ts @@ -2,12 +2,12 @@ import { isMainThread } from 'node:worker_threads' import type { WorkerAbstract } from './WorkerAbstract.js' -import { mergeDeepRight } from '../utils/index.js' import { DEFAULT_WORKER_OPTIONS } from './WorkerConstants.js' import { WorkerDynamicPool } from './WorkerDynamicPool.js' import { WorkerFixedPool } from './WorkerFixedPool.js' import { WorkerSet } from './WorkerSet.js' import { type WorkerData, type WorkerOptions, WorkerProcessType } from './WorkerTypes.js' +import { mergeDeepRight } from './WorkerUtils.js' // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class WorkerFactory { diff --git a/src/worker/WorkerUtils.ts b/src/worker/WorkerUtils.ts index a5a06908..f10811e1 100644 --- a/src/worker/WorkerUtils.ts +++ b/src/worker/WorkerUtils.ts @@ -3,6 +3,29 @@ import { getRandomValues } from 'node:crypto' import { WorkerProcessType } from './WorkerTypes.js' +const isPlainObject = (value: unknown): value is Record => { + if (typeof value !== 'object' || value === null) return false + return Object.prototype.toString.call(value).slice(8, -1) === 'Object' +} + +export const mergeDeepRight = (target: T, source: object): T => { + const output: Record = { ...(target as Record) } + + if (isPlainObject(target) && isPlainObject(source)) { + Object.keys(source).forEach(key => { + const sourceValue = source[key] + const targetValue = (target as Record)[key] + if (isPlainObject(sourceValue) && isPlainObject(targetValue)) { + output[key] = mergeDeepRight(targetValue, sourceValue) + } else { + output[key] = sourceValue + } + }) + } + + return output as T +} + export const sleep = async (milliSeconds: number): Promise => { return await new Promise(resolve => { const timeout = setTimeout(() => { diff --git a/tests/charging-station/ChargingStation-Transactions.test.ts b/tests/charging-station/ChargingStation-Transactions.test.ts index 66ec8c34..d6d5c798 100644 --- a/tests/charging-station/ChargingStation-Transactions.test.ts +++ b/tests/charging-station/ChargingStation-Transactions.test.ts @@ -10,7 +10,11 @@ import type { ChargingStation } from '../../src/charging-station/index.js' import { OCPP16ServiceUtils } from '../../src/charging-station/ocpp/1.6/OCPP16ServiceUtils.js' import { OCPP20ServiceUtils } from '../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js' import { OCPPVersion } from '../../src/types/index.js' -import { standardCleanup, withMockTimers } from '../helpers/TestLifecycleHelpers.js' +import { + setupConnectorWithTransaction, + standardCleanup, + withMockTimers, +} from '../helpers/TestLifecycleHelpers.js' import { TEST_HEARTBEAT_INTERVAL_MS, TEST_ID_TAG } from './ChargingStationTestConstants.js' import { cleanupChargingStation, createMockChargingStation } from './ChargingStationTestUtils.js' @@ -44,12 +48,7 @@ await describe('ChargingStation Transaction Management', async () => { // Arrange const result = createMockChargingStation({ connectorsCount: 2 }) station = result.station - const connector1 = station.getConnectorStatus(1) - if (connector1 != null) { - connector1.transactionStarted = true - connector1.transactionId = 100 - connector1.transactionIdTag = TEST_ID_TAG - } + setupConnectorWithTransaction(station, 1, { idTag: TEST_ID_TAG, transactionId: 100 }) // Act const connectorId = station.getConnectorIdByTransactionId(100) @@ -98,12 +97,7 @@ await describe('ChargingStation Transaction Management', async () => { }) station = result.station // Get connector in EVSE 1 - const connector1 = station.getConnectorStatus(1) - if (connector1 != null) { - connector1.transactionStarted = true - connector1.transactionId = 200 - connector1.transactionIdTag = TEST_ID_TAG - } + setupConnectorWithTransaction(station, 1, { idTag: TEST_ID_TAG, transactionId: 200 }) // Act const evseId = station.getEvseIdByTransactionId(200) @@ -116,12 +110,7 @@ await describe('ChargingStation Transaction Management', async () => { // Arrange const result = createMockChargingStation({ connectorsCount: 2 }) station = result.station - const connector1 = station.getConnectorStatus(1) - if (connector1 != null) { - connector1.transactionStarted = true - connector1.transactionId = 300 - connector1.transactionIdTag = 'MY-TAG-123' - } + setupConnectorWithTransaction(station, 1, { idTag: 'MY-TAG-123', transactionId: 300 }) // Act const idTag = station.getTransactionIdTag(300) @@ -301,28 +290,21 @@ await describe('ChargingStation Transaction Management', async () => { station = result.station // Set up transactions on connectors 1, 2, and 3 - const connector1 = station.getConnectorStatus(1) - const connector2 = station.getConnectorStatus(2) - const connector3 = station.getConnectorStatus(3) - - if (connector1 != null) { - connector1.transactionStarted = true - connector1.transactionId = 100 - connector1.transactionIdTag = 'TAG-A' - connector1.transactionEnergyActiveImportRegisterValue = 10000 - } - if (connector2 != null) { - connector2.transactionStarted = true - connector2.transactionId = 101 - connector2.transactionIdTag = 'TAG-B' - connector2.transactionEnergyActiveImportRegisterValue = 20000 - } - if (connector3 != null) { - connector3.transactionStarted = true - connector3.transactionId = 102 - connector3.transactionIdTag = 'TAG-C' - connector3.transactionEnergyActiveImportRegisterValue = 30000 - } + setupConnectorWithTransaction(station, 1, { + energyImport: 10000, + idTag: 'TAG-A', + transactionId: 100, + }) + setupConnectorWithTransaction(station, 2, { + energyImport: 20000, + idTag: 'TAG-B', + transactionId: 101, + }) + setupConnectorWithTransaction(station, 3, { + energyImport: 30000, + idTag: 'TAG-C', + transactionId: 102, + }) // Act & Assert - Running transactions count assert.strictEqual(station.getNumberOfRunningTransactions(), 3) @@ -352,21 +334,16 @@ await describe('ChargingStation Transaction Management', async () => { station = result.station // Set up transaction on connector 1 (EVSE 1) and connector 3 (EVSE 2) - const connector1 = station.getConnectorStatus(1) - const connector3 = station.getConnectorStatus(3) - - if (connector1 != null) { - connector1.transactionStarted = true - connector1.transactionId = 500 - connector1.transactionIdTag = 'EVSE1-TAG' - connector1.transactionEnergyActiveImportRegisterValue = 15000 - } - if (connector3 != null) { - connector3.transactionStarted = true - connector3.transactionId = 501 - connector3.transactionIdTag = 'EVSE2-TAG' - connector3.transactionEnergyActiveImportRegisterValue = 18000 - } + setupConnectorWithTransaction(station, 1, { + energyImport: 15000, + idTag: 'EVSE1-TAG', + transactionId: 500, + }) + setupConnectorWithTransaction(station, 3, { + energyImport: 18000, + idTag: 'EVSE2-TAG', + transactionId: 501, + }) // Act & Assert - Running transactions count assert.strictEqual(station.getNumberOfRunningTransactions(), 2) @@ -418,12 +395,7 @@ await describe('ChargingStation Transaction Management', async () => { }) station = result.station - const connector2 = station.getConnectorStatus(2) - if (connector2 != null) { - connector2.transactionStarted = true - connector2.transactionId = 600 - connector2.transactionIdTag = 'EVSE-MODE-TAG' - } + setupConnectorWithTransaction(station, 2, { idTag: 'EVSE-MODE-TAG', transactionId: 600 }) // Act const idTag = station.getTransactionIdTag(600) diff --git a/tests/helpers/OCPPAuthIntegrationTest.ts b/tests/helpers/OCPPAuthIntegrationTest.ts deleted file mode 100644 index 78949faa..00000000 --- a/tests/helpers/OCPPAuthIntegrationTest.ts +++ /dev/null @@ -1,382 +0,0 @@ -import type { ChargingStation } from '../../src/charging-station/index.js' -import type { - AuthConfiguration, - AuthorizationResult, - AuthRequest, - Identifier, -} from '../../src/charging-station/ocpp/auth/index.js' - -import { - AuthContext, - AuthenticationMethod, - AuthorizationStatus, - IdentifierType, - OCPPAuthServiceImpl, -} from '../../src/charging-station/ocpp/auth/index.js' -import { logger } from '../../src/utils/index.js' - -const KNOWN_STRATEGIES = ['local', 'remote', 'certificate'] as const - -export class OCPPAuthIntegrationTest { - private authService: OCPPAuthServiceImpl - private chargingStation: ChargingStation - - constructor (chargingStation: ChargingStation) { - this.chargingStation = chargingStation - this.authService = new OCPPAuthServiceImpl(chargingStation) - } - - public async runTests (): Promise<{ failed: number; passed: number; results: string[] }> { - const results: string[] = [] - let passed = 0 - let failed = 0 - - logger.info( - `${this.chargingStation.logPrefix()} Starting OCPP Authentication Integration Tests` - ) - - try { - this.testServiceInitialization() - results.push('✅ Service Initialization - PASSED') - passed++ - } catch (error) { - results.push(`❌ Service Initialization - FAILED: ${(error as Error).message}`) - failed++ - } - - try { - this.testConfigurationManagement() - results.push('✅ Configuration Management - PASSED') - passed++ - } catch (error) { - results.push(`❌ Configuration Management - FAILED: ${(error as Error).message}`) - failed++ - } - - try { - this.testStrategySelection() - results.push('✅ Strategy Selection Logic - PASSED') - passed++ - } catch (error) { - results.push(`❌ Strategy Selection Logic - FAILED: ${(error as Error).message}`) - failed++ - } - - try { - await this.testOCPP16AuthFlow() - results.push('✅ OCPP 1.6 Authentication Flow - PASSED') - passed++ - } catch (error) { - results.push(`❌ OCPP 1.6 Authentication Flow - FAILED: ${(error as Error).message}`) - failed++ - } - - try { - await this.testOCPP20AuthFlow() - results.push('✅ OCPP 2.0 Authentication Flow - PASSED') - passed++ - } catch (error) { - results.push(`❌ OCPP 2.0 Authentication Flow - FAILED: ${(error as Error).message}`) - failed++ - } - - try { - await this.testErrorHandling() - results.push('✅ Error Handling - PASSED') - passed++ - } catch (error) { - results.push(`❌ Error Handling - FAILED: ${(error as Error).message}`) - failed++ - } - - try { - await this.testCacheOperations() - results.push('✅ Cache Operations - PASSED') - passed++ - } catch (error) { - results.push(`❌ Cache Operations - FAILED: ${(error as Error).message}`) - failed++ - } - - try { - await this.testPerformanceAndStats() - results.push('✅ Performance and Statistics - PASSED') - passed++ - } catch (error) { - results.push(`❌ Performance and Statistics - FAILED: ${(error as Error).message}`) - failed++ - } - - logger.info( - `${this.chargingStation.logPrefix()} Integration Tests Complete: ${String(passed)} passed, ${String(failed)} failed` - ) - - return { failed, passed, results } - } - - private async testCacheOperations (): Promise { - const testIdentifier: Identifier = { - type: IdentifierType.LOCAL, - value: 'CACHE_TEST_ID', - } - - this.authService.invalidateCache(testIdentifier) - this.authService.clearCache() - await this.authService.isLocallyAuthorized(testIdentifier) - - logger.debug(`${this.chargingStation.logPrefix()} Cache operations tested`) - } - - private testConfigurationManagement (): void { - const originalConfiguration = this.authService.getConfiguration() - - const updates: Partial = { - authorizationTimeout: 60, - localAuthListEnabled: false, - maxCacheEntries: 2000, - } - - this.authService.updateConfiguration(updates) - - const updatedConfiguration = this.authService.getConfiguration() - - if (updatedConfiguration.authorizationTimeout !== 60) { - throw new Error('Configuration update failed: authorizationTimeout') - } - - if (updatedConfiguration.localAuthListEnabled) { - throw new Error('Configuration update failed: localAuthListEnabled') - } - - if (updatedConfiguration.maxCacheEntries !== 2000) { - throw new Error('Configuration update failed: maxCacheEntries') - } - - this.authService.updateConfiguration(originalConfiguration) - - logger.debug(`${this.chargingStation.logPrefix()} Configuration management test completed`) - } - - private async testErrorHandling (): Promise { - const invalidIdentifier: Identifier = { - type: IdentifierType.ISO14443, - value: '', - } - - const invalidRequest: AuthRequest = { - allowOffline: false, - connectorId: 999, - context: AuthContext.TRANSACTION_START, - identifier: invalidIdentifier, - timestamp: new Date(), - } - - const result = await this.authService.authenticate(invalidRequest) - - if (result.status === AuthorizationStatus.ACCEPTED) { - throw new Error('Expected INVALID status for invalid identifier, got ACCEPTED') - } - - try { - await this.authService.authorizeWithStrategy('non-existent', invalidRequest) - throw new Error('Expected error for non-existent strategy') - } catch (error) { - if (!(error as Error).message.includes('not found')) { - throw new Error('Unexpected error message for non-existent strategy') - } - } - - logger.debug(`${this.chargingStation.logPrefix()} Error handling verified`) - } - - private async testOCPP16AuthFlow (): Promise { - const identifier: Identifier = { - type: IdentifierType.ISO14443, - value: 'VALID_ID_123', - } - - const request: AuthRequest = { - allowOffline: true, - connectorId: 1, - context: AuthContext.TRANSACTION_START, - identifier, - timestamp: new Date(), - } - - const result = await this.authService.authenticate(request) - this.validateAuthenticationResult(result) - - const authResult = await this.authService.authorize(request) - this.validateAuthenticationResult(authResult) - - const localResult = await this.authService.isLocallyAuthorized(identifier, 1) - if (localResult) { - this.validateAuthenticationResult(localResult) - } - - logger.debug(`${this.chargingStation.logPrefix()} OCPP 1.6 authentication flow tested`) - } - - private async testOCPP20AuthFlow (): Promise { - const identifier: Identifier = { - type: IdentifierType.ISO15693, - value: 'VALID_ID_456', - } - - const request: AuthRequest = { - allowOffline: false, - connectorId: 2, - context: AuthContext.TRANSACTION_START, - identifier, - timestamp: new Date(), - } - - const contexts = [ - AuthContext.TRANSACTION_START, - AuthContext.TRANSACTION_STOP, - AuthContext.REMOTE_START, - AuthContext.REMOTE_STOP, - ] - - for (const context of contexts) { - const contextRequest = { ...request, context } - const result = await this.authService.authenticate(contextRequest) - this.validateAuthenticationResult(result) - } - - logger.debug(`${this.chargingStation.logPrefix()} OCPP 2.0 authentication flow tested`) - } - - private async testPerformanceAndStats (): Promise { - const connectivity = this.authService.testConnectivity() - if (typeof connectivity !== 'boolean') { - throw new Error('Invalid connectivity test result') - } - - const stats = this.authService.getStats() - if (typeof stats.totalRequests !== 'number') { - throw new Error('Invalid statistics object') - } - - const identifier: Identifier = { - type: IdentifierType.ISO14443, - value: 'PERF_TEST_ID', - } - - const startTime = Date.now() - const promises = [] - - for (let i = 0; i < 10; i++) { - const request: AuthRequest = { - allowOffline: true, - connectorId: 1, - context: AuthContext.TRANSACTION_START, - identifier: { ...identifier, value: `PERF_TEST_${String(i)}` }, - timestamp: new Date(), - } - promises.push(this.authService.authenticate(request)) - } - - const results = await Promise.all(promises) - const duration = Date.now() - startTime - - if (results.length !== 10) { - throw new Error('Not all performance test requests completed') - } - - if (duration > 5000) { - throw new Error(`Performance test too slow: ${String(duration)}ms for 10 requests`) - } - - logger.debug( - `${this.chargingStation.logPrefix()} Performance test: ${String(duration)}ms for 10 requests` - ) - } - - private testServiceInitialization (): void { - const availableStrategies = KNOWN_STRATEGIES.filter( - name => this.authService.getStrategy(name) != null - ) - if (availableStrategies.length === 0) { - throw new Error('No authentication strategies available') - } - - const config = this.authService.getConfiguration() - if (typeof config !== 'object') { - throw new Error('Invalid configuration object') - } - - const stats = this.authService.getStats() - if (typeof stats.totalRequests !== 'number') { - throw new Error('Invalid authentication statistics') - } - - logger.debug( - `${this.chargingStation.logPrefix()} Service initialized with ${String(availableStrategies.length)} strategies` - ) - } - - private testStrategySelection (): void { - const availableStrategies = KNOWN_STRATEGIES.filter( - name => this.authService.getStrategy(name) != null - ) - - if (availableStrategies.length === 0) { - throw new Error('No authentication strategies available') - } - - const testIdentifier: Identifier = { - type: IdentifierType.ISO14443, - value: 'TEST123', - } - - const isSupported = this.authService.isSupported(testIdentifier) - if (typeof isSupported !== 'boolean') { - throw new Error('Invalid support detection result') - } - - logger.debug(`${this.chargingStation.logPrefix()} Strategy selection logic verified`) - } - - private validateAuthenticationResult (result: AuthorizationResult): void { - if (typeof result.isOffline !== 'boolean') { - throw new Error('Authentication result missing or invalid isOffline flag') - } - - const validStatuses = Object.values(AuthorizationStatus) - if (!validStatuses.includes(result.status)) { - throw new Error(`Invalid authorization status: ${result.status}`) - } - - const validMethods = Object.values(AuthenticationMethod) - if (!validMethods.includes(result.method)) { - throw new Error(`Invalid authentication method: ${result.method}`) - } - - const now = new Date() - const diff = now.getTime() - result.timestamp.getTime() - if (diff > 60000) { - throw new Error(`Authentication timestamp too old: ${String(diff)}ms`) - } - - if (result.additionalInfo) { - if (typeof result.additionalInfo !== 'object') { - throw new Error('Invalid additionalInfo structure') - } - } - } -} - -/** - * Create and run integration tests for a charging station. - * @param chargingStation - Charging station instance to test - * @returns Test results with pass/fail counts and outcome messages - */ -export async function runOCPPAuthIntegrationTests (chargingStation: ChargingStation): Promise<{ - failed: number - passed: number - results: string[] -}> { - const tester = new OCPPAuthIntegrationTest(chargingStation) - return await tester.runTests() -} -- 2.43.0