From: Jérôme Benoit Date: Tue, 31 Mar 2026 21:26:14 +0000 (+0200) Subject: refactor(auth): remove getAuthCache from interfaces, own cache in service X-Git-Tag: ocpp-server@v4.1.0~2 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=84e3bf98278265eee72f7a040bd8126ad95a76af;p=e-mobility-charging-stations-simulator.git refactor(auth): remove getAuthCache from interfaces, own cache in service Remove getAuthCache() from AuthStrategy and OCPPAuthService interfaces. Store authCache directly as private field on OCPPAuthServiceImpl during initializeStrategies(), eliminating indirect access through LocalAuthStrategy for clearCache, invalidateCache, and updateCacheEntry operations. Add getTestAuthCache() helper to MockFactories to encapsulate cache access in tests. Harmonize all test files to use the shared helper instead of ad-hoc cast chains. Fix broken OCPPAuthIntegrationTest references and extract KNOWN_STRATEGIES constant. --- diff --git a/src/charging-station/ocpp/auth/interfaces/OCPPAuthService.ts b/src/charging-station/ocpp/auth/interfaces/OCPPAuthService.ts index 6345ea37..4ce7adfa 100644 --- a/src/charging-station/ocpp/auth/interfaces/OCPPAuthService.ts +++ b/src/charging-station/ocpp/auth/interfaces/OCPPAuthService.ts @@ -152,12 +152,6 @@ export interface AuthStrategy { */ configure?(config: Partial): Promise - /** - * Get the authorization cache if available - * @returns The authorization cache, or undefined if caching is disabled or unavailable - */ - getAuthCache(): AuthCache | undefined - /** * Get strategy-specific statistics */ @@ -398,12 +392,6 @@ export interface OCPPAuthService { */ clearCache(): void - /** - * Get the authorization cache if available - * @returns The authorization cache, or undefined if caching is disabled or unavailable - */ - getAuthCache(): AuthCache | undefined - /** * Get current authentication configuration */ diff --git a/src/charging-station/ocpp/auth/services/OCPPAuthServiceImpl.ts b/src/charging-station/ocpp/auth/services/OCPPAuthServiceImpl.ts index f8f7d3d9..ca6ac217 100644 --- a/src/charging-station/ocpp/auth/services/OCPPAuthServiceImpl.ts +++ b/src/charging-station/ocpp/auth/services/OCPPAuthServiceImpl.ts @@ -33,6 +33,7 @@ const moduleName = 'OCPPAuthServiceImpl' export class OCPPAuthServiceImpl implements OCPPAuthService { private adapter?: OCPPAuthAdapter + private authCache?: AuthCache private readonly chargingStation: ChargingStation private config: AuthConfiguration private readonly metrics: { @@ -273,76 +274,13 @@ export class OCPPAuthServiceImpl implements OCPPAuthService { * Clear all cached authorizations */ public clearCache (): void { - logger.debug( - `${this.chargingStation.logPrefix()} ${moduleName}.clearCache: Clearing all cached authorizations` - ) - - // Clear cache in local strategy - const localStrategy = this.strategies.get('local') - const localAuthCache = localStrategy?.getAuthCache() - if (localAuthCache) { - localAuthCache.clear() - logger.info( - `${this.chargingStation.logPrefix()} ${moduleName}.clearCache: Authorization cache cleared` - ) - } else { - logger.debug( - `${this.chargingStation.logPrefix()} ${moduleName}.clearCache: No authorization cache available to clear` - ) - } - } - - public getAuthCache (): AuthCache | undefined { - if (!this.config.authorizationCacheEnabled) { - return undefined - } - const localStrategy = this.strategies.get('local') - return localStrategy?.getAuthCache() - } - - /** - * Get authentication statistics - * @returns Authentication statistics including version and supported identifier types - */ - public getAuthenticationStats (): { - availableStrategies: string[] - ocppVersion: string - supportedIdentifierTypes: string[] - totalStrategies: number - } { - // Determine supported identifier types by testing each strategy - const supportedTypes = new Set() - - // Test common identifier types - const testIdentifiers: Identifier[] = [ - { type: IdentifierType.ISO14443, value: 'test' }, - { type: IdentifierType.ISO15693, value: 'test' }, - { type: IdentifierType.KEY_CODE, value: 'test' }, - { type: IdentifierType.LOCAL, value: 'test' }, - { type: IdentifierType.MAC_ADDRESS, value: 'test' }, - { type: IdentifierType.NO_AUTHORIZATION, value: 'test' }, - ] - - testIdentifiers.forEach(identifier => { - if (this.isSupported(identifier)) { - supportedTypes.add(identifier.type) - } - }) - - return { - availableStrategies: this.getAvailableStrategies(), - ocppVersion: this.chargingStation.stationInfo?.ocppVersion ?? 'unknown', - supportedIdentifierTypes: Array.from(supportedTypes), - totalStrategies: this.strategies.size, + if (this.authCache == null) { + return } - } - - /** - * Get all available strategies - * @returns Array of registered strategy names - */ - public getAvailableStrategies (): string[] { - return Array.from(this.strategies.keys()) + this.authCache.clear() + logger.info( + `${this.chargingStation.logPrefix()} ${moduleName}.clearCache: Authorization cache cleared` + ) } /** @@ -429,23 +367,13 @@ export class OCPPAuthServiceImpl implements OCPPAuthService { * @param identifier - Identifier whose cached authorization should be invalidated */ public invalidateCache (identifier: Identifier): void { - logger.debug( - `${this.chargingStation.logPrefix()} ${moduleName}.invalidateCache: Invalidating cache for identifier: ${truncateId(identifier.value)}` - ) - - // Invalidate in local strategy - const localStrategy = this.strategies.get('local') - const localAuthCache = localStrategy?.getAuthCache() - if (localAuthCache) { - localAuthCache.remove(identifier.value) - logger.info( - `${this.chargingStation.logPrefix()} ${moduleName}.invalidateCache: Cache invalidated for identifier: ${truncateId(identifier.value)}` - ) - } else { - logger.debug( - `${this.chargingStation.logPrefix()} ${moduleName}.invalidateCache: No local strategy available for cache invalidation` - ) + if (this.authCache == null) { + return } + this.authCache.remove(identifier.value) + logger.info( + `${this.chargingStation.logPrefix()} ${moduleName}.invalidateCache: Cache invalidated for identifier: ${truncateId(identifier.value)}` + ) } /** @@ -550,9 +478,7 @@ export class OCPPAuthServiceImpl implements OCPPAuthService { return } - const localStrategy = this.strategies.get('local') - const authCache = localStrategy?.getAuthCache() - if (authCache == null) { + if (this.authCache == null) { logger.debug( `${this.chargingStation.logPrefix()} ${moduleName}.updateCacheEntry: No auth cache available` ) @@ -582,7 +508,7 @@ export class OCPPAuthServiceImpl implements OCPPAuthService { timestamp: new Date(), } - authCache.set(identifier, result, effectiveTtl) + this.authCache.set(identifier, result, effectiveTtl) logger.debug( `${this.chargingStation.logPrefix()} ${moduleName}.updateCacheEntry: Updated cache for ${truncateId(identifier)} status=${status}${effectiveTtl != null ? `, ttl=${effectiveTtl.toString()}s` : ''}` @@ -688,15 +614,13 @@ export class OCPPAuthServiceImpl implements OCPPAuthService { throw new OCPPError(ErrorType.INTERNAL_ERROR, 'Adapter must be initialized before strategies') } - // Create auth cache for strategy injection - const authCache = AuthComponentFactory.createAuthCache(this.config) + this.authCache = AuthComponentFactory.createAuthCache(this.config) - // Create strategies using factory const strategies = AuthComponentFactory.createStrategies( this.chargingStation, this.adapter, undefined, // manager - delegated to OCPPAuthServiceImpl - authCache, + this.authCache, this.config ) diff --git a/src/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.ts b/src/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.ts index 22798c15..7452a93f 100644 --- a/src/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.ts +++ b/src/charging-station/ocpp/auth/strategies/CertificateAuthStrategy.ts @@ -132,10 +132,6 @@ export class CertificateAuthStrategy implements AuthStrategy { logger.debug(`${moduleName}: Certificate authentication strategy cleaned up`) } - public getAuthCache (): undefined { - return undefined - } - getStats (): JsonObject { return { ...this.stats, diff --git a/src/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.ts b/src/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.ts index 898d78a4..acf185a6 100644 --- a/src/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.ts +++ b/src/charging-station/ocpp/auth/strategies/RemoteAuthStrategy.ts @@ -219,10 +219,6 @@ export class RemoteAuthStrategy implements AuthStrategy { logger.debug(`${moduleName}: Cleared OCPP adapter`) } - public getAuthCache (): undefined { - return undefined - } - /** * Get strategy statistics * @returns Strategy statistics including success rates, response times, and error counts diff --git a/tests/charging-station/ocpp/1.6/OCPP16ServiceUtils.test.ts b/tests/charging-station/ocpp/1.6/OCPP16ServiceUtils.test.ts index ab84ac37..6fcc6822 100644 --- a/tests/charging-station/ocpp/1.6/OCPP16ServiceUtils.test.ts +++ b/tests/charging-station/ocpp/1.6/OCPP16ServiceUtils.test.ts @@ -44,6 +44,7 @@ import { } from '../../../../src/types/index.js' import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js' import { createMockChargingStation } from '../../ChargingStationTestUtils.js' +import { getTestAuthCache } from '../auth/helpers/MockFactories.js' import { createCommandsSupport, createMeterValuesTemplate } from './OCPP16TestUtils.js' await describe('OCPP16ServiceUtils — pure functions', async () => { @@ -879,8 +880,7 @@ await describe('OCPP16ServiceUtils — pure functions', async () => { // Assert const authService = OCPPAuthServiceFactory.getInstance(station) - const authCache = authService.getAuthCache() - assert.ok(authCache != null) + const authCache = getTestAuthCache(authService) const cached = authCache.get(TEST_ID_TAG) assert.ok(cached != null) assert.strictEqual(cached.status, AuthorizationStatus.ACCEPTED) @@ -904,8 +904,7 @@ await describe('OCPP16ServiceUtils — pure functions', async () => { // Assert const authService = OCPPAuthServiceFactory.getInstance(station) - const authCache = authService.getAuthCache() - assert.ok(authCache != null) + const authCache = getTestAuthCache(authService) const cached = authCache.get(TEST_ID_TAG) assert.ok(cached != null) assert.strictEqual(cached.status, AuthorizationStatus.BLOCKED) @@ -931,8 +930,7 @@ await describe('OCPP16ServiceUtils — pure functions', async () => { // Assert const authService = OCPPAuthServiceFactory.getInstance(station) - const authCache = authService.getAuthCache() - assert.ok(authCache != null) + const authCache = getTestAuthCache(authService) const cached = authCache.get(TEST_ID_TAG) assert.ok(cached != null, 'Cache entry should exist with future TTL') assert.strictEqual(cached.status, AuthorizationStatus.ACCEPTED) @@ -958,8 +956,7 @@ await describe('OCPP16ServiceUtils — pure functions', async () => { // Assert const authService = OCPPAuthServiceFactory.getInstance(station) - const authCache = authService.getAuthCache() - assert.ok(authCache != null) + const authCache = getTestAuthCache(authService) const cached = authCache.get(TEST_ID_TAG) assert.strictEqual(cached, undefined, 'Expired entry must not be cached') }) @@ -982,8 +979,7 @@ await describe('OCPP16ServiceUtils — pure functions', async () => { // Assert const authService = OCPPAuthServiceFactory.getInstance(station) - const authCache = authService.getAuthCache() - assert.ok(authCache != null) + const authCache = getTestAuthCache(authService) const cached = authCache.get(TEST_ID_TAG) assert.ok(cached != null, 'Cache entry should exist without TTL') assert.strictEqual(cached.status, AuthorizationStatus.ACCEPTED) diff --git a/tests/charging-station/ocpp/2.0/OCPP20ResponseService-CacheUpdate.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ResponseService-CacheUpdate.test.ts index 1cb6696b..704ef1ae 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ResponseService-CacheUpdate.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ResponseService-CacheUpdate.test.ts @@ -8,7 +8,6 @@ import assert from 'node:assert/strict' import { afterEach, beforeEach, describe, it } from 'node:test' import type { ChargingStation } from '../../../../src/charging-station/index.js' -import type { LocalAuthStrategy } from '../../../../src/charging-station/ocpp/auth/index.js' import type { AuthCache } from '../../../../src/charging-station/ocpp/auth/interfaces/OCPPAuthService.js' import { @@ -20,6 +19,7 @@ import { import { OCPPVersion } from '../../../../src/types/index.js' import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js' import { createMockChargingStation } from '../../ChargingStationTestUtils.js' +import { getTestAuthCache } from '../auth/helpers/MockFactories.js' const TEST_IDENTIFIER = 'TEST_RFID_TOKEN_001' const TEST_STATION_ID = 'CS_CACHE_UPDATE_TEST' @@ -43,10 +43,7 @@ await describe('C10 - TransactionEventResponse Cache Update', async () => { authService = new OCPPAuthServiceImpl(station) authService.initialize() - const localStrategy = authService.getStrategy('local') as LocalAuthStrategy | undefined - const cache = localStrategy?.getAuthCache() - assert.ok(cache != null, 'Auth cache must be available after initialization') - authCache = cache + authCache = getTestAuthCache(authService) }) afterEach(() => { @@ -195,9 +192,8 @@ await describe('C10 - TransactionEventResponse Cache Update', async () => { ) // Assert - const localStrategy = disabledService.getStrategy('local') as LocalAuthStrategy | undefined - const cache = localStrategy?.getAuthCache() - const cached = cache?.get(TEST_IDENTIFIER) + const disabledCache = getTestAuthCache(disabledService) + const cached = disabledCache.get(TEST_IDENTIFIER) assert.strictEqual(cached, undefined, 'Cache entry should not exist when cache is disabled') }) }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-AuthCache.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-AuthCache.test.ts index 9c441c21..8005077c 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-AuthCache.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-AuthCache.test.ts @@ -8,7 +8,6 @@ import assert from 'node:assert/strict' import { afterEach, beforeEach, describe, it } from 'node:test' import type { ChargingStation } from '../../../../src/charging-station/index.js' -import type { LocalAuthStrategy } from '../../../../src/charging-station/ocpp/auth/index.js' import type { AuthCache } from '../../../../src/charging-station/ocpp/auth/interfaces/OCPPAuthService.js' import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js' @@ -25,6 +24,7 @@ import { } from '../../../../src/types/index.js' import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js' import { createMockChargingStation } from '../../ChargingStationTestUtils.js' +import { getTestAuthCache } from '../auth/helpers/MockFactories.js' const TEST_STATION_ID = 'CS_AUTH_CACHE_UTILS_TEST' const TEST_TOKEN_VALUE = 'RFID_AUTH_CACHE_001' @@ -49,10 +49,7 @@ await describe('OCPP20ServiceUtils.updateAuthorizationCache', async () => { authService.initialize() OCPPAuthServiceFactory.setInstanceForTesting(TEST_STATION_ID, authService) - const localStrategy = authService.getStrategy('local') as LocalAuthStrategy | undefined - const cache = localStrategy?.getAuthCache() - assert.ok(cache != null, 'Auth cache must be available after initialization') - authCache = cache + authCache = getTestAuthCache(authService) }) afterEach(() => { diff --git a/tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts b/tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts index 39ea0e71..744cd978 100644 --- a/tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts +++ b/tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts @@ -26,6 +26,7 @@ import { createMockIdentifier, createMockLocalAuthListManager, createTestAuthConfig, + getTestAuthCache, } from './helpers/MockFactories.js' await describe('OCPP Authentication', async () => { @@ -233,10 +234,7 @@ await describe('OCPP Authentication', async () => { const service = new OCPPAuthServiceImpl(result16.station) service.initialize() - const localStrategy = service.getStrategy('local') as LocalAuthStrategy | undefined - assert.notStrictEqual(localStrategy, undefined) - - const authCache = localStrategy?.getAuthCache() + const authCache = getTestAuthCache(service) assert.notStrictEqual(authCache, undefined) }) diff --git a/tests/charging-station/ocpp/auth/helpers/MockFactories.ts b/tests/charging-station/ocpp/auth/helpers/MockFactories.ts index 7cb48975..b2186cbe 100644 --- a/tests/charging-station/ocpp/auth/helpers/MockFactories.ts +++ b/tests/charging-station/ocpp/auth/helpers/MockFactories.ts @@ -12,6 +12,8 @@ import type { OCPPAuthAdapter, OCPPAuthService, } from '../../../../../src/charging-station/ocpp/auth/interfaces/OCPPAuthService.js' +import type { OCPPAuthServiceImpl } from '../../../../../src/charging-station/ocpp/auth/services/OCPPAuthServiceImpl.js' +import type { LocalAuthStrategy } from '../../../../../src/charging-station/ocpp/auth/strategies/LocalAuthStrategy.js' import { type AuthConfiguration, @@ -350,3 +352,12 @@ export const createMockLocalAuthListManager = ( }), ...overrides, }) + +export const getTestAuthCache = (authService: OCPPAuthService): AuthCache => { + const localStrategy = (authService as OCPPAuthServiceImpl).getStrategy('local') as + | LocalAuthStrategy + | undefined + const cache = localStrategy?.getAuthCache() + assert.ok(cache != null, 'Auth cache must be available for test') + return cache +} diff --git a/tests/charging-station/ocpp/auth/services/OCPPAuthServiceImpl.test.ts b/tests/charging-station/ocpp/auth/services/OCPPAuthServiceImpl.test.ts index 655b146e..82de9891 100644 --- a/tests/charging-station/ocpp/auth/services/OCPPAuthServiceImpl.test.ts +++ b/tests/charging-station/ocpp/auth/services/OCPPAuthServiceImpl.test.ts @@ -9,7 +9,6 @@ import type { ChargingStation } from '../../../../../src/charging-station/index. import type { OCPPAuthService } from '../../../../../src/charging-station/ocpp/auth/interfaces/OCPPAuthService.js' import { OCPPAuthServiceImpl } from '../../../../../src/charging-station/ocpp/auth/services/OCPPAuthServiceImpl.js' -import { LocalAuthStrategy } from '../../../../../src/charging-station/ocpp/auth/strategies/LocalAuthStrategy.js' import { AuthContext, AuthenticationMethod, @@ -19,7 +18,7 @@ import { } from '../../../../../src/charging-station/ocpp/auth/types/AuthTypes.js' import { OCPPVersion } from '../../../../../src/types/index.js' import { standardCleanup } from '../../../../helpers/TestLifecycleHelpers.js' -import { createMockAuthServiceTestStation } from '../helpers/MockFactories.js' +import { createMockAuthServiceTestStation, getTestAuthCache } from '../helpers/MockFactories.js' await describe('OCPPAuthServiceImpl', async () => { afterEach(() => { @@ -420,12 +419,8 @@ await describe('OCPPAuthServiceImpl', async () => { const authService = new OCPPAuthServiceImpl(mockStation) authService.initialize() - const localStrategy = authService.getStrategy('local') - assert.notStrictEqual(localStrategy, undefined) - assert.ok(localStrategy instanceof LocalAuthStrategy) - - const local = localStrategy - assert.notStrictEqual(local.getAuthCache(), undefined) + const authCache = getTestAuthCache(authService) + assert.notStrictEqual(authCache, undefined) }) }) }) diff --git a/tests/helpers/OCPPAuthIntegrationTest.ts b/tests/helpers/OCPPAuthIntegrationTest.ts index ef0e5f2d..78949faa 100644 --- a/tests/helpers/OCPPAuthIntegrationTest.ts +++ b/tests/helpers/OCPPAuthIntegrationTest.ts @@ -15,6 +15,8 @@ import { } 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 @@ -256,11 +258,6 @@ export class OCPPAuthIntegrationTest { throw new Error('Invalid statistics object') } - const authStatistics = this.authService.getAuthenticationStats() - if (!Array.isArray(authStatistics.availableStrategies)) { - throw new Error('Invalid authentication statistics') - } - const identifier: Identifier = { type: IdentifierType.ISO14443, value: 'PERF_TEST_ID', @@ -297,8 +294,10 @@ export class OCPPAuthIntegrationTest { } private testServiceInitialization (): void { - const strategies = this.authService.getAvailableStrategies() - if (strategies.length === 0) { + const availableStrategies = KNOWN_STRATEGIES.filter( + name => this.authService.getStrategy(name) != null + ) + if (availableStrategies.length === 0) { throw new Error('No authentication strategies available') } @@ -307,24 +306,23 @@ export class OCPPAuthIntegrationTest { throw new Error('Invalid configuration object') } - const stats = this.authService.getAuthenticationStats() - if (!stats.ocppVersion) { + 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(strategies.length)} strategies` + `${this.chargingStation.logPrefix()} Service initialized with ${String(availableStrategies.length)} strategies` ) } private testStrategySelection (): void { - const strategies = this.authService.getAvailableStrategies() + const availableStrategies = KNOWN_STRATEGIES.filter( + name => this.authService.getStrategy(name) != null + ) - for (const strategyName of strategies) { - const strategy = this.authService.getStrategy(strategyName) - if (!strategy) { - throw new Error(`Strategy '${strategyName}' not found`) - } + if (availableStrategies.length === 0) { + throw new Error('No authentication strategies available') } const testIdentifier: Identifier = { diff --git a/tests/ocpp2-e2e-test-plan.md b/tests/ocpp2-e2e-test-plan.md new file mode 100644 index 00000000..220285cc --- /dev/null +++ b/tests/ocpp2-e2e-test-plan.md @@ -0,0 +1,414 @@ +# OCPP 2.0.1 End-to-End Test Plan + +E2E test scenarios for the charging station simulator's OCPP 2.0.1 stack. +Executed via MCP tools against the mock OCPP server (`tests/ocpp-server/`). + +## Conventions + +| Item | Value | +| ---------------- | --------------------------------------------------------------- | +| Mock server | `cd tests/ocpp-server && poetry run python server.py [OPTIONS]` | +| Station template | `keba-ocpp2.station-template.json` | +| Station ID | `CS-KEBA-OCPP2-00001` | +| EVSE / Connector | 1 / 1 | +| Supervision URL | `ws://localhost:9000` | + +### Execution Rules + +- **Tester manages the mock server only** — start/stop/restart with options. +- All `--boot-status` and enum CLI values are **Title-Case** (`Accepted`, not `accepted`). + +### Reconnection + +The station auto-reconnects when the server restarts, with a **fixed 30s delay** (`reconnectExponentialDelay: false`, `ConnectionTimeOut: 30`). This means: + +- After a server restart, the station takes ~30s to reconnect (WebSocket close → sleep 30s → reopen). +- The station does NOT re-send `BootNotification` if it already has `bootNotificationResponse.status = Accepted` in cache. It connects silently. +- To force a fresh boot (e.g., to clear cached Inoperative state), use `stopChargingStation`/`startChargingStation` as a **setup step**, not as a test step. + +**To avoid the 30s reconnect delay between server restarts**, use this pattern: + +``` +1. Kill mock server +2. Start new mock server with new options +3. MCP: closeConnection (triggers CLOSE_NORMAL → resets retry count to 0) +4. MCP: openConnection (immediate reconnect, no 30s wait) +5. Wait ~5s for WebSocket handshake +6. Proceed with test +``` + +### Verification + +A test case **passes** when ALL of: + +1. MCP tool response: `"status": "success"` (no `responsesFailed`) +2. `readCombinedLog`: expected OCPP messages in correct order +3. `listChargingStations`: expected station/connector state +4. `readErrorLog`: no unexpected errors + +### Server Lifecycle + +Tests are grouped by server configuration to minimize restarts. +Within a group, tests execute sequentially without restart. +Between groups, use the close/open pattern above to avoid cumulative reconnect delays. + +--- + +## A — Security + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | --------------------------- | ------------------------------- | ----------------------------------------------------------- | --------------------------- | +| A03 | CS-initiated cert update | MCP `signCertificate` | Send CSR with `certificateType: ChargingStationCertificate` | Response `status: Accepted` | +| A04 | Security event notification | MCP `securityEventNotification` | Send `type: FirmwareUpdated` | Response empty (success) | + +### Server: `--boot-status Accepted --command CertificateSigned --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| --- | -------------------------- | -------------- | --------- | --------------------------------------------------------------------------------------------------------------------- | +| A02 | CSMS-initiated cert update | Server command | Wait ~15s | CertificateSigned received → Rejected (statusInfo.reasonCode: InternalError — no cert manager in keba-ocpp2 template) | + +--- + +## B — Provisioning + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | -------------------- | --------------------- | ---------------------------------- | ----------------------------------------------------------------------------- | +| B01 | Cold Boot — Accepted | Auto (server restart) | Restart server, wait for reconnect | BootNotification → Accepted, StatusNotification(Available), Heartbeat started | +| B04 | Offline reconnection | Server kill/restart | Kill server, wait 10s, restart | Station reconnects, re-sends BootNotification, returns to Available | + +### Server: `--boot-status-sequence Pending,Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | ---------------------------- | --------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------- | +| B02 | Cold Boot — Pending→Accepted | Auto (server restart) | Restart server, wait for 2 boot cycles | 1st BootNotification → Pending, station retries, 2nd → Accepted, StatusNotification(Available) | + +### Server: `--boot-status Rejected` + +| TC | Use Case | Via | Steps | Expected | +| --- | -------------------- | --------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| B03 | Cold Boot — Rejected | Auto (server restart) | Restart server, wait for reconnect | BootNotification → Rejected → 1 retry (registrationMaxRetries defaults to 0) → Rejected → "Registration failure" log. No StatusNotification sent. Station stays in Rejected state but can still retry BootNotification on next reconnection (B03.FR.06). | + +### Server: various `--command X --delay 5` + +| TC | Use Case | Server flags | Expected | +| ---- | ---------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| B05 | SetVariables | `--command SetVariables --set-variables "OCPPCommCtrlr.HeartbeatInterval=30"` | SetVariablesResponse with result status | +| B06 | GetVariables | `--command GetVariables --get-variables "ChargingStation.AvailabilityState"` | GetVariablesResponse with variable value | +| B07 | GetBaseReport | `--command GetBaseReport` | GetBaseReportResponse Accepted + NotifyReport sequence | +| B09 | SetNetworkProfile | `--command SetNetworkProfile` | Response Rejected (NoSecurityDowngrade per B09.FR.01) | +| B11 | Reset (no transaction) | `--command Reset` | Reset Accepted → StatusNotification(Unavailable) → close → re-boot → Available | +| B11b | Reset OnIdle (no active txn) | `--command Reset --reset-type OnIdle` | Reset Accepted → StatusNotification(Unavailable) → re-boot → Available (no transaction active, immediate reset) | + +--- + +## C — Authorization + +### Server: `--boot-status Accepted` (normal auth) + +| TC | Use Case | Via | Steps | Expected | +| --- | ------------------ | --------------- | --------------------------------------------------- | ------------------------------ | +| C01 | Authorize — normal | MCP `authorize` | `idToken: {idToken: "any_token", type: "ISO14443"}` | `idTokenInfo.status: Accepted` | + +### Server: `--boot-status Accepted --auth-mode whitelist --whitelist valid_token test_token` + +| TC | Use Case | Via | Steps | Expected | +| ------------- | --------------------------- | --------------- | ------------------------ | ------------------ | +| C01-WL-OK | Authorize — whitelisted | MCP `authorize` | `idToken: test_token` | `status: Accepted` | +| C01-WL-REJECT | Authorize — not whitelisted | MCP `authorize` | `idToken: unknown_token` | `status: Blocked` | + +### Server: `--boot-status Accepted --auth-mode blacklist --blacklist blocked_token` + +| TC | Use Case | Via | Steps | Expected | +| ------------- | --------------------------- | --------------- | ------------------------ | ------------------ | +| C01-BL-OK | Authorize — not blacklisted | MCP `authorize` | `idToken: good_token` | `status: Accepted` | +| C01-BL-REJECT | Authorize — blacklisted | MCP `authorize` | `idToken: blocked_token` | `status: Blocked` | + +### Server: `--boot-status Accepted --auth-mode rate_limit` + +| TC | Use Case | Via | Steps | Expected | +| ------ | ------------------------ | --------------- | --------- | ----------------------- | +| C01-RL | Authorize — rate limited | MCP `authorize` | Any token | `status: NotAtThisTime` | + +### Server: `--boot-status Accepted --offline` + +| TC | Use Case | Via | Steps | Expected | +| ----------- | --------------------------- | --------------- | --------- | ------------------------- | +| C01-OFFLINE | Authorize — network failure | MCP `authorize` | Any token | InternalError from server | + +### Server: `--boot-status Accepted --auth-group-id MyGroup --auth-cache-expiry 3600` + +| TC | Use Case | Via | Steps | Expected | +| --- | ------------------- | --------------- | --------- | -------------------------------------------------------------------------- | +| C09 | GroupId in response | MCP `authorize` | Any token | `idTokenInfo.groupIdToken.idToken: MyGroup`, `cacheExpiryDateTime` present | + +### Server: `--boot-status Accepted --command ClearCache --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| --- | ---------------- | -------------- | --------- | --------------------- | +| C11 | Clear auth cache | Server command | Wait ~15s | ClearCache → Accepted | + +--- + +## E — Transactions + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| ---------- | --------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| E01-ATG | Transaction lifecycle (ATG) | MCP `startAutomaticTransactionGenerator` → wait 30s → `stopAutomaticTransactionGenerator` | Wait for full cycle | Authorize → TransactionEvent.Started(seqNo=0) → Updated(seqNo=1+, MeterValues) → Ended(seqNo=N, stoppedReason=Local) | +| E01-DIRECT | TransactionEvent direct | MCP `transactionEvent` | Send Started → Updated → Ended with `transactionId: "mcp-test-001"` | All 3 accepted, seqNo sequential | + +### Server: `--boot-status Accepted --commands "RequestStartTransaction:15,RequestStopTransaction:45"` + +| TC | Use Case | Via | Steps | Expected | +| ------- | -------------------------- | --------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| F01+F03 | Remote Start → Remote Stop | Server commands | Wait ~60s for full cycle | RequestStartTransaction → Accepted → TransactionEvent.Started(RemoteStart) → MeterValues → RequestStopTransaction (real txn ID via tracking) → TransactionEvent.Ended(Remote) | + +### Server: `--boot-status Accepted --command RequestStopTransaction --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| ---- | --------------------------- | -------------- | --------- | ---------------------------------------------------------------------------------------------------------------------- | +| F03b | Remote Stop — no active txn | Server command | Wait ~15s | RequestStopTransaction with fallback ID `test_transaction_123` → Rejected (invalid transaction ID format — not a UUID) | + +### Server: `--boot-status Accepted --total-cost 25.50` + +| TC | Use Case | Via | Steps | Expected | +| --- | ------------------ | --------------------------------------------------- | ---------------------------------------- | ------------------------------------ | +| I02 | Running total cost | MCP `startAutomaticTransactionGenerator` → wait 30s | Check TransactionEvent.Updated responses | `totalCost: 25.5` in server response | + +--- + +## F — Remote Control + +### Server: various `--command X --delay 5` + +| TC | Use Case | Server flags | Expected | +| --- | ------------------------------------ | -------------------------------- | --------------------------------------------------------------- | +| F05 | Unlock connector | `--command UnlockConnector` | UnlockConnector → Unlocked | +| E14 | GetTransactionStatus (no active txn) | `--command GetTransactionStatus` | GetTransactionStatus → messagesInQueue: false, uses fallback ID | + +### Server: `--boot-status Accepted --commands "RequestStartTransaction:15,GetTransactionStatus:25"` + +| TC | Use Case | Via | Steps | Expected | +| ---------- | ------------------------------------ | --------------- | --------- | ------------------------------------------------------------------------------------------------ | +| E14-ACTIVE | GetTransactionStatus with active txn | Server commands | Wait ~35s | GetTransactionStatus → ongoingIndicator: true, messagesInQueue: false (real txn ID via tracking) | + +### Server: `--boot-status Accepted --commands "RequestStartTransaction:15,UnlockConnector:25"` + +| TC | Use Case | Via | Steps | Expected | +| ------- | --------------------------------- | --------------- | --------- | ---------------------------------------------- | +| F05-TXN | UnlockConnector during active txn | Server commands | Wait ~35s | UnlockConnector → OngoingAuthorizedTransaction | + +### Server: various `--command X --delay 5` (continued) + +| F06-SN | TriggerMessage (StatusNotification) | `--command TriggerMessage` | TriggerMessage → Accepted → StatusNotification sent | +| F06-BN | TriggerMessage (BootNotification) | `--command TriggerMessage --trigger-message BootNotification` | TriggerMessage → Rejected(NotEnabled, F06.FR.17 — already accepted) | +| F06-HB | TriggerMessage (Heartbeat) | `--command TriggerMessage --trigger-message Heartbeat` | TriggerMessage → Accepted → Heartbeat sent | +| F06-MV | TriggerMessage (MeterValues) | `--command TriggerMessage --trigger-message MeterValues` | TriggerMessage → Accepted → MeterValues sent | +| F06-FW | TriggerMessage (FirmwareStatus) | `--command TriggerMessage --trigger-message FirmwareStatusNotification` | TriggerMessage → Accepted → FirmwareStatusNotification(Idle) sent | +| F06-LS | TriggerMessage (LogStatus) | `--command TriggerMessage --trigger-message LogStatusNotification` | TriggerMessage → Accepted → LogStatusNotification(Idle) sent | + +--- + +## G — Availability + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | ------------------ | ------------------------ | ------------------------------------------------------------------------- | ------------------------------ | +| G01 | StatusNotification | MCP `statusNotification` | Send for each status: Available, Occupied, Faulted, Unavailable, Reserved | All succeed (empty response) | +| G02 | Heartbeat | MCP `heartbeat` | Send 5x rapid | All succeed with `currentTime` | + +### Server: various `--command ChangeAvailability --delay 5` + +| TC | Use Case | Server flags | Expected | +| --- | ------------------------------ | ---------------------------------------------------------------- | ------------------------------------------ | +| G03 | ChangeAvailability Operative | `--command ChangeAvailability` | Accepted + StatusNotification(Available) | +| G04 | ChangeAvailability Inoperative | `--command ChangeAvailability --availability-status Inoperative` | Accepted + StatusNotification(Unavailable) | + +--- + +## J — MeterValues + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | --------------------------- | ------------------------------------------------------ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| J01 | Non-transaction MeterValues | MCP `meterValues` | Send Voltage=230V on evseId=1 | Response empty (success) | +| J02 | Transaction MeterValues | MCP `startAutomaticTransactionGenerator` → wait 60-90s | Check logs | TransactionEvent.Updated contains Voltage, Energy, Power, Current with context `Sample.Periodic`. Note: ATG start delay (15-30s) + MeterValueSampleInterval (30s) = first MeterValues ~45-60s after ATG start. | + +--- + +## L — Firmware Management + +### Server: `--boot-status Accepted --command UpdateFirmware --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| --- | ---------------------- | -------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| L01 | Secure Firmware Update | Server command | Wait ~40s | UpdateFirmware → Accepted → FirmwareStatusNotification: Downloading → Downloaded → Installing → Installed → SecurityEventNotification(FirmwareUpdated) | + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| ------- | -------------------------- | -------------------------------- | -------------------------------------- | ------------------------ | +| L01-MCP | FirmwareStatusNotification | MCP `firmwareStatusNotification` | Send `status: Installed, requestId: 1` | Response empty (success) | + +--- + +## M — ISO 15118 Certificate Management + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | ------------------------ | --------------------------- | ------------------- | --------------------------- | +| M01 | Get 15118 EV Certificate | MCP `get15118EVCertificate` | Send Install action | Response `status: Accepted` | +| M06 | Get Certificate Status | MCP `getCertificateStatus` | Send OCSP data | Response `status: Accepted` | + +### Server: various `--command X --delay 5` + +| TC | Use Case | Server flags | Expected | +| --- | ---------------------- | -------------------------------------- | --------------------------------------- | +| M03 | Get installed cert IDs | `--command GetInstalledCertificateIds` | Response NotFound (cert manager absent) | +| M04 | Delete certificate | `--command DeleteCertificate` | Response Failed (cert manager absent) | +| M05 | Install certificate | `--command InstallCertificate` | Response Failed (cert manager absent) | + +--- + +## N — Diagnostics + +### Server: `--boot-status Accepted --command GetLog --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| --- | ------------ | -------------- | --------- | -------------------------------------------------------------------- | +| N01 | Retrieve Log | Server command | Wait ~15s | GetLog(DiagnosticsLog) → Accepted → LogStatusNotification(Uploading) | + +### Server: `--boot-status Accepted --command CustomerInformation --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| --- | -------------------- | -------------- | --------- | ------------------------------------------------------------------------------------------------------------- | +| N09 | Customer Information | Server command | Wait ~15s | CustomerInformation(report=true, customerIdentifier=test_customer_001) → Accepted → NotifyCustomerInformation | + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| -------------- | ------------------------- | ------------------------------- | ---------------------------- | ------------------------ | +| N01-MCP | LogStatusNotification | MCP `logStatusNotification` | Send `status: Uploaded` | Response empty (success) | +| N09-MCP | NotifyCustomerInformation | MCP `notifyCustomerInformation` | Send data with requestId | Response empty (success) | +| N-NOTIF-REPORT | NotifyReport | MCP `notifyReport` | Send with requestId, seqNo=0 | Response empty (success) | + +--- + +## P — DataTransfer + +### Server: `--boot-status Accepted` + +| TC | Use Case | Via | Steps | Expected | +| --- | -------------------- | ------------------ | ----------------------------------------------- | --------------------------- | +| P02 | DataTransfer CS→CSMS | MCP `dataTransfer` | Send `vendorId: TestVendor, messageId: TestMsg` | Response `status: Accepted` | + +### Server: `--boot-status Accepted --command DataTransfer --delay 5` + +| TC | Use Case | Via | Steps | Expected | +| --- | -------------------- | -------------- | --------- | ---------------------------------------------------------------------- | +| P01 | DataTransfer CSMS→CS | Server command | Wait ~15s | DataTransfer received → Response `UnknownVendorId` (no custom handler) | + +--- + +## B12 — Reset With Active Transaction + +### Server: `--boot-status Accepted --commands "RequestStartTransaction:15,Reset:30"` + +| TC | Use Case | Via | Steps | Expected | +| --- | ---------------------- | -------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| B12 | Reset with ongoing txn | Server commands (single session) | Wait ~60s for full cycle | RequestStartTransaction → Accepted → TransactionEvent.Started → Reset(Immediate) → station stops transaction → StatusNotification(Unavailable) → re-boot → Available | + +--- + +## Offline / Reconnection + +### Server: `--boot-status Accepted` (kill/restart cycle) + +| TC | Use Case | Steps | Expected | +| -------- | -------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| B04-FULL | Server down and reconnect | Kill server → wait 10s → restart | Station enters reconnection loop, reconnects, re-boots, returns to Available | +| B04-TXN | Offline during transaction | Start ATG → kill server → wait 15s → restart → stop ATG | Transaction stopped on server kill (stopTransactionsOnStopped=true). After reconnect: BootNotification → Accepted → StatusNotification(Available). No queued TransactionEvents. | + +--- + +## Edge Cases / Negative Tests + +### Server: `--boot-status Accepted` + +| TC | Description | Via | Expected | +| ------ | ----------------------------------- | ------------------------------------------------------ | ------------------------------------------- | +| ERR-03 | Multi-measurand MeterValues | MCP `meterValues` with Voltage+Power+Current+Energy | All accepted | +| ERR-04 | FirmwareStatus all statuses | MCP `firmwareStatusNotification` × 14 statuses | All succeed | +| ERR-05 | Orphaned LogStatusNotification | MCP `logStatusNotification` with `requestId: 999` | Succeeds (no prior GetLog required) | +| ERR-06 | Orphaned FirmwareStatusNotification | MCP `firmwareStatusNotification` with `requestId: 999` | Succeeds (no prior UpdateFirmware required) | + +### Server: `--boot-status Accepted --commands "RequestStartTransaction:15,RequestStartTransaction:25"` + +| TC | Description | Expected | +| -------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------ | +| E-DOUBLE-START | Second RequestStartTransaction while first txn active | First → Accepted + TransactionEvent.Started. Second → Rejected (connector occupied). | + +### Server: `--boot-status-sequence Pending,Accepted --command Reset --delay 8` + +| TC | Description | Expected | +| ----------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| B11-PENDING | Reset while in Pending state | Station receives Reset → Accepted (Reset is not blocked by registration state). StatusNotification(Unavailable) → reconnect → re-boot. | + +--- + +## Execution Order + +Tests grouped by server configuration to minimize restarts. +Use `closeConnection`/`openConnection` between groups 8-18 to avoid cumulative reconnect delays. + +| # | Server Config | Test Cases | +| --- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | `--boot-status Accepted` | B01, B04, A03, A04, C01, G01, G02, J01, J02, P02, E01-ATG, E01-DIRECT, L01-MCP, M01, M06, N01-MCP, N09-MCP, N-NOTIF-REPORT, ERR-03→06 | +| 2 | `--boot-status Accepted --auth-mode whitelist --whitelist valid_token test_token` | C01-WL-OK, C01-WL-REJECT | +| 3 | `--boot-status Accepted --auth-mode blacklist --blacklist blocked_token` | C01-BL-OK, C01-BL-REJECT | +| 4 | `--boot-status Accepted --auth-mode rate_limit` | C01-RL | +| 5 | `--boot-status Accepted --offline` | C01-OFFLINE | +| 6 | `--boot-status Accepted --auth-group-id MyGroup --auth-cache-expiry 3600` | C09 | +| 7 | `--boot-status Accepted --total-cost 25.50` | I02 | +| 8 | `--boot-status Accepted --command X --delay 5` (sequential restarts) | A02, B05, B06, B07, B09, B11, B11b, C11, E14, F05, F06-SN, F06-BN, F06-HB, F06-MV, F06-FW, F06-LS, G03, G04, L01, M03, M04, M05, N01, N09, P01 | +| 9 | `--boot-status Accepted --commands "RequestStartTransaction:15,RequestStopTransaction:45"` | F01+F03 | +| 10 | `--boot-status Accepted --command RequestStopTransaction --delay 5` | F03b | +| 11 | `--boot-status Accepted --commands "RequestStartTransaction:15,Reset:30"` | B12 | +| 12 | `--boot-status Accepted` (kill/restart cycle) | B04-FULL, B04-TXN | +| 13 | `--boot-status Accepted --commands "RequestStartTransaction:15,RequestStartTransaction:25"` | E-DOUBLE-START | +| 14 | `--boot-status Accepted --commands "RequestStartTransaction:15,GetTransactionStatus:25"` | E14-ACTIVE | +| 15 | `--boot-status Accepted --commands "RequestStartTransaction:15,UnlockConnector:25"` | F05-TXN | +| 16 | `--boot-status-sequence Pending,Accepted` | B02 | +| 17 | `--boot-status-sequence Pending,Accepted --command Reset --delay 8` | B11-PENDING | +| 18 | `--boot-status Rejected` | B03 | + +## Coverage + +| Block | Implemented Use Cases Covered | Test Count | +| ----------------- | ------------------------------- | ---------- | +| A. Security | A02, A03, A04 | 3 | +| B. Provisioning | B01-B04, B05-B07, B09, B11, B12 | 11 | +| C. Authorization | C01, C09, C11 | 8 | +| E. Transactions | E01, E14 | 5 | +| F. Remote Control | F01, F03, F05, F06 | 10 | +| G. Availability | G01-G04 | 6 | +| I. Tariff/Cost | I02 | 1 | +| J. MeterValues | J01, J02 | 2 | +| L. Firmware | L01 | 2 | +| M. ISO15118 Certs | M01, M03-M06 | 5 | +| N. Diagnostics | N01, N09 | 5 | +| P. DataTransfer | P01, P02 | 2 | +| Edge/Negative | — | 7 | +| **Total** | **34/34 commands** | **~70** | + +### Not Testable (simulator not implemented) + +D (LocalAuthList), H (Reservation), K (SmartCharging), O (DisplayMessage), N02-N08 (Monitoring).