--- /dev/null
+/**
+ * Testable wrapper for OCPP 2.0 ResponseService
+ *
+ * This module provides type-safe access to private handler methods of
+ * OCPP20ResponseService for testing purposes. It replaces ad hoc
+ * `as unknown as` casts at test sites with a properly typed interface,
+ * enabling:
+ * - Type-safe method invocations in tests
+ * - IntelliSense and autocompletion for handler parameters and returns
+ * - Compile-time checking for test code
+ * @example
+ * ```typescript
+ * import { createTestableResponseService } from './__testable__/index.js'
+ *
+ * const testable = createTestableResponseService(new OCPP20ResponseService())
+ * await testable.handleResponseTransactionEvent(station, payload, requestPayload)
+ * ```
+ */
+
+import type {
+ OCPP20TransactionEventRequest,
+ OCPP20TransactionEventResponse,
+} from '../../../../types/index.js'
+import type { ChargingStation } from '../../../index.js'
+import type { OCPP20ResponseService } from '../OCPP20ResponseService.js'
+
+/**
+ * Interface exposing private handler methods of OCPP20ResponseService for testing.
+ * Each method signature matches the corresponding private method in the service class.
+ */
+export interface TestableOCPP20ResponseService {
+ handleResponseTransactionEvent: (
+ chargingStation: ChargingStation,
+ payload: OCPP20TransactionEventResponse,
+ requestPayload: OCPP20TransactionEventRequest
+ ) => Promise<void>
+}
+
+/**
+ * Creates a testable wrapper around OCPP20ResponseService.
+ * Provides type-safe access to private handler methods without `as any` casts.
+ * @param service - The OCPP20ResponseService instance to wrap
+ * @returns A typed interface exposing private handler methods
+ * @example
+ * ```typescript
+ * // Before (with as any cast):
+ * await (service as any).handleResponseTransactionEvent(station, payload, requestPayload)
+ *
+ * // After (with testable interface):
+ * const testable = createTestableResponseService(service)
+ * await testable.handleResponseTransactionEvent(station, payload, requestPayload)
+ * ```
+ */
+export function createTestableResponseService (
+ service: OCPP20ResponseService
+): TestableOCPP20ResponseService {
+ // Cast to unknown first to satisfy TypeScript while preserving runtime behavior
+ const serviceImpl = service as unknown as TestableOCPP20ResponseService
+
+ return {
+ handleResponseTransactionEvent: serviceImpl.handleResponseTransactionEvent.bind(service),
+ }
+}
type TestableRequestServiceResult,
} from './OCPP20RequestServiceTestable.js'
+export {
+ createTestableResponseService,
+ type TestableOCPP20ResponseService,
+} from './OCPP20ResponseServiceTestable.js'
+
export {
createTestableVariableManager,
type TestableOCPP20VariableManager,
import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../helpers/StationHelpers.js'
+import {
+ createMockAuthService,
+ createTestAuthConfig,
+ injectMockAuthService,
+ withThrowingAuthServiceFactory,
+} from '../auth/helpers/MockFactories.js'
-await describe('C11 - Clear Authorization Data in Authorization Cache', async () => {
- afterEach(() => {
- standardCleanup()
- })
+/**
+ * Configure and inject a mock auth service for the current station.
+ * @param station - Charging station under test
+ * @param authorizationCacheEnabled - Whether the mock auth cache is enabled
+ * @param clearCache - Mock cache clearing implementation
+ */
+function setupMockAuthService (
+ station: ChargingStation,
+ authorizationCacheEnabled: boolean,
+ clearCache: () => void = () => {
+ /* empty */
+ }
+): void {
+ injectMockAuthService(
+ station,
+ createMockAuthService({
+ clearCache,
+ getConfiguration: () => createTestAuthConfig({ authorizationCacheEnabled }),
+ })
+ )
+}
+await describe('C11 - Clear Authorization Data in Authorization Cache', async () => {
let station: ChargingStation
let incomingRequestService: OCPP20IncomingRequestService
let testableService: ReturnType<typeof createTestableIncomingRequestService>
testableService = createTestableIncomingRequestService(incomingRequestService)
})
+ afterEach(() => {
+ OCPPAuthServiceFactory.clearAllInstances()
+ standardCleanup()
+ })
+
// FR: C11.FR.01 - CS SHALL attempt to clear its Authorization Cache
await it('should handle ClearCache request successfully', async () => {
const response = await testableService.handleRequestClearCache(station)
// CLR-001: Verify Authorization Cache is cleared (not IdTagsCache)
await describe('CLR-001 - ClearCache clears Authorization Cache', async () => {
await it('should call authService.clearCache() on ClearCache request', async () => {
- // Create a mock auth service to verify clearCache is called
const clearCacheMock = mock.fn()
- const mockAuthService = {
- clearCache: clearCacheMock,
- getConfiguration: () => ({
- authorizationCacheEnabled: true,
- }),
- }
-
- // Mock the factory to return our mock auth service
- const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, true, clearCacheMock)
- try {
- const response = await testableService.handleRequestClearCache(station)
+ const response = await testableService.handleRequestClearCache(station)
- assert.strictEqual(clearCacheMock.mock.callCount(), 1)
- assert.strictEqual(response.status, GenericStatus.Accepted)
- } finally {
- // Restore original factory method
- Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
- }
+ assert.strictEqual(clearCacheMock.mock.callCount(), 1)
+ assert.strictEqual(response.status, GenericStatus.Accepted)
})
await it('should not call idTagsCache.deleteIdTags() on ClearCache request', async () => {
// CLR-002: Verify AuthCacheEnabled check per C11.FR.04
await describe('CLR-002 - AuthCacheEnabled Check (C11.FR.04)', async () => {
await it('should return Rejected when AuthCacheEnabled is false', async () => {
- // Create a mock auth service with cache disabled
- const mockAuthService = {
- clearCache: (): void => {
- throw new Error('clearCache should not be called when cache is disabled')
- },
- getConfiguration: () => ({
- authorizationCacheEnabled: false,
- }),
- }
-
- // Mock the factory to return our mock auth service
- const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
+ setupMockAuthService(station, false, () => {
+ throw new Error('clearCache should not be called when cache is disabled')
})
- try {
- const response = await testableService.handleRequestClearCache(station)
+ const response = await testableService.handleRequestClearCache(station)
- assert.strictEqual(response.status, GenericStatus.Rejected)
- } finally {
- // Restore original factory method
- Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
- }
+ assert.strictEqual(response.status, GenericStatus.Rejected)
})
await it('should return Accepted when AuthCacheEnabled is true and clear succeeds', async () => {
- // Create a mock auth service with cache enabled
- const mockAuthService = {
- clearCache: (): void => {
- /* empty */
- },
- getConfiguration: () => ({
- authorizationCacheEnabled: true,
- }),
- }
-
- // Mock the factory to return our mock auth service
- const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, true)
- try {
- const response = await testableService.handleRequestClearCache(station)
+ const response = await testableService.handleRequestClearCache(station)
- assert.strictEqual(response.status, GenericStatus.Accepted)
- } finally {
- // Restore original factory method
- Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
- }
+ assert.strictEqual(response.status, GenericStatus.Accepted)
})
await it('should return Rejected when clearCache throws an error', async () => {
- // Create a mock auth service that throws on clearCache
- const mockAuthService = {
- clearCache: (): void => {
- throw new Error('Cache clear failed')
- },
- getConfiguration: () => ({
- authorizationCacheEnabled: true,
- }),
- }
-
- // Mock the factory to return our mock auth service
- const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
+ setupMockAuthService(station, true, () => {
+ throw new Error('Cache clear failed')
})
- try {
- const response = await testableService.handleRequestClearCache(station)
+ const response = await testableService.handleRequestClearCache(station)
- assert.strictEqual(response.status, GenericStatus.Rejected)
- } finally {
- // Restore original factory method
- Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
- }
+ assert.strictEqual(response.status, GenericStatus.Rejected)
})
await it('should not attempt to clear cache when AuthCacheEnabled is false', async () => {
const clearCacheMock = mock.fn()
- const mockAuthService = {
- clearCache: clearCacheMock,
- getConfiguration: () => ({
- authorizationCacheEnabled: false,
- }),
- }
+ setupMockAuthService(station, false, clearCacheMock)
- // Mock the factory to return our mock auth service
- const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
-
- try {
- await testableService.handleRequestClearCache(station)
+ await testableService.handleRequestClearCache(station)
- // clearCache should NOT be called when cache is disabled
- assert.strictEqual(clearCacheMock.mock.callCount(), 0)
- } finally {
- // Restore original factory method
- Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
- }
+ assert.strictEqual(clearCacheMock.mock.callCount(), 0)
})
})
// C11.FR.05: IF the CS does not support an Authorization Cache → Rejected
await describe('C11.FR.05 - No Authorization Cache Support', async () => {
await it('should return Rejected when authService factory fails (no cache support)', async () => {
- // Mock factory to throw error (simulates no Authorization Cache support)
- const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): never => {
- throw new Error('Authorization Cache not supported')
- },
- })
-
- try {
- const response = await testableService.handleRequestClearCache(station)
+ const response = await withThrowingAuthServiceFactory(
+ 'Authorization Cache not supported',
+ () => testableService.handleRequestClearCache(station)
+ )
- // Per C11.FR.05: SHALL return Rejected if CS does not support Authorization Cache
- assert.strictEqual(response.status, GenericStatus.Rejected)
- } finally {
- // Restore original factory method
- Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
- }
+ // Per C11.FR.05: SHALL return Rejected if CS does not support Authorization Cache
+ assert.strictEqual(response.status, GenericStatus.Rejected)
})
})
})
import { afterEach, beforeEach, describe, it } from 'node:test'
import type { ChargingStation } from '../../../../src/charging-station/index.js'
+import type { LocalAuthListManager } from '../../../../src/charging-station/ocpp/auth/interfaces/OCPPAuthService.js'
import { buildConfigKey } from '../../../../src/charging-station/index.js'
import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../helpers/StationHelpers.js'
+import {
+ createMockAuthService,
+ createTestAuthConfig,
+ injectMockAuthService,
+ withThrowingAuthServiceFactory,
+} from '../auth/helpers/MockFactories.js'
import { upsertConfigurationKey } from './OCPP20TestUtils.js'
+/**
+ * Configure and inject a mock auth service for LocalAuthList request handling tests.
+ * @param station - Charging station under test
+ * @param manager - Local auth list manager exposed by the mock service
+ * @param localAuthListEnabled - Whether LocalAuthList is enabled in auth configuration
+ */
+function setupMockAuthService (
+ station: ChargingStation,
+ manager: LocalAuthListManager | undefined,
+ localAuthListEnabled = true
+): void {
+ injectMockAuthService(
+ station,
+ createMockAuthService({
+ getConfiguration: () => createTestAuthConfig({ localAuthListEnabled }),
+ getLocalAuthListManager: () => manager,
+ })
+ )
+}
+
await describe('OCPP20IncomingRequestService — LocalAuthList', async () => {
let station: ChargingStation
let testableService: ReturnType<typeof createTestableIncomingRequestService>
- let originalGetInstance: typeof OCPPAuthServiceFactory.getInstance
/**
* Toggle the LocalAuthListCtrlr.Enabled configuration key on the mock station.
station = mockStation
const incomingRequestService = new OCPP20IncomingRequestService()
testableService = createTestableIncomingRequestService(incomingRequestService)
- originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
setLocalAuthListEnabled(true)
})
afterEach(() => {
- Object.assign(OCPPAuthServiceFactory, { getInstance: originalGetInstance })
+ OCPPAuthServiceFactory.clearAllInstances()
standardCleanup()
})
await describe('GetLocalListVersion', async () => {
await it('should return version 0 for empty list', () => {
const manager = new InMemoryLocalAuthListManager()
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
const response = testableService.handleRequestGetLocalListVersion(station)
await it('should return version 0 when local auth list disabled', () => {
setLocalAuthListEnabled(false)
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: false }),
- getLocalAuthListManager: () => undefined,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, undefined, false)
const response = testableService.handleRequestGetLocalListVersion(station)
})
await it('should return version 0 when manager is undefined', () => {
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => undefined,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, undefined)
const response = testableService.handleRequestGetLocalListVersion(station)
await it('should return correct version after SendLocalList', () => {
const manager = new InMemoryLocalAuthListManager()
manager.setEntries([{ identifier: 'TOKEN_001', status: 'Accepted' }], 5)
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
const response = testableService.handleRequestGetLocalListVersion(station)
assert.strictEqual(response.versionNumber, 5)
})
- await it('should return version 0 when auth service throws', () => {
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): never => {
- throw new Error('Auth service unavailable')
- },
- })
-
- const response = testableService.handleRequestGetLocalListVersion(station)
+ await it('should return version 0 when auth service throws', async () => {
+ const response = await withThrowingAuthServiceFactory('Auth service unavailable', () =>
+ testableService.handleRequestGetLocalListVersion(station)
+ )
assert.strictEqual(response.versionNumber, 0)
})
await describe('SendLocalList', async () => {
await it('should accept Full update and replace list', () => {
const manager = new InMemoryLocalAuthListManager()
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
const response = testableService.handleRequestSendLocalList(station, {
localAuthorizationList: [
await it('should accept Differential update with complex IdToken types', () => {
const manager = new InMemoryLocalAuthListManager()
manager.setEntries([{ identifier: 'EXISTING_001', status: 'Accepted' }], 1)
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
const response = testableService.handleRequestSendLocalList(station, {
localAuthorizationList: [
await it('should return Failed when disabled (with statusInfo)', () => {
setLocalAuthListEnabled(false)
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: false }),
- getLocalAuthListManager: () => undefined,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, undefined, false)
const response = testableService.handleRequestSendLocalList(station, {
localAuthorizationList: [],
})
await it('should return Failed when manager is undefined', () => {
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => undefined,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, undefined)
const response = testableService.handleRequestSendLocalList(station, {
localAuthorizationList: [],
],
1
)
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
const response = testableService.handleRequestSendLocalList(station, {
localAuthorizationList: [],
assert.strictEqual(version, 2)
})
- await it('should return Failed when auth service throws', () => {
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): never => {
- throw new Error('Auth service unavailable')
- },
- })
-
- const response = testableService.handleRequestSendLocalList(station, {
- localAuthorizationList: [],
- updateType: OCPP20UpdateEnumType.Full,
- versionNumber: 1,
- })
+ await it('should return Failed when auth service throws', async () => {
+ const response = await withThrowingAuthServiceFactory('Auth service unavailable', () =>
+ testableService.handleRequestSendLocalList(station, {
+ localAuthorizationList: [],
+ updateType: OCPP20UpdateEnumType.Full,
+ versionNumber: 1,
+ })
+ )
assert.strictEqual(response.status, OCPP20SendLocalListStatusEnumType.Failed)
})
],
1
)
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
const response = testableService.handleRequestSendLocalList(station, {
localAuthorizationList: [
await it('should preserve idTokenType metadata in entries', () => {
const manager = new InMemoryLocalAuthListManager()
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
testableService.handleRequestSendLocalList(station, {
localAuthorizationList: [
await it('should handle Full update with undefined localAuthorizationList', () => {
const manager = new InMemoryLocalAuthListManager()
manager.setEntries([{ identifier: 'OLD_TOKEN', status: 'Accepted' }], 1)
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
const response = testableService.handleRequestSendLocalList(station, {
updateType: OCPP20UpdateEnumType.Full,
await it('should convert cacheExpiryDateTime to Date', () => {
const manager = new InMemoryLocalAuthListManager()
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
const expiryDate = new Date('2027-01-01T00:00:00.000Z')
await it('should return VersionMismatch for differential update with version < current', () => {
const manager = new InMemoryLocalAuthListManager()
manager.setEntries([{ identifier: 'TOKEN_001', status: 'Accepted' }], 5)
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
const response = testableService.handleRequestSendLocalList(station, {
localAuthorizationList: [
await it('should return VersionMismatch for differential update with version equal to current', () => {
const manager = new InMemoryLocalAuthListManager()
manager.setEntries([{ identifier: 'TOKEN_001', status: 'Accepted' }], 5)
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
const response = testableService.handleRequestSendLocalList(station, {
localAuthorizationList: [
await it('should return Failed with versionNumber <= 0', () => {
const manager = new InMemoryLocalAuthListManager()
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
const response = testableService.handleRequestSendLocalList(station, {
localAuthorizationList: [],
await it('should return Failed with negative versionNumber', () => {
const manager = new InMemoryLocalAuthListManager()
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
const response = testableService.handleRequestSendLocalList(station, {
localAuthorizationList: [],
await it('should accept Full update regardless of version (no VersionMismatch)', () => {
const manager = new InMemoryLocalAuthListManager()
manager.setEntries([{ identifier: 'TOKEN_001', status: 'Accepted' }], 5)
- const mockAuthService = {
- getConfiguration: () => ({ localAuthListEnabled: true }),
- getLocalAuthListManager: () => manager,
- }
- Object.assign(OCPPAuthServiceFactory, {
- getInstance: (): typeof mockAuthService => mockAuthService,
- })
+ setupMockAuthService(station, manager)
const response = testableService.handleRequestSendLocalList(station, {
localAuthorizationList: [
import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import type { ChargingStation } from '../../../../src/charging-station/index.js'
-import type {
- OCPP20TransactionEventRequest,
- OCPP20TransactionEventResponse,
- UUIDv4,
-} from '../../../../src/types/index.js'
+import type { OCPP20TransactionEventResponse } from '../../../../src/types/index.js'
+import {
+ createTestableResponseService,
+ type TestableOCPP20ResponseService,
+} from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
import {
OCPP20AuthorizationStatusEnumType,
OCPP20IdTokenEnumType,
OCPP20TransactionEventEnumType,
- OCPP20TriggerReasonEnumType,
OCPPVersion,
} from '../../../../src/types/index.js'
import { Constants, logger } from '../../../../src/utils/index.js'
TEST_TRANSACTION_UUID,
} from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../helpers/StationHelpers.js'
-
-interface TestableOCPP20ResponseService {
- handleResponseTransactionEvent: (
- chargingStation: ChargingStation,
- payload: OCPP20TransactionEventResponse,
- requestPayload: OCPP20TransactionEventRequest
- ) => Promise<void>
-}
-
-/**
- * Builds a minimal OCPP20TransactionEventRequest with the given event type and
- * transaction id. Used as the request-payload twin in handler dispatch.
- * @param transactionId - The transaction UUID embedded in transactionInfo
- * @param eventType - The TransactionEvent type (Started/Updated/Ended)
- * @param idToken - Optional idToken to attach; required to exercise the auth-cache
- * update path at OCPP20ResponseService.ts (C10.FR.01/04/05)
- * @param idToken.idToken - OCPP IdToken value (e.g. an RFID tag string)
- * @param idToken.type - OCPP 2.0.1 IdToken type (e.g. `ISO14443`, `ISO15693`, `Central`)
- * @returns A minimal OCPP20TransactionEventRequest
- */
-function buildTransactionEventRequest (
- transactionId: UUIDv4,
- eventType: OCPP20TransactionEventEnumType,
- idToken?: { idToken: string; type: OCPP20IdTokenEnumType }
-): OCPP20TransactionEventRequest {
- return {
- eventType,
- ...(idToken != null ? { idToken } : {}),
- meterValue: [],
- seqNo: 0,
- timestamp: new Date(),
- transactionInfo: {
- transactionId,
- },
- triggerReason: OCPP20TriggerReasonEnumType.Authorized,
- }
-}
-
-/**
- * Wraps an OCPP20ResponseService instance so its private
- * `handleResponseTransactionEvent` is reachable by tests via a typed cast.
- * Mirrors the helper in `OCPP20ResponseService-TransactionEvent.test.ts`.
- * @param service - The OCPP20ResponseService instance to wrap
- * @returns A typed interface exposing the private handler
- */
-function createTestableResponseService (
- service: OCPP20ResponseService
-): TestableOCPP20ResponseService {
- const serviceImpl = service as unknown as TestableOCPP20ResponseService
- return {
- handleResponseTransactionEvent: serviceImpl.handleResponseTransactionEvent.bind(service),
- }
-}
+import { buildTransactionEventRequest } from './OCPP20ResponseServiceTestUtils.js'
await describe('OCPP20ResponseService — forceTransactionOnInvalidIdToken (issue #1826)', async () => {
let station: ChargingStation
import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import type { ChargingStation } from '../../../../src/charging-station/index.js'
-import type {
- OCPP20TransactionEventRequest,
- OCPP20TransactionEventResponse,
- UUIDv4,
-} from '../../../../src/types/index.js'
+import type { OCPP20TransactionEventResponse, UUIDv4 } from '../../../../src/types/index.js'
+import {
+ createTestableResponseService,
+ type TestableOCPP20ResponseService,
+} from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
import {
OCPP20AuthorizationStatusEnumType,
OCPP20MessageFormatEnumType,
- OCPP20TransactionEventEnumType,
- OCPP20TriggerReasonEnumType,
OCPPVersion,
} from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
TEST_TRANSACTION_UUID,
} from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../helpers/StationHelpers.js'
-
-interface TestableOCPP20ResponseService {
- handleResponseTransactionEvent: (
- chargingStation: ChargingStation,
- payload: OCPP20TransactionEventResponse,
- requestPayload: OCPP20TransactionEventRequest
- ) => Promise<void>
-}
-
-/**
- * Builds a minimal OCPP20TransactionEventRequest for use as requestPayload in tests.
- * @param transactionId - The transaction UUID to embed in transactionInfo
- * @returns A minimal OCPP20TransactionEventRequest
- */
-function buildTransactionEventRequest (transactionId: UUIDv4): OCPP20TransactionEventRequest {
- return {
- eventType: OCPP20TransactionEventEnumType.Updated,
- meterValue: [],
- seqNo: 0,
- timestamp: new Date(),
- transactionInfo: {
- transactionId,
- },
- triggerReason: OCPP20TriggerReasonEnumType.Authorized,
- }
-}
-
-/**
- * Creates a testable wrapper around OCPP20ResponseService.
- * @param service - The OCPP20ResponseService instance to wrap
- * @returns A typed interface exposing private handler methods
- */
-function createTestableResponseService (
- service: OCPP20ResponseService
-): TestableOCPP20ResponseService {
- const serviceImpl = service as unknown as TestableOCPP20ResponseService
- return {
- handleResponseTransactionEvent: serviceImpl.handleResponseTransactionEvent.bind(service),
- }
-}
+import { buildTransactionEventRequest } from './OCPP20ResponseServiceTestUtils.js'
await describe('D01 - TransactionEvent Response', async () => {
let station: ChargingStation
--- /dev/null
+/**
+ * @file OCPP20ResponseServiceTestUtils
+ * @description Test-only payload builders for OCPP 2.0 response-service tests
+ */
+import type {
+ OCPP20IdTokenType,
+ OCPP20TransactionEventRequest,
+ UUIDv4,
+} from '../../../../src/types/index.js'
+
+import {
+ OCPP20TransactionEventEnumType,
+ OCPP20TriggerReasonEnumType,
+} from '../../../../src/types/index.js'
+
+/**
+ * Build a minimal TransactionEvent request payload for response-service tests.
+ * @param transactionId - Transaction id to embed in the request payload.
+ * @param eventType - TransactionEvent type to use; defaults to Updated.
+ * @param idToken - Optional idToken used by authorization-cache test cases
+ * (see `OCPP20IdTokenType` for field semantics).
+ * @returns Minimal TransactionEvent request payload.
+ */
+export const buildTransactionEventRequest = (
+ transactionId: UUIDv4,
+ eventType: OCPP20TransactionEventEnumType = OCPP20TransactionEventEnumType.Updated,
+ idToken?: OCPP20IdTokenType
+): OCPP20TransactionEventRequest => ({
+ eventType,
+ ...(idToken != null ? { idToken } : {}),
+ meterValue: [],
+ seqNo: 0,
+ timestamp: new Date(),
+ transactionInfo: {
+ transactionId,
+ },
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+})
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 { OCPPAuthServiceFactory } from '../../../../../src/charging-station/ocpp/auth/services/OCPPAuthServiceFactory.js'
import {
type AuthConfiguration,
AuthContext,
assert.ok(cache != null, 'Auth cache must be available for test')
return cache
}
+
+// ============================================================================
+// Factory Injection Helpers
+// ============================================================================
+
+/**
+ * Inject a mock OCPPAuthService into the factory cache for the given station.
+ * Centralizes the station-id lookup and `setInstanceForTesting` plumbing used
+ * by OCPP 2.0 IncomingRequest tests (ClearCache, LocalAuthList, ...).
+ * @param station - Charging station under test (station-id read from stationInfo).
+ * @param service - Mock auth service to inject.
+ * @returns The injected service for chaining.
+ */
+export const injectMockAuthService = (
+ station: ChargingStation,
+ service: OCPPAuthService
+): OCPPAuthService => {
+ OCPPAuthServiceFactory.setInstanceForTesting(
+ station.stationInfo?.chargingStationId ?? 'unknown',
+ service
+ )
+ return service
+}
+
+/**
+ * Replace `OCPPAuthServiceFactory.getInstance` with a throwing function for the
+ * duration of `fn`, then restore the original. Used by tests that must exercise
+ * handler code paths surviving a factory failure (factory unavailable, no
+ * Authorization Cache support, ...). Required because `setInstanceForTesting`
+ * only injects a returned instance — it cannot make `getInstance` itself throw.
+ * @param errorMessage - Error message thrown by the patched `getInstance`.
+ * @param fn - Synchronous or asynchronous function executed while patched.
+ * @returns The value returned by `fn`.
+ */
+export const withThrowingAuthServiceFactory = async <T>(
+ errorMessage: string,
+ fn: () => Promise<T> | T
+): Promise<T> => {
+ const original = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
+ Object.assign(OCPPAuthServiceFactory, {
+ getInstance: (): never => {
+ throw new Error(errorMessage)
+ },
+ })
+ try {
+ return await fn()
+ } finally {
+ Object.assign(OCPPAuthServiceFactory, { getInstance: original })
+ }
+}
import type { IncomingMessage } from 'node:http'
import assert from 'node:assert/strict'
-import { once } from 'node:events'
import { afterEach, beforeEach, describe, it } from 'node:test'
import { gunzipSync } from 'node:zlib'
import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
import { TEST_UUID } from './UIServerTestConstants.js'
import {
+ awaitFinish,
createMockBootstrap,
createMockIncomingMessage,
createMockUIServerConfiguration,
this.responseHandlers.set(uuid, res as never)
}
+ public emitRequest (req: IncomingMessage, res: MockServerResponse): void {
+ const httpServer = Reflect.get(this, 'httpServer') as {
+ emit: (eventName: string, req: IncomingMessage, res: MockServerResponse) => boolean
+ }
+ httpServer.emit('request', req, res)
+ }
+
public getAcceptsGzip (): Map<UUIDv4, boolean> {
return Reflect.get(this, 'acceptsGzip')
}
try {
gatedServer.start()
- httpServer.emit('request', req, res)
+ gatedServer.emitRequest(req, res)
} finally {
httpServer.removeAllListeners()
gatedServer.stop()
try {
gatedServer.start()
- httpServer.emit('request', denyingReq, res)
+ gatedServer.emitRequest(denyingReq, res)
} finally {
httpServer.removeAllListeners()
gatedServer.stop()
gzipServer.setAcceptsGzip(TEST_UUID, true)
gzipServer.sendResponse([TEST_UUID, createLargePayload()])
- await once(res, 'finish')
+ await awaitFinish(res)
assert.strictEqual(res.headers['Content-Encoding'], 'gzip')
assert.strictEqual(res.headers['Content-Type'], 'application/json')
gzipServer.setAcceptsGzip(TEST_UUID, true)
gzipServer.sendResponse([TEST_UUID, payload])
- await once(res, 'finish')
+ await awaitFinish(res)
assert.notStrictEqual(res.bodyBuffer, undefined)
if (res.bodyBuffer == null) {
gzipServer.sendResponse([TEST_UUID, createLargePayload()])
- await once(res, 'finish')
+ await awaitFinish(res)
assert.strictEqual(gzipServer.getAcceptsGzip().has(TEST_UUID), false)
})
import type { Registry } from 'prom-client'
import assert from 'node:assert/strict'
-import { once } from 'node:events'
import { afterEach, beforeEach, describe, it } from 'node:test'
import type {
import { logger } from '../../../src/utils/index.js'
import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
import {
+ awaitFinish,
createMockBootstrap,
createMockIncomingMessage,
createMockUIServerConfiguration,
+ drainResponses,
MockServerResponse,
} from './UIServerTestUtils.js'
server.start()
const res = new MockServerResponse()
server.emitRequest(buildMetricsRequest(), res)
- await once(res, 'finish')
+ await awaitFinish(res)
assert.strictEqual(res.statusCode, 200)
assert.match(res.headers['Content-Type'] ?? '', /^text\/plain;\s*version=0\.0\.4/)
assert.match(res.body ?? '', /^# HELP /m)
server.start()
const res = new MockServerResponse()
server.emitRequest(buildMetricsRequest(), res)
- await once(res, 'finish')
+ await awaitFinish(res)
const body = res.body ?? ''
assert.match(body, /^simulator_charging_stations_configured_total\s+5$/m)
assert.match(body, /^simulator_charging_stations_provisioned_total\s+2$/m)
server.start()
const res = new MockServerResponse()
server.emitRequest(buildMetricsRequest(), res)
- await once(res, 'finish')
+ await awaitFinish(res)
const body = res.body ?? ''
assert.match(body, /simulator_station_started\{[^}]*hash_id="station-T5"[^}]*\}\s+1/)
assert.match(body, /simulator_station_ws_state\{[^}]*hash_id="station-T5"[^}]*\}\s+1/)
server.start()
const res = new MockServerResponse()
server.emitRequest(buildMetricsRequest(), res)
- await once(res, 'finish')
+ await awaitFinish(res)
const body = res.body ?? ''
const line = body
.split('\n')
}),
res
)
- await once(res, 'finish')
+ await awaitFinish(res)
assert.strictEqual(res.statusCode, 200)
} finally {
authServer.stop()
server.start()
const res = new MockServerResponse()
server.emitRequest(buildMetricsRequest(), res)
- await once(res, 'finish')
+ await awaitFinish(res)
const body = res.body ?? ''
for (const secret of [
'SECRET-IDTAG-12345',
server.start()
const res = new MockServerResponse()
server.emitRequest(buildMetricsRequest(), res)
- await once(res, 'finish')
+ await awaitFinish(res)
const body = res.body ?? ''
assert.ok(
!/^fake_metric\b/m.test(body),
server.start()
const res = new MockServerResponse()
server.emitRequest(buildMetricsRequest(), res)
- await once(res, 'finish')
+ await awaitFinish(res)
assert.strictEqual(res.statusCode, 200)
const matchingCalls = warnSpy.mock.calls.filter(call => {
const message: unknown = call.arguments[0]
server.start()
const res = new MockServerResponse()
server.emitRequest(buildMetricsRequest(), res)
- await once(res, 'finish')
+ await awaitFinish(res)
const body = res.body ?? ''
assert.ok(
!/simulator_station_ws_state\{[^}]*hash_id="station-Mws"[^}]*\}/.test(body),
server.start()
const res = new MockServerResponse()
server.emitRequest(buildMetricsRequest(), res)
- await once(res, 'finish')
+ await awaitFinish(res)
const body = res.body ?? ''
assert.match(body, /simulator_station_connectors_total\{[^}]*hash_id="station-T18"[^}]*\}\s+1/)
const statusLine = body
probeServer.start()
const probeRes = new MockServerResponse()
probeServer.emitRequest(buildMetricsRequest(), probeRes)
- await once(probeRes, 'finish')
+ await awaitFinish(probeRes)
const probedSampleCount = (probeRes.body ?? '')
.split('\n')
.filter(line => line.length > 0 && !line.startsWith('#')).length
exactServer.start()
const exactRes = new MockServerResponse()
exactServer.emitRequest(buildMetricsRequest(), exactRes)
- await once(exactRes, 'finish')
+ await awaitFinish(exactRes)
const exactSoftCapCalls = warnSpy.mock.calls.filter(call => {
const message: unknown = call.arguments[0]
return typeof message === 'string' && message.includes(METRICS_SOFT_CAP_WARN_PREFIX)
belowServer.start()
const belowRes = new MockServerResponse()
belowServer.emitRequest(buildMetricsRequest(), belowRes)
- await once(belowRes, 'finish')
+ await awaitFinish(belowRes)
const belowSoftCapCalls = warnSpy.mock.calls.filter(call => {
const message: unknown = call.arguments[0]
return typeof message === 'string' && message.includes(METRICS_SOFT_CAP_WARN_PREFIX)
probeServer.start()
const probeRes = new MockServerResponse()
probeServer.emitRequest(buildMetricsRequest(), probeRes)
- await once(probeRes, 'finish')
+ await awaitFinish(probeRes)
const probedSampleCount = (probeRes.body ?? '')
.split('\n')
.filter(line => line.length > 0 && !line.startsWith('#')).length
const resB = new MockServerResponse()
concurrentServer.emitRequest(buildMetricsRequest(), resA)
concurrentServer.emitRequest(buildMetricsRequest(), resB)
- await Promise.all([once(resA, 'finish'), once(resB, 'finish')])
+ await drainResponses([resA, resB])
concurrentServer.stop()
assert.strictEqual(resA.statusCode, 200)
server.start()
const res = new MockServerResponse()
server.emitRequest(buildMetricsRequest(), res)
- await once(res, 'finish')
+ await awaitFinish(res)
const body = res.body ?? ''
assert.ok(body.includes('1.0.0-✓'), 'non-ASCII version must propagate into exposition body')
const checkMarkOccurrences = (body.match(/✓/gu) ?? []).length
const chain0 = getChain()
const r1 = new MockServerResponse()
server.emitRequest(buildMetricsRequest(), r1)
- await once(r1, 'finish')
+ await awaitFinish(r1)
const chainAfterScrape1 = getChain()
assert.notStrictEqual(
chainAfterScrape1,
)
const r2 = new MockServerResponse()
server.emitRequest(buildMetricsRequest(), r2)
- await once(r2, 'finish')
+ await awaitFinish(r2)
const chainAfterScrape2 = getChain()
const all = [chain0, chainAfterScrape1, chainAfterStop, chain1, chainAfterScrape2]
assert.strictEqual(
localServer.start()
const r1 = new MockServerResponse()
localServer.emitRequest(buildMetricsRequest(), r1)
- await once(r1, 'finish')
+ await awaitFinish(r1)
const registry = localServer.getMetricsRegistry()
assert.ok(registry !== undefined, 'precondition: registry present before stop()')
let clearCalls = 0
}
const res = new MockServerResponse()
localServer.emitRequest(buildMetricsRequest(), res)
- await once(res, 'finish')
+ await awaitFinish(res)
const rejectedChain = Reflect.get(localServer, 'metricsScrapeChain') as Promise<void>
let rejected = false
await rejectedChain.catch(() => {
setImmediate(resolve)
})
releaseGate()
- await once(res, 'finish')
+ await awaitFinish(res)
const scrapeWarns = warnSpy.mock.calls.filter(
c =>
typeof c.arguments[0] === 'string' &&
localServer.start()
const res = new MockServerResponse()
localServer.emitRequest(buildMetricsRequest(), res)
- await once(res, 'finish')
+ await awaitFinish(res)
assert.strictEqual(res.statusCode, 200)
const sampleCount = (res.body ?? '')
.split('\n')
localServer.start()
const res = new MockServerResponse()
localServer.emitRequest(buildMetricsRequest(), res)
- await once(res, 'finish')
+ await awaitFinish(res)
assert.strictEqual(res.statusCode, 200)
const scrapeWarns = warnSpy.mock.calls.filter(
c =>
'precondition: stop() nulled this.metricsRegistry mid-flight'
)
releaseGate()
- await once(res, 'finish')
+ await awaitFinish(res)
assert.strictEqual(
res.statusCode,
200,