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'
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
)
}
)
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
)
}
const workerConfiguration = Configuration.getConfigurationSection<WorkerConfiguration>(
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`
+ )
}
}
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
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)
})
}
})
}
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)
}
Configuration.getConfigurationSection<UIServerConfiguration>(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)
}
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,
this.bootNotificationResponse?.interval != null
? secondsToMilliseconds(this.bootNotificationResponse.interval)
: Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL,
- jitterMs: 1000,
+ jitterMs: Constants.DEFAULT_WS_RECONNECT_TIMEOUT_OFFSET,
retryNumber: registrationRetryCount,
})
)
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(() => {
*/
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,
})
}
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
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 {
import { WorkerProcessType } from './WorkerTypes.js'
+const isPlainObject = (value: unknown): value is Record<string, unknown> => {
+ if (typeof value !== 'object' || value === null) return false
+ return Object.prototype.toString.call(value).slice(8, -1) === 'Object'
+}
+
+export const mergeDeepRight = <T extends object>(target: T, source: object): T => {
+ const output: Record<string, unknown> = { ...(target as Record<string, unknown>) }
+
+ if (isPlainObject(target) && isPlainObject(source)) {
+ Object.keys(source).forEach(key => {
+ const sourceValue = source[key]
+ const targetValue = (target as Record<string, unknown>)[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<NodeJS.Timeout> => {
return await new Promise<NodeJS.Timeout>(resolve => {
const timeout = setTimeout(() => {
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'
// 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)
})
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)
// 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)
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)
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)
})
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)
+++ /dev/null
-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<void> {
- 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<AuthConfiguration> = {
- 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<void> {
- 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<void> {
- 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<void> {
- 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<void> {
- 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()
-}