roundTo,
truncateId,
} from '../../../utils/index.js'
-import {
- AuthenticationMethod,
- type AuthorizationResult,
- mapOCPP16Status,
- OCPPAuthServiceFactory,
-} from '../auth/index.js'
+import { mapOCPP16Status, OCPPAuthServiceFactory } from '../auth/index.js'
import {
buildEmptyMeterValue,
buildMeterValue,
): void {
try {
const authService = OCPPAuthServiceFactory.getInstance(chargingStation)
- const authCache = authService.getAuthCache()
- if (authCache == null) {
- return
- }
- const result: AuthorizationResult = {
- isOffline: false,
- method: AuthenticationMethod.REMOTE_AUTHORIZATION,
- status: mapOCPP16Status(idTagInfo.status),
- timestamp: new Date(),
- }
- let ttl: number | undefined
- if (idTagInfo.expiryDate != null) {
- const expiryDate = convertToDate(idTagInfo.expiryDate)
- if (expiryDate != null) {
- const ttlSeconds = Math.ceil((expiryDate.getTime() - Date.now()) / 1000)
- if (ttlSeconds <= 0) {
- logger.debug(
- `${chargingStation.logPrefix()} ${moduleName}.updateAuthorizationCache: Skipping expired entry for '${truncateId(idTag)}'`
- )
- return
- }
- ttl = ttlSeconds
- }
- }
- authCache.set(idTag, result, ttl)
- logger.debug(
- `${chargingStation.logPrefix()} ${moduleName}.updateAuthorizationCache: Updated cache for '${truncateId(idTag)}' status=${result.status}${ttl != null ? `, ttl=${ttl.toString()}s` : ''}`
- )
+ authService.updateCacheEntry(idTag, mapOCPP16Status(idTagInfo.status), idTagInfo.expiryDate)
} catch (error) {
logger.warn(
`${chargingStation.logPrefix()} ${moduleName}.updateAuthorizationCache: Cache update failed for '${truncateId(idTag)}':`,
-import type {
- JsonObject,
- OCPP20IdTokenInfoType,
- OCPP20IdTokenType,
- OCPPVersion,
-} from '../../../../types/index.js'
+import type { JsonObject, OCPPVersion } from '../../../../types/index.js'
import type {
AuthConfiguration,
AuthorizationResult,
AuthRequest,
Identifier,
} from '../types/AuthTypes.js'
-import type { IdentifierType } from '../types/AuthTypes.js'
+import type { AuthorizationStatus, IdentifierType } from '../types/AuthTypes.js'
/**
* Authorization cache interface
* Adapters handle the translation between auth types
* and version-specific OCPP types and protocols.
*/
-export interface OCPPAuthAdapter<TVersionId = OCPP20IdTokenType | string> {
+export interface OCPPAuthAdapter<TVersionId = unknown> {
/**
* Perform remote authorization using version-specific protocol
* @param identifier - Identifier to authorize
testConnectivity(): boolean
/**
- * Update a cache entry from TransactionEventResponse idTokenInfo (C10.FR.01/04/05)
+ * Update a cache entry from a CSMS authorization response (C10.FR.01/04/05)
* @param identifier - The idToken string to cache
- * @param idTokenInfo - The idTokenInfo from the CSMS response
+ * @param status - The authorization status (mapped from OCPP version-specific types)
+ * @param expiryDate - Optional expiry date from the CSMS response
* @param identifierType - Optional identifier type for cache skip logic (C02.FR.03/C03.FR.02)
*/
updateCacheEntry(
identifier: string,
- idTokenInfo: OCPP20IdTokenInfoType,
+ status: AuthorizationStatus,
+ expiryDate?: Date | string,
identifierType?: IdentifierType
): void
-import type { OCPP20IdTokenInfoType } from '../../../../types/index.js'
import type { OCPPAuthAdapter } from '../interfaces/OCPPAuthService.js'
import { OCPPError } from '../../../../exception/index.js'
type AuthRequest,
type Identifier,
IdentifierType,
- mapOCPP20AuthorizationStatus,
} from '../types/AuthTypes.js'
import { AuthConfigValidator } from '../utils/ConfigValidator.js'
public updateCacheEntry (
identifier: string,
- idTokenInfo: OCPP20IdTokenInfoType,
+ status: AuthorizationStatus,
+ expiryDate?: Date | string,
identifierType?: IdentifierType
): void {
if (!this.config.authorizationCacheEnabled) {
return
}
- const mappedStatus = mapOCPP20AuthorizationStatus(idTokenInfo.status)
+ let ttl: number | undefined
+ if (expiryDate != null) {
+ const parsed = convertToDate(expiryDate)
+ if (parsed != null) {
+ const ttlSeconds = Math.ceil((parsed.getTime() - Date.now()) / 1000)
+ if (ttlSeconds <= 0) {
+ logger.debug(
+ `${this.chargingStation.logPrefix()} ${moduleName}.updateCacheEntry: Skipping expired entry for ${truncateId(identifier)}`
+ )
+ return
+ }
+ ttl = ttlSeconds
+ }
+ }
+ const effectiveTtl = ttl ?? this.config.authorizationCacheLifetime
const result: AuthorizationResult = {
isOffline: false,
method: AuthenticationMethod.REMOTE_AUTHORIZATION,
- status: mappedStatus,
+ status,
timestamp: new Date(),
}
- let ttl: number | undefined
- if (idTokenInfo.cacheExpiryDateTime != null) {
- const expiryDate = convertToDate(idTokenInfo.cacheExpiryDateTime)
- if (expiryDate != null) {
- const expiryMs = expiryDate.getTime()
- const ttlSeconds = Math.ceil((expiryMs - Date.now()) / 1000)
- if (ttlSeconds > 0) {
- ttl = ttlSeconds
- }
- }
- }
- ttl ??= this.config.authorizationCacheLifetime
-
- authCache.set(identifier, result, ttl)
+ authCache.set(identifier, result, effectiveTtl)
logger.debug(
- `${this.chargingStation.logPrefix()} ${moduleName}.updateCacheEntry: Updated cache for ${truncateId(identifier)} status=${mappedStatus}, ttl=${ttl != null ? ttl.toString() : 'default'}s`
+ `${this.chargingStation.logPrefix()} ${moduleName}.updateCacheEntry: Updated cache for ${truncateId(identifier)} status=${status}${effectiveTtl != null ? `, ttl=${effectiveTtl.toString()}s` : ''}`
)
}
/**
- * @file Tests for OCPP20ResponseService cache update on TransactionEventResponse
- * @description Unit tests for auth cache auto-update from TransactionEventResponse idTokenInfo
- * per OCPP 2.0.1 C10.FR.01/05, C12.FR.06, C02.FR.03, C03.FR.02
+ * @file Tests for OCPPAuthServiceImpl.updateCacheEntry
+ * @description Unit tests for auth cache updates per OCPP 2.0.1
+ * C10.FR.01/05, C12.FR.06, C02.FR.03, C03.FR.02
*/
import assert from 'node:assert/strict'
OCPPAuthServiceFactory,
OCPPAuthServiceImpl,
} from '../../../../src/charging-station/ocpp/auth/index.js'
-import { OCPP20AuthorizationStatusEnumType, OCPPVersion } from '../../../../src/types/index.js'
+import { OCPPVersion } from '../../../../src/types/index.js'
import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
})
await it('C10.FR.05 - should update cache on TransactionEventResponse with Accepted idTokenInfo', () => {
- // Arrange
- const idTokenInfo = {
- status: OCPP20AuthorizationStatusEnumType.Accepted,
- }
-
// Act
- authService.updateCacheEntry(TEST_IDENTIFIER, idTokenInfo, IdentifierType.ISO14443)
+ authService.updateCacheEntry(
+ TEST_IDENTIFIER,
+ AuthorizationStatus.ACCEPTED,
+ undefined,
+ IdentifierType.ISO14443
+ )
// Assert
const cached = authCache.get(TEST_IDENTIFIER)
await it('C10.FR.09 - should use cacheExpiryDateTime as TTL when present in idTokenInfo', () => {
// Arrange — expiry 600 seconds from now
const futureDate = new Date(Date.now() + 600_000)
- const idTokenInfo = {
- cacheExpiryDateTime: futureDate,
- status: OCPP20AuthorizationStatusEnumType.Accepted,
- }
// Act
- authService.updateCacheEntry(TEST_IDENTIFIER, idTokenInfo, IdentifierType.ISO14443)
+ authService.updateCacheEntry(
+ TEST_IDENTIFIER,
+ AuthorizationStatus.ACCEPTED,
+ futureDate,
+ IdentifierType.ISO14443
+ )
- // Assert — entry is cached (TTL is explicit, checked via presence)
+ // Assert
const cached = authCache.get(TEST_IDENTIFIER)
assert.ok(cached != null, 'Cache entry should exist with explicit TTL')
assert.strictEqual(cached.status, AuthorizationStatus.ACCEPTED)
})
await it('C10.FR.08 - should use AuthCacheLifeTime as TTL when cacheExpiryDateTime absent', () => {
- // Arrange — no cacheExpiryDateTime
- const idTokenInfo = {
- status: OCPP20AuthorizationStatusEnumType.Accepted,
- }
+ // Act — no expiryDate, uses config.authorizationCacheLifetime
+ authService.updateCacheEntry(
+ TEST_IDENTIFIER,
+ AuthorizationStatus.ACCEPTED,
+ undefined,
+ IdentifierType.ISO14443
+ )
- // Act
- authService.updateCacheEntry(TEST_IDENTIFIER, idTokenInfo, IdentifierType.ISO14443)
-
- // Assert — entry is cached (uses config.authorizationCacheLifetime as default TTL)
+ // Assert
const cached = authCache.get(TEST_IDENTIFIER)
assert.ok(cached != null, 'Cache entry should exist with default TTL')
assert.strictEqual(cached.status, AuthorizationStatus.ACCEPTED)
})
await it('C02.FR.03 - should NOT cache NoAuthorization token type', () => {
- // Arrange
- const idTokenInfo = {
- status: OCPP20AuthorizationStatusEnumType.Accepted,
- }
-
// Act
- authService.updateCacheEntry('', idTokenInfo, IdentifierType.NO_AUTHORIZATION)
+ authService.updateCacheEntry(
+ '',
+ AuthorizationStatus.ACCEPTED,
+ undefined,
+ IdentifierType.NO_AUTHORIZATION
+ )
// Assert
const cached = authCache.get('')
})
await it('C03.FR.02 - should NOT cache Central token type', () => {
- // Arrange
- const idTokenInfo = {
- status: OCPP20AuthorizationStatusEnumType.Accepted,
- }
-
// Act
- authService.updateCacheEntry('CENTRAL_TOKEN_001', idTokenInfo, IdentifierType.CENTRAL)
+ authService.updateCacheEntry(
+ 'CENTRAL_TOKEN_001',
+ AuthorizationStatus.ACCEPTED,
+ undefined,
+ IdentifierType.CENTRAL
+ )
// Assert
const cached = authCache.get('CENTRAL_TOKEN_001')
})
await it('C10.FR.01 - should cache non-Accepted status (Blocked, Expired, etc.)', () => {
- // Arrange — multiple non-Accepted statuses per C10.FR.01: cache ALL statuses
- const blockedInfo = {
- status: OCPP20AuthorizationStatusEnumType.Blocked,
- }
- const expiredInfo = {
- status: OCPP20AuthorizationStatusEnumType.Expired,
- }
-
// Act
- authService.updateCacheEntry('BLOCKED_TOKEN', blockedInfo, IdentifierType.ISO14443)
- authService.updateCacheEntry('EXPIRED_TOKEN', expiredInfo, IdentifierType.ISO14443)
+ authService.updateCacheEntry(
+ 'BLOCKED_TOKEN',
+ AuthorizationStatus.BLOCKED,
+ undefined,
+ IdentifierType.ISO14443
+ )
+ authService.updateCacheEntry(
+ 'EXPIRED_TOKEN',
+ AuthorizationStatus.EXPIRED,
+ undefined,
+ IdentifierType.ISO14443
+ )
// Assert
const cachedBlocked = authCache.get('BLOCKED_TOKEN')
assert.strictEqual(cachedExpired.status, AuthorizationStatus.EXPIRED)
})
+ await it('should skip caching when expiryDate is in the past', () => {
+ // Arrange
+ const pastDate = new Date(Date.now() - 60_000)
+
+ // Act
+ authService.updateCacheEntry(
+ TEST_IDENTIFIER,
+ AuthorizationStatus.ACCEPTED,
+ pastDate,
+ IdentifierType.ISO14443
+ )
+
+ // Assert
+ const cached = authCache.get(TEST_IDENTIFIER)
+ assert.strictEqual(cached, undefined, 'Expired entry must not be cached')
+ })
+
await it('should not update cache when authorizationCacheEnabled is false', () => {
// Arrange — create service with cache disabled
const { station: disabledStation } = createMockChargingStation({
disabledService.initialize()
disabledService.updateConfiguration({ authorizationCacheEnabled: false })
- const idTokenInfo = {
- status: OCPP20AuthorizationStatusEnumType.Accepted,
- }
-
// Act
- disabledService.updateCacheEntry(TEST_IDENTIFIER, idTokenInfo, IdentifierType.ISO14443)
+ disabledService.updateCacheEntry(
+ TEST_IDENTIFIER,
+ AuthorizationStatus.ACCEPTED,
+ undefined,
+ IdentifierType.ISO14443
+ )
- // Assert — cache should not have been written to
+ // Assert
const localStrategy = disabledService.getStrategy('local') as LocalAuthStrategy | undefined
const cache = localStrategy?.getAuthCache()
const cached = cache?.get(TEST_IDENTIFIER)