From 7ffcb51d41fd164d10db5f538a59c0ed10f7de83 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sun, 1 Mar 2026 00:25:52 +0100 Subject: [PATCH] refactor(tests): remove aliases, extract time constants, fix naming and lint issues --- .../ChargingStation-Configuration.test.ts | 38 ++-- .../ChargingStation-Connectors.test.ts | 46 +++-- .../ChargingStation-Resilience.test.ts | 3 +- .../ChargingStation-Transactions.test.ts | 21 +- .../charging-station/ChargingStation.test.ts | 21 +- .../ChargingStationTestConstants.ts | 3 + .../ConfigurationKeyUtils.test.ts | 126 +++++++++++- tests/charging-station/Helpers.test.ts | 135 ++++++++++++- .../ocpp/2.0/OCPP20CertificateManager.test.ts | 98 ++++----- ...0IncomingRequestService-ClearCache.test.ts | 2 +- ...OCPP20IncomingRequestService-Reset.test.ts | 186 ++++++++++++------ .../2.0/OCPP20RequestService-ISO15118.test.ts | 2 + ...CPP20ServiceUtils-TransactionEvent.test.ts | 8 +- .../ocpp/2.0/OCPP20VariableManager.test.ts | 26 ++- .../ocpp/auth/OCPPAuthIntegration.test.ts | 51 +++-- .../auth/adapters/OCPP20AuthAdapter.test.ts | 1 + .../factories/AuthComponentFactory.test.ts | 23 +-- .../auth/strategies/LocalAuthStrategy.test.ts | 13 ++ .../ui-server/UIHttpServer.test.ts | 104 +++++----- .../ui-server/UIServerSecurity.test.ts | 32 +-- 20 files changed, 645 insertions(+), 294 deletions(-) diff --git a/tests/charging-station/ChargingStation-Configuration.test.ts b/tests/charging-station/ChargingStation-Configuration.test.ts index 83de2259..089715ac 100644 --- a/tests/charging-station/ChargingStation-Configuration.test.ts +++ b/tests/charging-station/ChargingStation-Configuration.test.ts @@ -9,11 +9,9 @@ import type { ChargingStation } from '../../src/charging-station/ChargingStation import { AvailabilityType, RegistrationStatusEnumType } from '../../src/types/index.js' import { standardCleanup, withMockTimers } from '../helpers/TestLifecycleHelpers.js' +import { TEST_HEARTBEAT_INTERVAL_MS, TEST_ONE_HOUR_MS } from './ChargingStationTestConstants.js' import { cleanupChargingStation, createMockChargingStation } from './ChargingStationTestUtils.js' -// Alias for tests that reference createRealChargingStation -const createRealChargingStation = createMockChargingStation - await describe('ChargingStation Configuration Management', async () => { // ===== B02/B03 BOOT NOTIFICATION BEHAVIOR TESTS ===== // These tests verify behavioral requirements, not just state detection @@ -214,7 +212,7 @@ await describe('ChargingStation Configuration Management', async () => { expect(pendingStation.station.inPendingState()).toBe(true) expect(rejectedStation.station.inRejectedState()).toBe(true) expect(pendingStation.station.getHeartbeatInterval()).toBe(60000) - expect(rejectedStation.station.getHeartbeatInterval()).toBe(3600000) + expect(rejectedStation.station.getHeartbeatInterval()).toBe(TEST_ONE_HOUR_MS) // Cleanup cleanupChargingStation(pendingStation.station) @@ -259,7 +257,7 @@ await describe('ChargingStation Configuration Management', async () => { await it('should return heartbeat interval in milliseconds', () => { // Arrange - create station with 60 second heartbeat - const result = createRealChargingStation({ heartbeatInterval: 60 }) + const result = createMockChargingStation({ heartbeatInterval: 60 }) station = result.station // Act & Assert - should convert seconds to milliseconds @@ -268,7 +266,7 @@ await describe('ChargingStation Configuration Management', async () => { await it('should return default heartbeat interval when not explicitly configured', () => { // Arrange - use default heartbeat interval (TEST_HEARTBEAT_INTERVAL_SECONDS = 60) - const result = createRealChargingStation() + const result = createMockChargingStation() station = result.station // Act & Assert - default 60s * 1000 = 60000ms @@ -277,16 +275,16 @@ await describe('ChargingStation Configuration Management', async () => { await it('should return connection timeout in milliseconds', () => { // Arrange - const result = createRealChargingStation() + const result = createMockChargingStation() station = result.station // Act & Assert - default connection timeout is 30 seconds - expect(station.getConnectionTimeout()).toBe(30000) + expect(station.getConnectionTimeout()).toBe(TEST_HEARTBEAT_INTERVAL_MS) }) await it('should return authorize remote TX requests as boolean', () => { // Arrange - create station which defaults to false for AuthorizeRemoteTxRequests - const result = createRealChargingStation() + const result = createMockChargingStation() station = result.station // Act & Assert - getAuthorizeRemoteTxRequests returns boolean @@ -296,7 +294,7 @@ await describe('ChargingStation Configuration Management', async () => { await it('should return local auth list enabled as boolean', () => { // Arrange - const result = createRealChargingStation() + const result = createMockChargingStation() station = result.station // Act & Assert - getLocalAuthListEnabled returns boolean @@ -308,7 +306,7 @@ await describe('ChargingStation Configuration Management', async () => { await it('should call saveOcppConfiguration without throwing', () => { // Arrange - const result = createRealChargingStation() + const result = createMockChargingStation() station = result.station // Act & Assert - should not throw @@ -317,7 +315,7 @@ await describe('ChargingStation Configuration Management', async () => { await it('should have ocppConfiguration object with configurationKey array', () => { // Arrange - const result = createRealChargingStation() + const result = createMockChargingStation() station = result.station // Act & Assert - configuration structure should be present @@ -330,13 +328,13 @@ await describe('ChargingStation Configuration Management', async () => { await it('should allow updating heartbeat interval', () => { // Arrange - create with 60 second interval - const result = createRealChargingStation({ heartbeatInterval: 60 }) + const result = createMockChargingStation({ heartbeatInterval: 60 }) station = result.station const initialInterval = station.getHeartbeatInterval() expect(initialInterval).toBe(60000) // Act - simulate configuration change by creating new station with different interval - const result2 = createRealChargingStation({ heartbeatInterval: 120 }) + const result2 = createMockChargingStation({ heartbeatInterval: 120 }) const station2 = result2.station // Assert - different configurations have different intervals @@ -349,7 +347,7 @@ await describe('ChargingStation Configuration Management', async () => { await it('should support setSupervisionUrl method if available', () => { // Arrange - const result = createRealChargingStation() + const result = createMockChargingStation() station = result.station // Act & Assert - setSupervisionUrl should be a function if available @@ -365,7 +363,7 @@ await describe('ChargingStation Configuration Management', async () => { await it('should have template file reference', () => { // Arrange - const result = createRealChargingStation({ templateFile: 'custom-template.json' }) + const result = createMockChargingStation({ templateFile: 'custom-template.json' }) station = result.station // Act & Assert - station info should have template reference @@ -374,7 +372,7 @@ await describe('ChargingStation Configuration Management', async () => { await it('should have hashId for configuration persistence', () => { // Arrange - const result = createRealChargingStation() + const result = createMockChargingStation() station = result.station // Act & Assert - hashId is used for configuration file naming @@ -384,7 +382,7 @@ await describe('ChargingStation Configuration Management', async () => { await it('should preserve station info properties for persistence', () => { // Arrange - const result = createRealChargingStation({ + const result = createMockChargingStation({ baseName: 'PERSIST-CS', index: 5, }) @@ -399,7 +397,7 @@ await describe('ChargingStation Configuration Management', async () => { await it('should track configuration file path via templateFile', () => { // Arrange - const result = createRealChargingStation() + const result = createMockChargingStation() station = result.station // Act & Assert - templateFile is used to track configuration source @@ -409,7 +407,7 @@ await describe('ChargingStation Configuration Management', async () => { await it('should use mocked file system without real file writes', () => { // Arrange - const result = createRealChargingStation() + const result = createMockChargingStation() station = result.station const mocks = result.mocks diff --git a/tests/charging-station/ChargingStation-Connectors.test.ts b/tests/charging-station/ChargingStation-Connectors.test.ts index 354eb6b6..4ea1112f 100644 --- a/tests/charging-station/ChargingStation-Connectors.test.ts +++ b/tests/charging-station/ChargingStation-Connectors.test.ts @@ -9,13 +9,11 @@ import type { ChargingStation } from '../../src/charging-station/ChargingStation import { RegistrationStatusEnumType } from '../../src/types/index.js' import { standardCleanup } from '../helpers/TestLifecycleHelpers.js' +import { TEST_ONE_HOUR_MS } from './ChargingStationTestConstants.js' import { cleanupChargingStation, createMockChargingStation } from './ChargingStationTestUtils.js' -// Alias for tests that reference createRealChargingStation -const createRealChargingStation = createMockChargingStation - await describe('ChargingStation Connector and EVSE State', async () => { - await describe('Connector Query Tests', async () => { + await describe('Connector Query', async () => { let station: ChargingStation | undefined beforeEach(() => { @@ -98,7 +96,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { }) }) - await describe('Connector 0 (Shared Power) Tests', async () => { + await describe('Connector 0 (Shared Power)', async () => { let station: ChargingStation | undefined beforeEach(() => { @@ -129,7 +127,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { }) }) - await describe('EVSE Query Tests (non-EVSE mode)', async () => { + await describe('EVSE Query (non-EVSE mode)', async () => { let station: ChargingStation | undefined beforeEach(() => { @@ -165,7 +163,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { }) }) - await describe('EVSE Mode Tests', async () => { + await describe('EVSE Mode', async () => { let station: ChargingStation | undefined beforeEach(() => { @@ -307,7 +305,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { await it('should return true for inAcceptedState when boot status is ACCEPTED', () => { // Arrange - const result = createRealChargingStation({ + const result = createMockChargingStation({ bootNotificationStatus: RegistrationStatusEnumType.ACCEPTED, }) station = result.station @@ -321,7 +319,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { await it('should return true for inPendingState when boot status is PENDING', () => { // Arrange - const result = createRealChargingStation({ + const result = createMockChargingStation({ bootNotificationStatus: RegistrationStatusEnumType.PENDING, }) station = result.station @@ -335,7 +333,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { await it('should return true for inRejectedState when boot status is REJECTED', () => { // Arrange - const result = createRealChargingStation({ + const result = createMockChargingStation({ bootNotificationStatus: RegistrationStatusEnumType.REJECTED, }) station = result.station @@ -349,7 +347,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { await it('should return true for inUnknownState when boot notification response is null', () => { // Arrange - create station with default accepted status, then delete the response - const result = createRealChargingStation({ connectorsCount: 1 }) + const result = createMockChargingStation({ connectorsCount: 1 }) station = result.station // Act - simulate unknown state by clearing boot notification response @@ -364,7 +362,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { await it('should allow state transitions from PENDING to ACCEPTED', () => { // Arrange - const result = createRealChargingStation({ + const result = createMockChargingStation({ bootNotificationStatus: RegistrationStatusEnumType.PENDING, }) station = result.station @@ -381,7 +379,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { await it('should allow state transitions from PENDING to REJECTED', () => { // Arrange - const result = createRealChargingStation({ + const result = createMockChargingStation({ bootNotificationStatus: RegistrationStatusEnumType.PENDING, }) station = result.station @@ -416,7 +414,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { station = result.station const reservation = { connectorId: 1, - expiryDate: new Date(Date.now() + 3600000), // 1 hour from now + expiryDate: new Date(Date.now() + TEST_ONE_HOUR_MS), // 1 hour from now idTag: 'test-tag-1', reservationId: 101, } @@ -437,13 +435,13 @@ await describe('ChargingStation Connector and EVSE State', async () => { station = result.station const firstReservation = { connectorId: 1, - expiryDate: new Date(Date.now() + 3600000), + expiryDate: new Date(Date.now() + TEST_ONE_HOUR_MS), idTag: 'tag-1', reservationId: 201, } const secondReservation = { connectorId: 2, - expiryDate: new Date(Date.now() + 3600000), + expiryDate: new Date(Date.now() + TEST_ONE_HOUR_MS), idTag: 'tag-2', reservationId: 201, // Same ID } @@ -465,7 +463,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { station = result.station const reservation = { connectorId: 1, - expiryDate: new Date(Date.now() + 3600000), + expiryDate: new Date(Date.now() + TEST_ONE_HOUR_MS), idTag: 'test-tag-expired', reservationId: 301, } @@ -486,7 +484,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { station = result.station const reservation = { connectorId: 1, - expiryDate: new Date(Date.now() + 3600000), + expiryDate: new Date(Date.now() + TEST_ONE_HOUR_MS), idTag: 'test-tag-replace', reservationId: 401, } @@ -507,7 +505,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { station = result.station const reservation = { connectorId: 2, - expiryDate: new Date(Date.now() + 3600000), + expiryDate: new Date(Date.now() + TEST_ONE_HOUR_MS), idTag: 'query-test-id', reservationId: 501, } @@ -528,7 +526,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { station = result.station const reservation = { connectorId: 1, - expiryDate: new Date(Date.now() + 3600000), + expiryDate: new Date(Date.now() + TEST_ONE_HOUR_MS), idTag: 'search-by-tag', reservationId: 601, } @@ -549,7 +547,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { station = result.station const reservation = { connectorId: 2, - expiryDate: new Date(Date.now() + 3600000), + expiryDate: new Date(Date.now() + TEST_ONE_HOUR_MS), idTag: 'connector-search', reservationId: 701, } @@ -570,7 +568,7 @@ await describe('ChargingStation Connector and EVSE State', async () => { station = result.station const reservation = { connectorId: 1, - expiryDate: new Date(Date.now() + 3600000), + expiryDate: new Date(Date.now() + TEST_ONE_HOUR_MS), idTag: 'reservable-check', reservationId: 801, } @@ -613,13 +611,13 @@ await describe('ChargingStation Connector and EVSE State', async () => { station = result.station const reservation1 = { connectorId: 1, - expiryDate: new Date(Date.now() + 3600000), + expiryDate: new Date(Date.now() + TEST_ONE_HOUR_MS), idTag: 'multi-test-1', reservationId: 1001, } const reservation2 = { connectorId: 2, - expiryDate: new Date(Date.now() + 3600000), + expiryDate: new Date(Date.now() + TEST_ONE_HOUR_MS), idTag: 'multi-test-2', reservationId: 1002, } diff --git a/tests/charging-station/ChargingStation-Resilience.test.ts b/tests/charging-station/ChargingStation-Resilience.test.ts index e4756de0..c36966d2 100644 --- a/tests/charging-station/ChargingStation-Resilience.test.ts +++ b/tests/charging-station/ChargingStation-Resilience.test.ts @@ -9,6 +9,7 @@ import type { ChargingStation } from '../../src/charging-station/ChargingStation import { RegistrationStatusEnumType } from '../../src/types/index.js' import { standardCleanup } from '../helpers/TestLifecycleHelpers.js' +import { TEST_HEARTBEAT_INTERVAL_MS } from './ChargingStationTestConstants.js' import { cleanupChargingStation, createMockChargingStation } from './ChargingStationTestUtils.js' await describe('ChargingStation Error Recovery and Resilience', async () => { @@ -251,7 +252,7 @@ await describe('ChargingStation Error Recovery and Resilience', async () => { // Set up a heartbeat timer (simulated) station.heartbeatSetInterval = setInterval(() => { /* empty */ - }, 30000) as unknown as NodeJS.Timeout + }, TEST_HEARTBEAT_INTERVAL_MS) as unknown as NodeJS.Timeout // Act - Cleanup station cleanupChargingStation(station) diff --git a/tests/charging-station/ChargingStation-Transactions.test.ts b/tests/charging-station/ChargingStation-Transactions.test.ts index 20e2ae57..e7b6915d 100644 --- a/tests/charging-station/ChargingStation-Transactions.test.ts +++ b/tests/charging-station/ChargingStation-Transactions.test.ts @@ -8,11 +8,11 @@ import { afterEach, beforeEach, describe, it } from 'node:test' import type { ChargingStation } from '../../src/charging-station/ChargingStation.js' import { standardCleanup, withMockTimers } from '../helpers/TestLifecycleHelpers.js' -import { TEST_ID_TAG } from './ChargingStationTestConstants.js' +import { TEST_HEARTBEAT_INTERVAL_MS, TEST_ID_TAG } from './ChargingStationTestConstants.js' import { cleanupChargingStation, createMockChargingStation } from './ChargingStationTestUtils.js' await describe('ChargingStation Transaction Management', async () => { - await describe('Transaction Query Tests', async () => { + await describe('Transaction Query', async () => { let station: ChargingStation | undefined beforeEach(() => { @@ -175,7 +175,7 @@ await describe('ChargingStation Transaction Management', async () => { }) }) - await describe('Energy Meter Tests', async () => { + await describe('Energy Meter', async () => { let station: ChargingStation | undefined beforeEach(() => { @@ -466,7 +466,10 @@ await describe('ChargingStation Transaction Management', async () => { await it('should create interval when startHeartbeat() is called with valid interval', async t => { await withMockTimers(t, ['setInterval'], () => { // Arrange - const result = createMockChargingStation({ connectorsCount: 1, heartbeatInterval: 30000 }) + const result = createMockChargingStation({ + connectorsCount: 1, + heartbeatInterval: TEST_HEARTBEAT_INTERVAL_MS, + }) station = result.station // Act @@ -481,7 +484,10 @@ await describe('ChargingStation Transaction Management', async () => { await it('should restart heartbeat interval when restartHeartbeat() is called', async t => { await withMockTimers(t, ['setInterval'], () => { // Arrange - const result = createMockChargingStation({ connectorsCount: 1, heartbeatInterval: 30000 }) + const result = createMockChargingStation({ + connectorsCount: 1, + heartbeatInterval: TEST_HEARTBEAT_INTERVAL_MS, + }) station = result.station station.startHeartbeat() const firstInterval = station.heartbeatSetInterval @@ -500,7 +506,10 @@ await describe('ChargingStation Transaction Management', async () => { await it('should not create heartbeat interval if already started', async t => { await withMockTimers(t, ['setInterval'], () => { // Arrange - const result = createMockChargingStation({ connectorsCount: 1, heartbeatInterval: 30000 }) + const result = createMockChargingStation({ + connectorsCount: 1, + heartbeatInterval: TEST_HEARTBEAT_INTERVAL_MS, + }) station = result.station station.startHeartbeat() const firstInterval = station.heartbeatSetInterval diff --git a/tests/charging-station/ChargingStation.test.ts b/tests/charging-station/ChargingStation.test.ts index 47f09ac9..9f86f080 100644 --- a/tests/charging-station/ChargingStation.test.ts +++ b/tests/charging-station/ChargingStation.test.ts @@ -9,7 +9,7 @@ * - ChargingStation-Configuration.test.ts: boot notification, config persistence, WebSocket, error handling */ import { expect } from '@std/expect' -import { afterEach, describe, it } from 'node:test' +import { afterEach, beforeEach, describe, it } from 'node:test' import type { ChargingStation } from '../../src/charging-station/ChargingStation.js' @@ -17,6 +17,7 @@ import { RegistrationStatusEnumType } from '../../src/types/index.js' import { standardCleanup } from '../helpers/TestLifecycleHelpers.js' import { TEST_ID_TAG, + TEST_ONE_HOUR_MS, TEST_TRANSACTION_ENERGY_WH, TEST_TRANSACTION_ID, } from './ChargingStationTestConstants.js' @@ -29,8 +30,8 @@ import { WebSocketReadyState, } from './ChargingStationTestUtils.js' -await describe('ChargingStation Integration Tests', async () => { - await describe('Test Utilities Verification', async () => { +await describe('ChargingStation', async () => { + await describe('Test Utilities', async () => { afterEach(() => { standardCleanup() }) @@ -116,15 +117,19 @@ await describe('ChargingStation Integration Tests', async () => { }) }) - await describe('Cross-Domain Integration', async () => { + await describe('Cross-Domain', async () => { + let station: ChargingStation | undefined + + beforeEach(() => { + station = undefined + }) + afterEach(() => { standardCleanup() if (station != null) { cleanupChargingStation(station) } }) - let station: ChargingStation | undefined - await it('should support full lifecycle with transactions', async () => { // Create station const result = createMockChargingStation({ connectorsCount: 2 }) @@ -198,7 +203,7 @@ await describe('ChargingStation Integration Tests', async () => { // Add reservation const reservation = { connectorId: 1, - expiryDate: new Date(Date.now() + 3600000), + expiryDate: new Date(Date.now() + TEST_ONE_HOUR_MS), idTag: 'RESERVATION-TAG', reservationId: 1, } @@ -246,7 +251,7 @@ await describe('ChargingStation Integration Tests', async () => { }) }) - await describe('Mock Reset Verification', async () => { + await describe('Mock Reset', async () => { await it('should reset singleton mocks between tests', () => { // First test - create and use mocks const result1 = createMockChargingStation() diff --git a/tests/charging-station/ChargingStationTestConstants.ts b/tests/charging-station/ChargingStationTestConstants.ts index 08608e44..d69ec4e8 100644 --- a/tests/charging-station/ChargingStationTestConstants.ts +++ b/tests/charging-station/ChargingStationTestConstants.ts @@ -20,6 +20,9 @@ export const TEST_CHARGING_STATION_HASH_ID = 'cs-test-hash-001' * Test values for timing-related configuration and expectations */ export const TEST_HEARTBEAT_INTERVAL_SECONDS = 60 +export const TEST_HEARTBEAT_INTERVAL_MS = 30000 +export const TEST_AUTHORIZATION_TIMEOUT_MS = 30000 +export const TEST_ONE_HOUR_MS = 3600000 /** * Charging Station Information diff --git a/tests/charging-station/ConfigurationKeyUtils.test.ts b/tests/charging-station/ConfigurationKeyUtils.test.ts index f0b6191a..b41f6fac 100644 --- a/tests/charging-station/ConfigurationKeyUtils.test.ts +++ b/tests/charging-station/ConfigurationKeyUtils.test.ts @@ -27,49 +27,75 @@ await describe('ConfigurationKeyUtils', async () => { standardCleanup() mock.restoreAll() }) - await describe('getConfigurationKey()', async () => { + await describe('GetConfigurationKey', async () => { await it('should return undefined when configurationKey array is missing', () => { + // Arrange const { station: cs } = createMockChargingStation() // Simulate missing configurationKey array cs.ocppConfiguration = {} as Partial + + // Act & Assert expect(getConfigurationKey(cs, TEST_KEY_1)).toBeUndefined() }) await it('should find existing key (case-sensitive)', () => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, TEST_KEY_1, VALUE_A, undefined, { save: false }) + + // Act const k = getConfigurationKey(cs, TEST_KEY_1) + + // Assert expect(k?.key).toBe(TEST_KEY_1) expect(k?.value).toBe(VALUE_A) }) await it('should respect case sensitivity (no match)', () => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, MIXED_CASE_KEY, VALUE_A, undefined, { save: false }) + + // Act & Assert expect(getConfigurationKey(cs, MIXED_CASE_KEY.toLowerCase())).toBeUndefined() }) await it('should support caseInsensitive lookup', () => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, MIXED_CASE_KEY, VALUE_A, undefined, { save: false }) + + // Act const k = getConfigurationKey(cs, MIXED_CASE_KEY.toLowerCase(), true) + + // Assert expect(k?.key).toBe(MIXED_CASE_KEY) }) }) - await describe('addConfigurationKey()', async () => { + await describe('AddConfigurationKey', async () => { await it('should no-op when configurationKey array missing', () => { + // Arrange const { station: cs } = createMockChargingStation() // Simulate missing configurationKey array cs.ocppConfiguration = {} as Partial + + // Act addConfigurationKey(cs, TEST_KEY_1, VALUE_A) + + // Assert expect(getConfigurationKey(cs, TEST_KEY_1)).toBeUndefined() }) await it('should add new key with default options', () => { + // Arrange const { station: cs } = createMockChargingStation() + + // Act addConfigurationKey(cs, TEST_KEY_1, VALUE_A, undefined, { save: false }) const k = getConfigurationKey(cs, TEST_KEY_1) + + // Assert expect(k).toBeDefined() expect(k?.value).toBe(VALUE_A) // defaults @@ -79,7 +105,10 @@ await describe('ConfigurationKeyUtils', async () => { }) await it('should add new key with custom options', () => { + // Arrange const { station: cs } = createMockChargingStation() + + // Act addConfigurationKey( cs, TEST_KEY_1, @@ -88,15 +117,20 @@ await describe('ConfigurationKeyUtils', async () => { { save: false } ) const k = getConfigurationKey(cs, TEST_KEY_1) + + // Assert expect(k?.readonly).toBe(true) expect(k?.reboot).toBe(true) expect(k?.visible).toBe(false) }) await it('should log error and not overwrite value when key exists and overwrite=false', t => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, TEST_KEY_1, VALUE_A, { readonly: false }, { save: false }) const errorMock = t.mock.method(logger, 'error') + + // Act // Attempt to add same key with different value and option change addConfigurationKey( cs, @@ -106,6 +140,8 @@ await describe('ConfigurationKeyUtils', async () => { { overwrite: false, save: false } ) const k = getConfigurationKey(cs, TEST_KEY_1) + + // Assert // value unchanged expect(k?.value).toBe(VALUE_A) // options updated only where differing (all provided differ) @@ -116,6 +152,7 @@ await describe('ConfigurationKeyUtils', async () => { }) await it('should log error and leave key untouched when identical options & value attempted (overwrite=false)', t => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey( cs, @@ -125,6 +162,8 @@ await describe('ConfigurationKeyUtils', async () => { { save: false } ) const errorMock = t.mock.method(logger, 'error') + + // Act // Attempt to add same key with identical value and options addConfigurationKey( cs, @@ -134,6 +173,8 @@ await describe('ConfigurationKeyUtils', async () => { { overwrite: false, save: false } ) const k = getConfigurationKey(cs, TEST_KEY_1) + + // Assert expect(k?.value).toBe(VALUE_A) expect(k?.readonly).toBe(true) expect(k?.reboot).toBe(false) @@ -142,8 +183,11 @@ await describe('ConfigurationKeyUtils', async () => { }) await it('should overwrite existing key value and options when overwrite=true', () => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, TEST_KEY_1, VALUE_A, { readonly: false }, { save: false }) + + // Act addConfigurationKey( cs, TEST_KEY_1, @@ -152,6 +196,8 @@ await describe('ConfigurationKeyUtils', async () => { { overwrite: true, save: false } ) const k = getConfigurationKey(cs, TEST_KEY_1) + + // Assert expect(k?.value).toBe(VALUE_B) expect(k?.readonly).toBe(true) expect(k?.reboot).toBe(true) @@ -159,8 +205,11 @@ await describe('ConfigurationKeyUtils', async () => { }) await it('should caseInsensitive overwrite update existing differently cased key', () => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, MIXED_CASE_KEY, VALUE_A, undefined, { save: false }) + + // Act addConfigurationKey( cs, MIXED_CASE_KEY.toLowerCase(), @@ -169,35 +218,50 @@ await describe('ConfigurationKeyUtils', async () => { { caseInsensitive: true, overwrite: true, save: false } ) const k = getConfigurationKey(cs, MIXED_CASE_KEY) + + // Assert expect(k?.value).toBe(VALUE_B) expect(k?.readonly).toBe(true) }) await it('should case-insensitive false create separate key with different case', () => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, MIXED_CASE_KEY, VALUE_A, undefined, { save: false }) + + // Act addConfigurationKey(cs, MIXED_CASE_KEY.toLowerCase(), VALUE_B, undefined, { overwrite: true, save: false, }) const orig = getConfigurationKey(cs, MIXED_CASE_KEY) const second = getConfigurationKey(cs, MIXED_CASE_KEY.toLowerCase()) + + // Assert expect(orig).toBeDefined() expect(second).toBeDefined() expect(orig).not.toBe(second) }) await it('should call saveOcppConfiguration when params.save=true (new key)', t => { + // Arrange const { station: cs } = createMockChargingStation() const saveMock = t.mock.method(cs, 'saveOcppConfiguration') + + // Act addConfigurationKey(cs, TEST_KEY_1, VALUE_A, undefined, { save: true }) + + // Assert expect(saveMock.mock.calls.length).toBe(1) }) await it('should call saveOcppConfiguration when overwriting existing key and save=true', t => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, TEST_KEY_1, VALUE_A, undefined, { save: false }) const saveMock = t.mock.method(cs, 'saveOcppConfiguration') + + // Act addConfigurationKey( cs, TEST_KEY_1, @@ -205,66 +269,103 @@ await describe('ConfigurationKeyUtils', async () => { { readonly: true }, { overwrite: true, save: true } ) + + // Assert expect(saveMock.mock.calls.length).toBe(1) }) }) - await describe('setConfigurationKeyValue()', async () => { + await describe('SetConfigurationKeyValue', async () => { await it('should return undefined and log error for non-existing key', t => { + // Arrange const { station: cs } = createMockChargingStation() const errorMock = t.mock.method(logger, 'error') + + // Act const res = setConfigurationKeyValue(cs, TEST_KEY_1, VALUE_A) + + // Assert expect(res).toBeUndefined() expect(errorMock.mock.calls.length).toBe(1) }) await it('should return undefined without logging when configurationKey array missing', t => { + // Arrange const { station: cs } = createMockChargingStation() // Simulate missing configurationKey array cs.ocppConfiguration = {} as Partial const errorMock = t.mock.method(logger, 'error') + + // Act const res = setConfigurationKeyValue(cs, TEST_KEY_1, VALUE_A) + + // Assert expect(res).toBeUndefined() expect(errorMock.mock.calls.length).toBe(0) }) await it('should update existing key value and save', t => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, TEST_KEY_1, VALUE_A, undefined, { save: false }) const saveMock = t.mock.method(cs, 'saveOcppConfiguration') + + // Act const updated = setConfigurationKeyValue(cs, TEST_KEY_1, VALUE_B) + + // Assert expect(updated?.value).toBe(VALUE_B) expect(saveMock.mock.calls.length).toBe(1) }) await it('should caseInsensitive value update work', () => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, MIXED_CASE_KEY, VALUE_A, undefined, { save: false }) + + // Act const updated = setConfigurationKeyValue(cs, MIXED_CASE_KEY.toLowerCase(), VALUE_B, true) + + // Assert expect(updated?.value).toBe(VALUE_B) }) }) - await describe('deleteConfigurationKey()', async () => { + await describe('DeleteConfigurationKey', async () => { await it('should return undefined when configurationKey array missing', () => { + // Arrange const { station: cs } = createMockChargingStation() // Simulate missing configurationKey array cs.ocppConfiguration = {} as Partial + + // Act const res = deleteConfigurationKey(cs, TEST_KEY_1) + + // Assert expect(res).toBeUndefined() }) await it('should return undefined when key does not exist', () => { + // Arrange const { station: cs } = createMockChargingStation() + + // Act const res = deleteConfigurationKey(cs, TEST_KEY_1) + + // Assert expect(res).toBeUndefined() }) await it('should delete existing key and save by default', t => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, TEST_KEY_1, VALUE_A, undefined, { save: false }) const saveMock = t.mock.method(cs, 'saveOcppConfiguration') + + // Act const deleted = deleteConfigurationKey(cs, TEST_KEY_1) + + // Assert expect(Array.isArray(deleted)).toBe(true) expect(deleted).toHaveLength(1) expect(deleted?.[0].key).toBe(TEST_KEY_1) @@ -273,21 +374,31 @@ await describe('ConfigurationKeyUtils', async () => { }) await it('should not save when params.save=false', t => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, TEST_KEY_1, VALUE_A, undefined, { save: false }) const saveMock = t.mock.method(cs, 'saveOcppConfiguration') + + // Act const deleted = deleteConfigurationKey(cs, TEST_KEY_1, { save: false }) + + // Assert expect(deleted).toHaveLength(1) expect(saveMock.mock.calls.length).toBe(0) }) await it('should caseInsensitive deletion remove key with different case', () => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, MIXED_CASE_KEY, VALUE_A, undefined, { save: false }) + + // Act const deleted = deleteConfigurationKey(cs, MIXED_CASE_KEY.toLowerCase(), { caseInsensitive: true, save: false, }) + + // Assert expect(deleted).toHaveLength(1) expect(getConfigurationKey(cs, MIXED_CASE_KEY)).toBeUndefined() }) @@ -295,11 +406,16 @@ await describe('ConfigurationKeyUtils', async () => { await describe('Combined scenarios', async () => { await it('should add then set then delete lifecycle', () => { + // Arrange const { station: cs } = createMockChargingStation() addConfigurationKey(cs, TEST_KEY_1, VALUE_A, { readonly: false }, { save: false }) + + // Act const setRes = setConfigurationKeyValue(cs, TEST_KEY_1, VALUE_B) - expect(setRes?.value).toBe(VALUE_B) const delRes = deleteConfigurationKey(cs, TEST_KEY_1, { save: false }) + + // Assert + expect(setRes?.value).toBe(VALUE_B) expect(delRes).toHaveLength(1) expect(getConfigurationKey(cs, TEST_KEY_1)).toBeUndefined() }) diff --git a/tests/charging-station/Helpers.test.ts b/tests/charging-station/Helpers.test.ts index 8e9cd75f..115da9bd 100644 --- a/tests/charging-station/Helpers.test.ts +++ b/tests/charging-station/Helpers.test.ts @@ -4,7 +4,7 @@ */ import { expect } from '@std/expect' -import { afterEach, describe, it } from 'node:test' +import { afterEach, beforeEach, describe, it } from 'node:test' import { checkChargingStationState, @@ -40,8 +40,13 @@ import { } from './ChargingStationTestUtils.js' await describe('Helpers', async () => { - const baseName = 'CS-TEST' - const chargingStationTemplate = createMockChargingStationTemplate(baseName) + let baseName: string + let chargingStationTemplate: ChargingStationTemplate + + beforeEach(() => { + baseName = 'CS-TEST' + chargingStationTemplate = createMockChargingStationTemplate(baseName) + }) afterEach(() => { standardCleanup() @@ -57,55 +62,70 @@ await describe('Helpers', async () => { }) as Reservation await it('should return formatted charging station ID with index', () => { + // Arrange & Act & Assert expect(getChargingStationId(1, chargingStationTemplate)).toBe(`${baseName}-00001`) }) await it('should return consistent hash ID for same template and index', () => { + // Arrange & Act & Assert expect(getHashId(1, chargingStationTemplate)).toBe( 'b4b1e8ec4fca79091d99ea9a7ea5901548010e6c0e98be9296f604b9d68734444dfdae73d7d406b6124b42815214d088' ) }) await it('should throw when stationInfo is missing', () => { + // Arrange // For validation edge cases, we need to manually create invalid states // since the factory is designed to create valid configurations const { station: stationNoInfo } = createMockChargingStation({ baseName }) stationNoInfo.stationInfo = undefined + + // Act & Assert expect(() => { validateStationInfo(stationNoInfo) }).toThrow(new BaseError('Missing charging station information')) }) await it('should throw when stationInfo is empty object', () => { + // Arrange // For validation edge cases, manually create empty stationInfo const { station: stationEmptyInfo } = createMockChargingStation({ baseName }) stationEmptyInfo.stationInfo = {} as ChargingStationInfo + + // Act & Assert expect(() => { validateStationInfo(stationEmptyInfo) }).toThrow(new BaseError('Missing charging station information')) }) await it('should throw when chargingStationId is undefined', () => { + // Arrange const { station: stationMissingId } = createMockChargingStation({ baseName, stationInfo: { baseName, chargingStationId: undefined }, }) + + // Act & Assert expect(() => { validateStationInfo(stationMissingId) }).toThrow(new BaseError('Missing chargingStationId in stationInfo properties')) }) await it('should throw when chargingStationId is empty string', () => { + // Arrange const { station: stationEmptyId } = createMockChargingStation({ baseName, stationInfo: { baseName, chargingStationId: '' }, }) + + // Act & Assert expect(() => { validateStationInfo(stationEmptyId) }).toThrow(new BaseError('Missing chargingStationId in stationInfo properties')) }) await it('should throw when hashId is undefined', () => { + // Arrange const { station: stationMissingHash } = createMockChargingStation({ baseName, stationInfo: { @@ -114,12 +134,15 @@ await describe('Helpers', async () => { hashId: undefined, }, }) + + // Act & Assert expect(() => { validateStationInfo(stationMissingHash) }).toThrow(new BaseError(`${baseName}-00001: Missing hashId in stationInfo properties`)) }) await it('should throw when hashId is empty string', () => { + // Arrange const { station: stationEmptyHash } = createMockChargingStation({ baseName, stationInfo: { @@ -128,12 +151,15 @@ await describe('Helpers', async () => { hashId: '', }, }) + + // Act & Assert expect(() => { validateStationInfo(stationEmptyHash) }).toThrow(new BaseError(`${baseName}-00001: Missing hashId in stationInfo properties`)) }) await it('should throw when templateIndex is undefined', () => { + // Arrange const { station: stationMissingTemplate } = createMockChargingStation({ baseName, stationInfo: { @@ -143,12 +169,15 @@ await describe('Helpers', async () => { templateIndex: undefined, }, }) + + // Act & Assert expect(() => { validateStationInfo(stationMissingTemplate) }).toThrow(new BaseError(`${baseName}-00001: Missing templateIndex in stationInfo properties`)) }) await it('should throw when templateIndex is zero', () => { + // Arrange const { station: stationInvalidTemplate } = createMockChargingStation({ baseName, stationInfo: { @@ -158,6 +187,8 @@ await describe('Helpers', async () => { templateIndex: 0, }, }) + + // Act & Assert expect(() => { validateStationInfo(stationInvalidTemplate) }).toThrow( @@ -166,6 +197,7 @@ await describe('Helpers', async () => { }) await it('should throw when templateName is undefined', () => { + // Arrange const { station: stationMissingName } = createMockChargingStation({ baseName, stationInfo: { @@ -176,12 +208,15 @@ await describe('Helpers', async () => { templateName: undefined, }, }) + + // Act & Assert expect(() => { validateStationInfo(stationMissingName) }).toThrow(new BaseError(`${baseName}-00001: Missing templateName in stationInfo properties`)) }) await it('should throw when templateName is empty string', () => { + // Arrange const { station: stationEmptyName } = createMockChargingStation({ baseName, stationInfo: { @@ -192,12 +227,15 @@ await describe('Helpers', async () => { templateName: '', }, }) + + // Act & Assert expect(() => { validateStationInfo(stationEmptyName) }).toThrow(new BaseError(`${baseName}-00001: Missing templateName in stationInfo properties`)) }) await it('should throw when maximumPower is undefined', () => { + // Arrange const { station: stationMissingPower } = createMockChargingStation({ baseName, stationInfo: { @@ -209,12 +247,15 @@ await describe('Helpers', async () => { templateName: 'test-template.json', }, }) + + // Act & Assert expect(() => { validateStationInfo(stationMissingPower) }).toThrow(new BaseError(`${baseName}-00001: Missing maximumPower in stationInfo properties`)) }) await it('should throw when maximumPower is zero', () => { + // Arrange const { station: stationInvalidPower } = createMockChargingStation({ baseName, stationInfo: { @@ -226,6 +267,8 @@ await describe('Helpers', async () => { templateName: 'test-template.json', }, }) + + // Act & Assert expect(() => { validateStationInfo(stationInvalidPower) }).toThrow( @@ -234,6 +277,7 @@ await describe('Helpers', async () => { }) await it('should throw when maximumAmperage is undefined', () => { + // Arrange const { station: stationMissingAmperage } = createMockChargingStation({ baseName, stationInfo: { @@ -246,6 +290,8 @@ await describe('Helpers', async () => { templateName: 'test-template.json', }, }) + + // Act & Assert expect(() => { validateStationInfo(stationMissingAmperage) }).toThrow( @@ -254,6 +300,7 @@ await describe('Helpers', async () => { }) await it('should throw when maximumAmperage is zero', () => { + // Arrange const { station: stationInvalidAmperage } = createMockChargingStation({ baseName, stationInfo: { @@ -266,6 +313,8 @@ await describe('Helpers', async () => { templateName: 'test-template.json', }, }) + + // Act & Assert expect(() => { validateStationInfo(stationInvalidAmperage) }).toThrow( @@ -274,6 +323,7 @@ await describe('Helpers', async () => { }) await it('should pass validation with complete valid configuration', () => { + // Arrange const { station: validStation } = createMockChargingStation({ baseName, stationInfo: { @@ -286,12 +336,15 @@ await describe('Helpers', async () => { templateName: 'test-template.json', }, }) + + // Act & Assert expect(() => { validateStationInfo(validStation) }).not.toThrow() }) await it('should throw for OCPP 2.0 without EVSE configuration', () => { + // Arrange const { station: stationOcpp20 } = createMockChargingStation({ baseName, connectorsCount: 0, // Ensure no EVSEs are created @@ -307,6 +360,8 @@ await describe('Helpers', async () => { templateName: 'test-template.json', }, }) + + // Act & Assert expect(() => { validateStationInfo(stationOcpp20) }).toThrow( @@ -317,6 +372,7 @@ await describe('Helpers', async () => { }) await it('should throw for OCPP 2.0.1 without EVSE configuration', () => { + // Arrange const { station: stationOcpp201 } = createMockChargingStation({ baseName, connectorsCount: 0, // Ensure no EVSEs are created @@ -332,6 +388,8 @@ await describe('Helpers', async () => { templateName: 'test-template.json', }, }) + + // Act & Assert expect(() => { validateStationInfo(stationOcpp201) }).toThrow( @@ -342,39 +400,58 @@ await describe('Helpers', async () => { }) await it('should return false and warn when station is not started or starting', t => { + // Arrange const warnMock = t.mock.method(logger, 'warn') const { station: stationNotStarted } = createMockChargingStation({ baseName, started: false, starting: false, }) - expect(checkChargingStationState(stationNotStarted, 'log prefix |')).toBe(false) + + // Act + const result = checkChargingStationState(stationNotStarted, 'log prefix |') + + // Assert + expect(result).toBe(false) expect(warnMock.mock.calls.length).toBe(1) }) await it('should return true when station is starting', t => { + // Arrange const warnMock = t.mock.method(logger, 'warn') const { station: stationStarting } = createMockChargingStation({ baseName, started: false, starting: true, }) - expect(checkChargingStationState(stationStarting, 'log prefix |')).toBe(true) + + // Act + const result = checkChargingStationState(stationStarting, 'log prefix |') + + // Assert + expect(result).toBe(true) expect(warnMock.mock.calls.length).toBe(0) }) await it('should return true when station is started', t => { + // Arrange const warnMock = t.mock.method(logger, 'warn') const { station: stationStarted } = createMockChargingStation({ baseName, started: true, starting: false, }) - expect(checkChargingStationState(stationStarted, 'log prefix |')).toBe(true) + + // Act + const result = checkChargingStationState(stationStarted, 'log prefix |') + + // Assert + expect(result).toBe(true) expect(warnMock.mock.calls.length).toBe(0) }) await it('should return correct phase rotation value for connector and phase count', () => { + // Arrange & Act & Assert expect(getPhaseRotationValue(0, 0)).toBe('0.RST') expect(getPhaseRotationValue(1, 0)).toBe('1.NotApplicable') expect(getPhaseRotationValue(2, 0)).toBe('2.NotApplicable') @@ -390,13 +467,17 @@ await describe('Helpers', async () => { }) await it('should return -1 for undefined EVSEs and 0 for empty object', () => { + // Arrange & Act & Assert expect(getMaxNumberOfEvses(undefined)).toBe(-1) expect(getMaxNumberOfEvses({})).toBe(0) }) await it('should throw for undefined or empty template', t => { + // Arrange const warnMock = t.mock.method(logger, 'warn') const errorMock = t.mock.method(logger, 'error') + + // Act & Assert expect(() => { checkTemplate(undefined, 'log prefix |', 'test-template.json') }).toThrow(new BaseError('Failed to read charging station template file test-template.json')) @@ -412,7 +493,10 @@ await describe('Helpers', async () => { }) await it('should throw for undefined or empty configuration', t => { + // Arrange const errorMock = t.mock.method(logger, 'error') + + // Act & Assert expect(() => { checkConfiguration(undefined, 'log prefix |', 'configuration.json') }).toThrow( @@ -426,8 +510,11 @@ await describe('Helpers', async () => { }) await it('should warn and clear status when connector has predefined status', t => { + // Arrange const warnMock = t.mock.method(logger, 'warn') checkStationInfoConnectorStatus(1, {} as ConnectorStatus, 'log prefix |', 'test-template.json') + + // Act & Assert expect(warnMock.mock.calls.length).toBe(0) const connectorStatus = { status: ConnectorStatusEnum.Available, @@ -438,24 +525,31 @@ await describe('Helpers', async () => { }) await it('should return Available when no bootStatus is defined', () => { + // Arrange const { station: chargingStation } = createMockChargingStation({ baseName, connectorsCount: 2 }) const connectorStatus = {} as ConnectorStatus + + // Act & Assert expect(getBootConnectorStatus(chargingStation, 1, connectorStatus)).toBe( ConnectorStatusEnum.Available ) }) await it('should return bootStatus from template when defined', () => { + // Arrange const { station: chargingStation } = createMockChargingStation({ baseName, connectorsCount: 2 }) const connectorStatus = { bootStatus: ConnectorStatusEnum.Unavailable, } as ConnectorStatus + + // Act & Assert expect(getBootConnectorStatus(chargingStation, 1, connectorStatus)).toBe( ConnectorStatusEnum.Unavailable ) }) await it('should return Unavailable when charging station is inoperative', () => { + // Arrange const { station: chargingStation } = createMockChargingStation({ baseName, connectorDefaults: { availability: AvailabilityType.Inoperative }, @@ -464,12 +558,15 @@ await describe('Helpers', async () => { const connectorStatus = { bootStatus: ConnectorStatusEnum.Available, } as ConnectorStatus + + // Act & Assert expect(getBootConnectorStatus(chargingStation, 1, connectorStatus)).toBe( ConnectorStatusEnum.Unavailable ) }) await it('should return Unavailable when connector is inoperative', () => { + // Arrange const { station: chargingStation } = createMockChargingStation({ baseName, connectorDefaults: { availability: AvailabilityType.Inoperative }, @@ -479,30 +576,38 @@ await describe('Helpers', async () => { availability: AvailabilityType.Inoperative, bootStatus: ConnectorStatusEnum.Available, } as ConnectorStatus + + // Act & Assert expect(getBootConnectorStatus(chargingStation, 1, connectorStatus)).toBe( ConnectorStatusEnum.Unavailable ) }) await it('should restore previous status when transaction is in progress', () => { + // Arrange const { station: chargingStation } = createMockChargingStation({ baseName, connectorsCount: 2 }) const connectorStatus = { bootStatus: ConnectorStatusEnum.Available, status: ConnectorStatusEnum.Charging, transactionStarted: true, } as ConnectorStatus + + // Act & Assert expect(getBootConnectorStatus(chargingStation, 1, connectorStatus)).toBe( ConnectorStatusEnum.Charging ) }) await it('should use bootStatus over previous status when no transaction', () => { + // Arrange const { station: chargingStation } = createMockChargingStation({ baseName, connectorsCount: 2 }) const connectorStatus = { bootStatus: ConnectorStatusEnum.Available, status: ConnectorStatusEnum.Charging, transactionStarted: false, } as ConnectorStatus + + // Act & Assert expect(getBootConnectorStatus(chargingStation, 1, connectorStatus)).toBe( ConnectorStatusEnum.Available ) @@ -510,52 +615,65 @@ await describe('Helpers', async () => { // Tests for reservation helper functions await it('should return true when reservation has expired', () => { + // Arrange & Act & Assert expect(hasReservationExpired(createTestReservation(true))).toBe(true) }) await it('should return false when reservation is still valid', () => { + // Arrange & Act & Assert expect(hasReservationExpired(createTestReservation(false))).toBe(false) }) await it('should return false when connector has no reservation', () => { + // Arrange & Act & Assert const connectorStatus = {} as ConnectorStatus expect(hasPendingReservation(connectorStatus)).toBe(false) }) await it('should return true when connector has valid pending reservation', () => { + // Arrange & Act & Assert const connectorStatus = { reservation: createTestReservation(false) } as ConnectorStatus expect(hasPendingReservation(connectorStatus)).toBe(true) }) await it('should return false when connector reservation has expired', () => { + // Arrange & Act & Assert const connectorStatus = { reservation: createTestReservation(true) } as ConnectorStatus expect(hasPendingReservation(connectorStatus)).toBe(false) }) await it('should return false when no reservations exist (connector mode)', () => { + // Arrange & Act & Assert const { station: chargingStation } = createMockChargingStation({ baseName, connectorsCount: 2 }) expect(hasPendingReservations(chargingStation)).toBe(false) }) await it('should return true when pending reservation exists (connector mode)', () => { + // Arrange const { station: chargingStation } = createMockChargingStation({ baseName, connectorsCount: 2 }) const connectorStatus = chargingStation.connectors.get(1) if (connectorStatus != null) { connectorStatus.reservation = createTestReservation(false) } + + // Act & Assert expect(hasPendingReservations(chargingStation)).toBe(true) }) await it('should return false when no reservations exist (EVSE mode)', () => { + // Arrange const { station: chargingStation } = createMockChargingStation({ baseName, connectorsCount: 2, stationInfo: { ocppVersion: OCPPVersion.VERSION_201 }, }) + + // Act & Assert expect(hasPendingReservations(chargingStation)).toBe(false) }) await it('should return true when pending reservation exists (EVSE mode)', () => { + // Arrange const { station: chargingStation } = createMockChargingStation({ baseName, connectorsCount: 2, @@ -566,10 +684,13 @@ await describe('Helpers', async () => { if (firstConnector != null) { firstConnector.reservation = createTestReservation(false) } + + // Act & Assert expect(hasPendingReservations(chargingStation)).toBe(true) }) await it('should return false when only expired reservations exist (EVSE mode)', () => { + // Arrange const { station: chargingStation } = createMockChargingStation({ baseName, connectorsCount: 2, @@ -580,6 +701,8 @@ await describe('Helpers', async () => { if (firstConnector != null) { firstConnector.reservation = createTestReservation(true) } + + // Act & Assert expect(hasPendingReservations(chargingStation)).toBe(false) }) }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20CertificateManager.test.ts b/tests/charging-station/ocpp/2.0/OCPP20CertificateManager.test.ts index 9e01913c..483e1656 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20CertificateManager.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20CertificateManager.test.ts @@ -5,7 +5,7 @@ import { expect } from '@std/expect' import { rm } from 'node:fs/promises' -import { afterEach, describe, it } from 'node:test' +import { afterEach, beforeEach, describe, it } from 'node:test' import { OCPP20CertificateManager } from '../../../../src/charging-station/ocpp/2.0/OCPP20CertificateManager.js' import { @@ -66,9 +66,13 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await describe('storeCertificate', async () => { - await it('should store a valid PEM certificate to the correct path', async () => { - const manager = new OCPP20CertificateManager() + let manager: OCPP20CertificateManager + + beforeEach(() => { + manager = new OCPP20CertificateManager() + }) + await it('should store a valid PEM certificate to the correct path', async () => { const result = await manager.storeCertificate( TEST_STATION_HASH_ID, TEST_CERT_TYPE, @@ -83,8 +87,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should reject invalid PEM certificate without BEGIN/END markers', async () => { - const manager = new OCPP20CertificateManager() - const result = await manager.storeCertificate( TEST_STATION_HASH_ID, TEST_CERT_TYPE, @@ -97,8 +99,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should reject empty certificate data', async () => { - const manager = new OCPP20CertificateManager() - const result = await manager.storeCertificate( TEST_STATION_HASH_ID, TEST_CERT_TYPE, @@ -111,8 +111,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should create certificate directory structure if not exists', async () => { - const manager = new OCPP20CertificateManager() - const result = await manager.storeCertificate( TEST_STATION_HASH_ID, InstallCertificateUseEnumType.V2GRootCertificate, @@ -126,9 +124,12 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await describe('deleteCertificate', async () => { - await it('should delete certificate by hash data', async () => { - const manager = new OCPP20CertificateManager() + let manager: OCPP20CertificateManager + beforeEach(() => { + manager = new OCPP20CertificateManager() + }) + await it('should delete certificate by hash data', async () => { const hashData: CertificateHashDataType = { hashAlgorithm: HashAlgorithmEnumType.SHA256, issuerKeyHash: 'abc123def456', @@ -144,8 +145,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should return NotFound for non-existent certificate', async () => { - const manager = new OCPP20CertificateManager() - const hashData: CertificateHashDataType = { hashAlgorithm: HashAlgorithmEnumType.SHA256, issuerKeyHash: 'non-existent-hash', @@ -160,8 +159,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should handle filesystem errors gracefully', async () => { - const manager = new OCPP20CertificateManager() - const hashData: CertificateHashDataType = { hashAlgorithm: HashAlgorithmEnumType.SHA256, issuerKeyHash: 'valid-hash', @@ -177,9 +174,12 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await describe('getInstalledCertificates', async () => { - await it('should return list of installed certificates for station', async () => { - const manager = new OCPP20CertificateManager() + let manager: OCPP20CertificateManager + beforeEach(() => { + manager = new OCPP20CertificateManager() + }) + await it('should return list of installed certificates for station', async () => { const result = await manager.getInstalledCertificates(TEST_STATION_HASH_ID) expect(result).toBeDefined() @@ -187,8 +187,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should filter certificates by type when filter provided', async () => { - const manager = new OCPP20CertificateManager() - const filterTypes = [InstallCertificateUseEnumType.CSMSRootCertificate] const result = await manager.getInstalledCertificates(TEST_STATION_HASH_ID, filterTypes) @@ -197,8 +195,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should return empty list when no certificates installed', async () => { - const manager = new OCPP20CertificateManager() - const result = await manager.getInstalledCertificates('empty-station-hash-id') expect(result).toBeDefined() @@ -206,8 +202,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should support multiple certificate type filters', async () => { - const manager = new OCPP20CertificateManager() - const filterTypes = [ InstallCertificateUseEnumType.CSMSRootCertificate, InstallCertificateUseEnumType.V2GRootCertificate, @@ -221,9 +215,12 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await describe('computeCertificateHash', async () => { - await it('should compute hash data for valid PEM certificate', () => { - const manager = new OCPP20CertificateManager() + let manager: OCPP20CertificateManager + beforeEach(() => { + manager = new OCPP20CertificateManager() + }) + await it('should compute hash data for valid PEM certificate', () => { const hashData = manager.computeCertificateHash(VALID_PEM_CERTIFICATE) expect(hashData).toBeDefined() @@ -237,8 +234,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should return hex-encoded hash values', () => { - const manager = new OCPP20CertificateManager() - const hashData = manager.computeCertificateHash(VALID_PEM_CERTIFICATE) const hexPattern = /^[a-fA-F0-9]+$/ @@ -247,24 +242,18 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should throw error for invalid PEM certificate', () => { - const manager = new OCPP20CertificateManager() - expect(() => { manager.computeCertificateHash(INVALID_PEM_NO_MARKERS) }).toThrow() }) await it('should throw error for empty certificate', () => { - const manager = new OCPP20CertificateManager() - expect(() => { manager.computeCertificateHash(EMPTY_PEM_CERTIFICATE) }).toThrow() }) await it('should support SHA384 hash algorithm', () => { - const manager = new OCPP20CertificateManager() - const hashData = manager.computeCertificateHash( VALID_PEM_CERTIFICATE, HashAlgorithmEnumType.SHA384 @@ -275,8 +264,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should support SHA512 hash algorithm', () => { - const manager = new OCPP20CertificateManager() - const hashData = manager.computeCertificateHash( VALID_PEM_CERTIFICATE, HashAlgorithmEnumType.SHA512 @@ -288,41 +275,36 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await describe('validateCertificateFormat', async () => { - await it('should return true for valid PEM certificate', () => { - const manager = new OCPP20CertificateManager() + let manager: OCPP20CertificateManager + beforeEach(() => { + manager = new OCPP20CertificateManager() + }) + await it('should return true for valid PEM certificate', () => { const isValid = manager.validateCertificateFormat(VALID_PEM_CERTIFICATE) expect(isValid).toBe(true) }) await it('should return false for certificate without BEGIN marker', () => { - const manager = new OCPP20CertificateManager() - const isValid = manager.validateCertificateFormat(INVALID_PEM_NO_MARKERS) expect(isValid).toBe(false) }) await it('should return false for certificate with wrong markers', () => { - const manager = new OCPP20CertificateManager() - const isValid = manager.validateCertificateFormat(INVALID_PEM_WRONG_MARKERS) expect(isValid).toBe(false) }) await it('should return false for empty string', () => { - const manager = new OCPP20CertificateManager() - const isValid = manager.validateCertificateFormat(EMPTY_PEM_CERTIFICATE) expect(isValid).toBe(false) }) await it('should return false for null/undefined input', () => { - const manager = new OCPP20CertificateManager() - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- testing invalid null input expect(manager.validateCertificateFormat(null as any)).toBe(false) // eslint-disable-next-line @typescript-eslint/no-explicit-any -- testing invalid undefined input @@ -330,8 +312,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should return true for certificate with extra whitespace', () => { - const manager = new OCPP20CertificateManager() - const pemWithWhitespace = ` -----BEGIN CERTIFICATE----- @@ -347,9 +327,12 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await describe('getCertificatePath', async () => { - await it('should return correct file path for certificate', () => { - const manager = new OCPP20CertificateManager() + let manager: OCPP20CertificateManager + beforeEach(() => { + manager = new OCPP20CertificateManager() + }) + await it('should return correct file path for certificate', () => { const path = manager.getCertificatePath(TEST_STATION_HASH_ID, TEST_CERT_TYPE, 'SERIAL-12345') expect(path).toBeDefined() @@ -361,8 +344,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should handle special characters in serial number', () => { - const manager = new OCPP20CertificateManager() - const path = manager.getCertificatePath( TEST_STATION_HASH_ID, TEST_CERT_TYPE, @@ -376,8 +357,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should return different paths for different certificate types', () => { - const manager = new OCPP20CertificateManager() - const csmsPath = manager.getCertificatePath( TEST_STATION_HASH_ID, InstallCertificateUseEnumType.CSMSRootCertificate, @@ -396,8 +375,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should return path following project convention', () => { - const manager = new OCPP20CertificateManager() - const path = manager.getCertificatePath(TEST_STATION_HASH_ID, TEST_CERT_TYPE, 'SERIAL-12345') expect(path).toMatch(/configurations/) @@ -407,9 +384,12 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await describe('Edge cases and error handling', async () => { - await it('should handle concurrent certificate operations', async () => { - const manager = new OCPP20CertificateManager() + let manager: OCPP20CertificateManager + beforeEach(() => { + manager = new OCPP20CertificateManager() + }) + await it('should handle concurrent certificate operations', async () => { const results = await Promise.all([ manager.storeCertificate( TEST_STATION_HASH_ID, @@ -431,8 +411,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should handle very long certificate chains', async () => { - const manager = new OCPP20CertificateManager() - const longChain = Array(5).fill(VALID_PEM_CERTIFICATE).join('\n') const result = await manager.storeCertificate(TEST_STATION_HASH_ID, TEST_CERT_TYPE, longChain) @@ -441,8 +419,6 @@ await describe('I02-I04 - ISO15118 Certificate Management', async () => { }) await it('should sanitize station hash ID for filesystem safety', () => { - const manager = new OCPP20CertificateManager() - const maliciousHashId = '../../../etc/passwd' const path = manager.getCertificatePath(maliciousHashId, TEST_CERT_TYPE, 'SERIAL-001') diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts index df3c9a01..d4ad89d7 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts @@ -97,7 +97,7 @@ await describe('C11 - Clear Authorization Data in Authorization Cache', async () } }) - await it('should NOT call idTagsCache.deleteIdTags() on ClearCache request', async () => { + await it('should not call idTagsCache.deleteIdTags() on ClearCache request', async () => { // Verify that IdTagsCache is not touched let deleteIdTagsCalled = false const originalDeleteIdTags = station.idTagsCache.deleteIdTags.bind(station.idTagsCache) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts index d16fc942..8596daf5 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts @@ -25,7 +25,10 @@ import { } from '../../../../src/types/index.js' import { Constants } from '../../../../src/utils/index.js' import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js' -import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js' +import { + TEST_CHARGING_STATION_BASE_NAME, + TEST_ONE_HOUR_MS, +} from '../../ChargingStationTestConstants.js' import { createMockChargingStation } from '../../ChargingStationTestUtils.js' await describe('B11 & B12 - Reset', async () => { @@ -72,6 +75,40 @@ await describe('B11 & B12 - Reset', async () => { }) await describe('B11 - Reset - Without Ongoing Transaction', async () => { + let b11MockChargingStation: ChargingStation + let b11MockStation: ChargingStation & { + getNumberOfRunningTransactions: () => number + reset: () => Promise + } + + beforeEach(() => { + const { station } = createMockChargingStation({ + baseName: TEST_CHARGING_STATION_BASE_NAME, + connectorsCount: 3, + evseConfiguration: { evsesCount: 3 }, + heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL, + stationInfo: { + ocppStrictCompliance: false, + ocppVersion: OCPPVersion.VERSION_201, + resetTime: 5000, + }, + websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, + }) + b11MockChargingStation = station + + interface MockChargingStation extends ChargingStation { + getNumberOfRunningTransactions: () => number + reset: () => Promise + } + b11MockStation = b11MockChargingStation as MockChargingStation + b11MockStation.getNumberOfRunningTransactions = () => 0 + b11MockStation.reset = () => Promise.resolve() + }) + + afterEach(() => { + standardCleanup() + }) + // FR: B11.FR.01 await it('should handle Reset request with Immediate type when no transactions', async () => { const resetRequest: OCPP20ResetRequest = { @@ -79,7 +116,7 @@ await describe('B11 & B12 - Reset', async () => { } const response: OCPP20ResetResponse = await testableService.handleRequestReset( - mockChargingStation, + b11MockChargingStation, resetRequest ) @@ -100,7 +137,7 @@ await describe('B11 & B12 - Reset', async () => { } const response: OCPP20ResetResponse = await testableService.handleRequestReset( - mockChargingStation, + b11MockChargingStation, resetRequest ) @@ -121,7 +158,7 @@ await describe('B11 & B12 - Reset', async () => { } const response: OCPP20ResetResponse = await testableService.handleRequestReset( - mockChargingStation, + b11MockChargingStation, resetRequest ) @@ -141,7 +178,7 @@ await describe('B11 & B12 - Reset', async () => { } const response: OCPP20ResetResponse = await testableService.handleRequestReset( - mockChargingStation, + b11MockChargingStation, resetRequest ) @@ -158,7 +195,7 @@ await describe('B11 & B12 - Reset', async () => { } const response: OCPP20ResetResponse = await testableService.handleRequestReset( - mockChargingStation, + b11MockChargingStation, resetRequest ) @@ -167,7 +204,7 @@ await describe('B11 & B12 - Reset', async () => { expect(typeof response.status).toBe('string') // For immediate reset without active transactions, should be accepted - if (mockStation.getNumberOfRunningTransactions() === 0) { + if (b11MockStation.getNumberOfRunningTransactions() === 0) { expect(response.status).toBe(ResetStatusEnumType.Accepted) } }) @@ -178,7 +215,7 @@ await describe('B11 & B12 - Reset', async () => { } const response: OCPP20ResetResponse = await testableService.handleRequestReset( - mockChargingStation, + b11MockChargingStation, resetRequest ) @@ -186,31 +223,36 @@ await describe('B11 & B12 - Reset', async () => { expect(response.status).toBe(ResetStatusEnumType.Accepted) }) - await it('should reject EVSE reset when not supported and no transactions', async () => { - // Mock charging station without EVSE support - const originalHasEvses = mockChargingStation.hasEvses - ;(mockChargingStation as { hasEvses: boolean }).hasEvses = false - - const resetRequest: OCPP20ResetRequest = { - evseId: 1, - type: ResetEnumType.Immediate, - } - - const response: OCPP20ResetResponse = await testableService.handleRequestReset( - mockChargingStation, - resetRequest - ) + // Mock charging station without EVSE support + Object.defineProperty(b11MockChargingStation, 'hasEvses', { + configurable: true, + value: false, + writable: true, + }) - expect(response).toBeDefined() - expect(response.status).toBe(ResetStatusEnumType.Rejected) - expect(response.statusInfo).toBeDefined() - expect(response.statusInfo?.reasonCode).toBe(ReasonCodeEnumType.UnsupportedRequest) - expect(response.statusInfo?.additionalInfo).toContain( - 'does not support resetting individual EVSE' - ) + const resetRequest: OCPP20ResetRequest = { + evseId: 1, + type: ResetEnumType.Immediate, + } - // Restore original state - ;(mockChargingStation as { hasEvses: boolean }).hasEvses = originalHasEvses + const response: OCPP20ResetResponse = await testableService.handleRequestReset( + b11MockChargingStation, + resetRequest + ) + + expect(response).toBeDefined() + expect(response.status).toBe(ResetStatusEnumType.Rejected) + expect(response.statusInfo).toBeDefined() + expect(response.statusInfo?.reasonCode).toBe(ReasonCodeEnumType.UnsupportedRequest) + expect(response.statusInfo?.additionalInfo).toContain( + 'does not support resetting individual EVSE' + ) + + // Restore original state + Object.defineProperty(b11MockChargingStation, 'hasEvses', { + configurable: false, + value: true, + writable: false, }) await it('should handle EVSE-specific reset without transactions', async () => { @@ -220,7 +262,7 @@ await describe('B11 & B12 - Reset', async () => { } const response: OCPP20ResetResponse = await testableService.handleRequestReset( - mockChargingStation, + b11MockChargingStation, resetRequest ) @@ -231,54 +273,81 @@ await describe('B11 & B12 - Reset', async () => { }) await describe('B12 - Reset - With Ongoing Transaction', async () => { + let b12MockChargingStation: ChargingStation + let b12MockStation: ChargingStation & { + getNumberOfRunningTransactions: () => number + reset: () => Promise + } + + beforeEach(() => { + const { station } = createMockChargingStation({ + baseName: TEST_CHARGING_STATION_BASE_NAME, + connectorsCount: 3, + evseConfiguration: { evsesCount: 3 }, + heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL, + stationInfo: { + ocppStrictCompliance: false, + ocppVersion: OCPPVersion.VERSION_201, + resetTime: 5000, + }, + websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, + }) + b12MockChargingStation = station + + interface MockChargingStation extends ChargingStation { + getNumberOfRunningTransactions: () => number + reset: () => Promise + } + b12MockStation = b12MockChargingStation as MockChargingStation + b12MockStation.getNumberOfRunningTransactions = () => 0 + b12MockStation.reset = () => Promise.resolve() + }) + + afterEach(() => { + standardCleanup() + }) + // FR: B12.FR.02 await it('should handle immediate reset with active transactions', async () => { // Mock active transactions - mockStation.getNumberOfRunningTransactions = () => 1 + b12MockStation.getNumberOfRunningTransactions = () => 1 const resetRequest: OCPP20ResetRequest = { type: ResetEnumType.Immediate, } const response: OCPP20ResetResponse = await testableService.handleRequestReset( - mockChargingStation, + b12MockChargingStation, resetRequest ) expect(response).toBeDefined() expect(response.status).toBe(ResetStatusEnumType.Accepted) // Should accept immediate reset expect(response.statusInfo).toBeUndefined() - - // Reset mock - mockStation.getNumberOfRunningTransactions = () => 0 }) - // FR: B12.FR.01 await it('should handle OnIdle reset with active transactions', async () => { // Mock active transactions - mockStation.getNumberOfRunningTransactions = () => 1 + b12MockStation.getNumberOfRunningTransactions = () => 1 const resetRequest: OCPP20ResetRequest = { type: ResetEnumType.OnIdle, } const response: OCPP20ResetResponse = await testableService.handleRequestReset( - mockChargingStation, + b12MockChargingStation, resetRequest ) expect(response).toBeDefined() expect(response.status).toBe(ResetStatusEnumType.Scheduled) // Should schedule OnIdle reset expect(response.statusInfo).toBeUndefined() - - // Reset mock - mockStation.getNumberOfRunningTransactions = () => 0 }) // FR: B12.FR.03 await it('should handle EVSE-specific reset with active transactions', async () => { // Mock active transactions - mockStation.getNumberOfRunningTransactions = () => 1 + b12MockStation.getNumberOfRunningTransactions = () => 1 const resetRequest: OCPP20ResetRequest = { evseId: 1, @@ -286,7 +355,7 @@ await describe('B11 & B12 - Reset', async () => { } const response: OCPP20ResetResponse = await testableService.handleRequestReset( - mockChargingStation, + b12MockChargingStation, resetRequest ) @@ -295,16 +364,16 @@ await describe('B11 & B12 - Reset', async () => { expect([ResetStatusEnumType.Accepted, ResetStatusEnumType.Scheduled]).toContain( response.status ) - - // Reset mock - mockStation.getNumberOfRunningTransactions = () => 0 }) await it('should reject EVSE reset when not supported with active transactions', async () => { // Mock charging station without EVSE support and active transactions - const originalHasEvses = mockChargingStation.hasEvses - ;(mockChargingStation as { hasEvses: boolean }).hasEvses = false - mockStation.getNumberOfRunningTransactions = () => 1 + Object.defineProperty(b12MockChargingStation, 'hasEvses', { + configurable: true, + value: false, + writable: true, + }) + b12MockStation.getNumberOfRunningTransactions = () => 1 const resetRequest: OCPP20ResetRequest = { evseId: 1, @@ -312,7 +381,7 @@ await describe('B11 & B12 - Reset', async () => { } const response: OCPP20ResetResponse = await testableService.handleRequestReset( - mockChargingStation, + b12MockChargingStation, resetRequest ) @@ -325,8 +394,11 @@ await describe('B11 & B12 - Reset', async () => { ) // Restore original state - ;(mockChargingStation as { hasEvses: boolean }).hasEvses = originalHasEvses - mockStation.getNumberOfRunningTransactions = () => 0 + Object.defineProperty(b12MockChargingStation, 'hasEvses', { + configurable: false, + value: true, + writable: false, + }) }) // RST-001: Reset OnIdle Errata 2.14 Compliance Tests @@ -471,7 +543,7 @@ await describe('B11 & B12 - Reset', async () => { await it('should return Scheduled when connector has non-expired reservation', async () => { const station = createTestStation() // Create a reservation that expires in 1 hour (future) - const futureExpiryDate = new Date(Date.now() + 3600000) + const futureExpiryDate = new Date(Date.now() + TEST_ONE_HOUR_MS) const mockReservation: Partial = { expiryDate: futureExpiryDate, id: 1, @@ -504,7 +576,7 @@ await describe('B11 & B12 - Reset', async () => { await it('should return Accepted when reservation is expired', async () => { const station = createTestStation() // Create a reservation that expired 1 hour ago (past) - const pastExpiryDate = new Date(Date.now() - 3600000) + const pastExpiryDate = new Date(Date.now() - TEST_ONE_HOUR_MS) const mockReservation: Partial = { expiryDate: pastExpiryDate, id: 1, @@ -588,7 +660,7 @@ await describe('B11 & B12 - Reset', async () => { firmwareStatus: FirmwareStatus.Downloading, }) // Active reservation - const futureExpiryDate = new Date(Date.now() + 3600000) + const futureExpiryDate = new Date(Date.now() + TEST_ONE_HOUR_MS) const mockReservation: Partial = { expiryDate: futureExpiryDate, id: 1, diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-ISO15118.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-ISO15118.test.ts index 00c39026..dae44e14 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-ISO15118.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-ISO15118.test.ts @@ -230,6 +230,7 @@ await describe('M03 - GetCertificateStatus Request', async () => { }) afterEach(() => { + standardCleanup() mock.restoreAll() }) @@ -348,6 +349,7 @@ await describe('Request Command Names', async () => { }) afterEach(() => { + standardCleanup() mock.restoreAll() }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts index 58c7acda..6bb7d539 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts @@ -1118,7 +1118,7 @@ await describe('E01-E04 - OCPP 2.0.1 TransactionEvent Implementation', async () // ========================================================================== // E02 Cable-First Specific Tests // ========================================================================== - await describe('E02 - Cable-First Transaction Flow', async () => { + await describe('E02 - Cable-First Transaction', async () => { beforeEach(() => { resetConnectorTransactionState(mockChargingStation) }) @@ -1207,7 +1207,7 @@ await describe('E01-E04 - OCPP 2.0.1 TransactionEvent Implementation', async () }) }) - await describe('EV Detection Flow', async () => { + await describe('EV Detection', async () => { await it('should include EVDetected between cable plug and charging start', () => { const connectorId = 1 const transactionId = generateUUID() @@ -1421,7 +1421,7 @@ await describe('E01-E04 - OCPP 2.0.1 TransactionEvent Implementation', async () // ========================================================================== // E03 IdToken-First Specific Tests // ========================================================================== - await describe('E03 - IdToken-First Pre-Authorization Flow', async () => { + await describe('E03 - IdToken-First Pre-Authorization', async () => { beforeEach(() => { resetConnectorTransactionState(mockChargingStation) }) @@ -1748,7 +1748,7 @@ await describe('E01-E04 - OCPP 2.0.1 TransactionEvent Implementation', async () }) }) - await describe('Authorization Status in E03 Flow', async () => { + await describe('Authorization Status in E03', async () => { await it('should support Deauthorized trigger for rejected authorization', () => { const context: OCPP20TransactionContext = { authorizationMethod: 'idToken', diff --git a/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts b/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts index 94001aa9..3c2a0ef3 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts @@ -98,8 +98,11 @@ await describe('B05 - OCPP20VariableManager', async () => { }) await describe('getVariables method tests', async () => { - const manager = OCPP20VariableManager.getInstance() + let manager: OCPP20VariableManager + beforeEach(() => { + manager = OCPP20VariableManager.getInstance() + }) await it('should handle valid OCPPCommCtrlr and TxCtrlr component requests', () => { const request: OCPP20GetVariableDataType[] = [ { @@ -349,9 +352,13 @@ await describe('B05 - OCPP20VariableManager', async () => { }) await describe('Component validation tests', async () => { - const manager = OCPP20VariableManager.getInstance() - const testable = createTestableVariableManager(manager) + let manager: OCPP20VariableManager + let testable: ReturnType + beforeEach(() => { + manager = OCPP20VariableManager.getInstance() + testable = createTestableVariableManager(manager) + }) await it('should validate OCPPCommCtrlr component as always valid', () => { // Behavior: Connector components are unsupported and isComponentValid returns false. // Scope: Per-connector variable validation not implemented; tests assert current behavior. @@ -380,9 +387,13 @@ await describe('B05 - OCPP20VariableManager', async () => { }) await describe('Variable support validation tests', async () => { - const manager = OCPP20VariableManager.getInstance() - const testable = createTestableVariableManager(manager) + let manager: OCPP20VariableManager + let testable: ReturnType + beforeEach(() => { + manager = OCPP20VariableManager.getInstance() + testable = createTestableVariableManager(manager) + }) await it('should support standard HeartbeatInterval variable', () => { const component: ComponentType = { name: OCPP20ComponentName.OCPPCommCtrlr } const variable: VariableType = { name: OCPP20OptionalVariableName.HeartbeatInterval } @@ -411,8 +422,11 @@ await describe('B05 - OCPP20VariableManager', async () => { }) await describe('setVariables method tests', async () => { - const manager = OCPP20VariableManager.getInstance() + let manager: OCPP20VariableManager + beforeEach(() => { + manager = OCPP20VariableManager.getInstance() + }) await it('should accept setting writable variables (Actual default)', () => { const request: OCPP20SetVariableDataType[] = [ { diff --git a/tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts b/tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts index 84999597..8f57ff78 100644 --- a/tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts +++ b/tests/charging-station/ocpp/auth/OCPPAuthIntegration.test.ts @@ -19,7 +19,7 @@ import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js' import { createMockChargingStation } from '../../ChargingStationTestUtils.js' import { createMockAuthRequest, createMockIdentifier } from './helpers/MockFactories.js' -await describe('OCPP Authentication Integration Tests', async () => { +await describe('OCPP Authentication', async () => { let mockChargingStation16: ChargingStation let mockChargingStation20: ChargingStation @@ -54,16 +54,21 @@ await describe('OCPP Authentication Integration Tests', async () => { mock.restoreAll() }) - await describe('OCPP 1.6 Authentication Flow', async () => { + await describe('OCPP 1.6 Authentication', async () => { + let authService16: OCPPAuthServiceImpl + + beforeEach(() => { + authService16 = new OCPPAuthServiceImpl(mockChargingStation16) + }) + await it('should authenticate with valid identifier', async () => { - const authService = new OCPPAuthServiceImpl(mockChargingStation16) const request = createMockAuthRequest({ connectorId: 1, context: AuthContext.TRANSACTION_START, identifier: createMockIdentifier(OCPPVersion.VERSION_16, 'VALID_ID_123'), }) - const result = await authService.authenticate(request) + const result = await authService16.authenticate(request) expect(result).toBeDefined() expect(result.timestamp).toBeInstanceOf(Date) @@ -73,7 +78,6 @@ await describe('OCPP Authentication Integration Tests', async () => { }) await it('should handle multiple auth contexts', async () => { - const authService = new OCPPAuthServiceImpl(mockChargingStation16) const contexts = [ AuthContext.TRANSACTION_START, AuthContext.TRANSACTION_STOP, @@ -88,35 +92,39 @@ await describe('OCPP Authentication Integration Tests', async () => { identifier: createMockIdentifier(OCPPVersion.VERSION_16, `CONTEXT_TEST_${context}`), }) - const result = await authService.authenticate(request) + const result = await authService16.authenticate(request) expect(result).toBeDefined() expect(result.timestamp).toBeInstanceOf(Date) } }) await it('should authorize request directly', async () => { - const authService = new OCPPAuthServiceImpl(mockChargingStation16) const request = createMockAuthRequest({ connectorId: 1, identifier: createMockIdentifier(OCPPVersion.VERSION_16, 'AUTH_DIRECT_TEST'), }) - const result = await authService.authorize(request) + const result = await authService16.authorize(request) expect(result).toBeDefined() expect(result.timestamp).toBeInstanceOf(Date) }) }) - await describe('OCPP 2.0 Authentication Flow', async () => { + await describe('OCPP 2.0 Authentication', async () => { + let authService20: OCPPAuthServiceImpl + + beforeEach(() => { + authService20 = new OCPPAuthServiceImpl(mockChargingStation20) + }) + await it('should authenticate with valid identifier', async () => { - const authService = new OCPPAuthServiceImpl(mockChargingStation20) const request = createMockAuthRequest({ connectorId: 2, context: AuthContext.TRANSACTION_START, identifier: createMockIdentifier(OCPPVersion.VERSION_20, 'VALID_ID_456'), }) - const result = await authService.authenticate(request) + const result = await authService20.authenticate(request) expect(result).toBeDefined() expect(result.timestamp).toBeInstanceOf(Date) @@ -125,7 +133,6 @@ await describe('OCPP Authentication Integration Tests', async () => { }) await it('should handle all auth contexts', async () => { - const authService = new OCPPAuthServiceImpl(mockChargingStation20) const contexts = [ AuthContext.TRANSACTION_START, AuthContext.TRANSACTION_STOP, @@ -140,7 +147,7 @@ await describe('OCPP Authentication Integration Tests', async () => { identifier: createMockIdentifier(OCPPVersion.VERSION_20, `V20_CONTEXT_${context}`), }) - const result = await authService.authenticate(request) + const result = await authService20.authenticate(request) expect(result).toBeDefined() expect(result.timestamp).toBeInstanceOf(Date) } @@ -148,8 +155,13 @@ await describe('OCPP Authentication Integration Tests', async () => { }) await describe('Integration Error Scenarios', async () => { + let authServiceError: OCPPAuthServiceImpl + + beforeEach(() => { + authServiceError = new OCPPAuthServiceImpl(mockChargingStation16) + }) + await it('should handle invalid identifier gracefully during auth flow', async () => { - const authService = new OCPPAuthServiceImpl(mockChargingStation16) const request = createMockAuthRequest({ connectorId: 999, // Invalid connector context: AuthContext.TRANSACTION_START, @@ -160,7 +172,7 @@ await describe('OCPP Authentication Integration Tests', async () => { }, }) - const result = await authService.authenticate(request) + const result = await authServiceError.authenticate(request) // Should return a result (not throw) with non-ACCEPTED status expect(result).toBeDefined() @@ -169,8 +181,13 @@ await describe('OCPP Authentication Integration Tests', async () => { }) await describe('Concurrent Operations', async () => { + let authServiceConcurrent: OCPPAuthServiceImpl + + beforeEach(() => { + authServiceConcurrent = new OCPPAuthServiceImpl(mockChargingStation16) + }) + await it('should handle concurrent authentication requests with mixed contexts', async () => { - const authService = new OCPPAuthServiceImpl(mockChargingStation16) const requestCount = 10 const promises = [] @@ -180,7 +197,7 @@ await describe('OCPP Authentication Integration Tests', async () => { context: i % 2 === 0 ? AuthContext.TRANSACTION_START : AuthContext.TRANSACTION_STOP, identifier: createMockIdentifier(OCPPVersion.VERSION_16, `CONCURRENT_${String(i)}`), }) - promises.push(authService.authenticate(request)) + promises.push(authServiceConcurrent.authenticate(request)) } const results = await Promise.all(promises) diff --git a/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts b/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts index 0dfb917d..28b8566d 100644 --- a/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts +++ b/tests/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.test.ts @@ -407,6 +407,7 @@ await describe('OCPP20AuthAdapter', async () => { }) afterEach(() => { + standardCleanup() mock.reset() }) diff --git a/tests/charging-station/ocpp/auth/factories/AuthComponentFactory.test.ts b/tests/charging-station/ocpp/auth/factories/AuthComponentFactory.test.ts index 8fb6c63f..fd9c993d 100644 --- a/tests/charging-station/ocpp/auth/factories/AuthComponentFactory.test.ts +++ b/tests/charging-station/ocpp/auth/factories/AuthComponentFactory.test.ts @@ -11,6 +11,7 @@ import type { AuthConfiguration } from '../../../../../src/charging-station/ocpp import { AuthComponentFactory } from '../../../../../src/charging-station/ocpp/auth/factories/AuthComponentFactory.js' import { OCPPVersion } from '../../../../../src/types/ocpp/OCPPVersion.js' import { standardCleanup } from '../../../../helpers/TestLifecycleHelpers.js' +import { TEST_AUTHORIZATION_TIMEOUT_MS } from '../../../ChargingStationTestConstants.js' import { createMockChargingStation } from '../../../ChargingStationTestUtils.js' await describe('AuthComponentFactory', async () => { @@ -74,7 +75,7 @@ await describe('AuthComponentFactory', async () => { const config: AuthConfiguration = { allowOfflineTxForUnknownId: false, authorizationCacheEnabled: true, - authorizationTimeout: 30000, + authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS, certificateAuthEnabled: false, localAuthListEnabled: false, localPreAuthorize: false, @@ -97,7 +98,7 @@ await describe('AuthComponentFactory', async () => { const config: AuthConfiguration = { allowOfflineTxForUnknownId: false, authorizationCacheEnabled: false, - authorizationTimeout: 30000, + authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS, certificateAuthEnabled: false, localAuthListEnabled: true, localPreAuthorize: false, @@ -115,7 +116,7 @@ await describe('AuthComponentFactory', async () => { const config: AuthConfiguration = { allowOfflineTxForUnknownId: false, authorizationCacheEnabled: false, - authorizationTimeout: 30000, + authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS, certificateAuthEnabled: false, localAuthListEnabled: false, localPreAuthorize: false, @@ -131,7 +132,7 @@ await describe('AuthComponentFactory', async () => { const config: AuthConfiguration = { allowOfflineTxForUnknownId: false, authorizationCacheEnabled: false, - authorizationTimeout: 30000, + authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS, certificateAuthEnabled: false, localAuthListEnabled: true, localPreAuthorize: false, @@ -156,7 +157,7 @@ await describe('AuthComponentFactory', async () => { const config: AuthConfiguration = { allowOfflineTxForUnknownId: false, authorizationCacheEnabled: false, - authorizationTimeout: 30000, + authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS, certificateAuthEnabled: false, localAuthListEnabled: false, localPreAuthorize: false, @@ -177,7 +178,7 @@ await describe('AuthComponentFactory', async () => { const config: AuthConfiguration = { allowOfflineTxForUnknownId: false, authorizationCacheEnabled: false, - authorizationTimeout: 30000, + authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS, certificateAuthEnabled: false, localAuthListEnabled: false, localPreAuthorize: false, @@ -203,7 +204,7 @@ await describe('AuthComponentFactory', async () => { const config: AuthConfiguration = { allowOfflineTxForUnknownId: false, authorizationCacheEnabled: false, - authorizationTimeout: 30000, + authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS, certificateAuthEnabled: true, localAuthListEnabled: false, localPreAuthorize: false, @@ -230,7 +231,7 @@ await describe('AuthComponentFactory', async () => { const config: AuthConfiguration = { allowOfflineTxForUnknownId: false, authorizationCacheEnabled: false, - authorizationTimeout: 30000, + authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS, certificateAuthEnabled: false, localAuthListEnabled: false, localPreAuthorize: false, @@ -257,7 +258,7 @@ await describe('AuthComponentFactory', async () => { const config: AuthConfiguration = { allowOfflineTxForUnknownId: false, authorizationCacheEnabled: false, - authorizationTimeout: 30000, + authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS, certificateAuthEnabled: false, localAuthListEnabled: true, localPreAuthorize: false, @@ -286,7 +287,7 @@ await describe('AuthComponentFactory', async () => { allowOfflineTxForUnknownId: false, authorizationCacheEnabled: true, authorizationCacheLifetime: 600, - authorizationTimeout: 30000, + authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS, certificateAuthEnabled: false, localAuthListEnabled: true, localPreAuthorize: false, @@ -304,7 +305,7 @@ await describe('AuthComponentFactory', async () => { allowOfflineTxForUnknownId: false, authorizationCacheEnabled: true, authorizationCacheLifetime: -1, // Invalid - authorizationTimeout: 30000, + authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS, certificateAuthEnabled: false, localAuthListEnabled: false, localPreAuthorize: false, diff --git a/tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts b/tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts index f34eb837..acdecb62 100644 --- a/tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts +++ b/tests/charging-station/ocpp/auth/strategies/LocalAuthStrategy.test.ts @@ -102,6 +102,19 @@ await describe('LocalAuthStrategy', async () => { await strategy.initialize(config) }) + afterEach(() => { + // Reset mock properties to prevent state pollution between tests + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + delete (mockLocalAuthListManager as any).getEntry + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + delete (mockAuthCache as any).get + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + delete (mockAuthCache as any).set + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + delete (mockAuthCache as any).remove + mock.restoreAll() + }) + await it('should authenticate using local auth list', async () => { mockLocalAuthListManager.getEntry = async () => await Promise.resolve({ diff --git a/tests/charging-station/ui-server/UIHttpServer.test.ts b/tests/charging-station/ui-server/UIHttpServer.test.ts index 9e81b9e2..209e21f0 100644 --- a/tests/charging-station/ui-server/UIHttpServer.test.ts +++ b/tests/charging-station/ui-server/UIHttpServer.test.ts @@ -5,7 +5,7 @@ // Copyright Jerome Benoit. 2024-2025. All Rights Reserved. import { expect } from '@std/expect' -import { afterEach, describe, it, mock } from 'node:test' +import { afterEach, beforeEach, describe, it, mock } from 'node:test' import { gunzipSync } from 'node:zlib' import type { UUIDv4 } from '../../../src/types/index.js' @@ -48,12 +48,18 @@ const createLargePayload = (status: ResponseStatus = ResponseStatus.SUCCESS) => }) await describe('UIHttpServer', async () => { + let server: TestableUIHttpServer + + beforeEach(() => { + server = new TestableUIHttpServer(createHttpServerConfig()) + }) + afterEach(() => { mock.restoreAll() standardCleanup() }) + await it('should delete response handler after successful send', () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() server.addResponseHandler(TEST_UUID, res) @@ -67,15 +73,12 @@ await describe('UIHttpServer', async () => { }) await it('should log error when response handler not found', () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) - server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }]) expect(server.hasResponseHandler(TEST_UUID)).toBe(false) }) await it('should set status code 400 for failure responses', () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() server.addResponseHandler(TEST_UUID, res) @@ -87,7 +90,6 @@ await describe('UIHttpServer', async () => { }) await it('should handle send errors gracefully without throwing', () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() res.end = (): never => { throw new Error('HTTP response end error') @@ -100,7 +102,6 @@ await describe('UIHttpServer', async () => { }) await it('should set application/json Content-Type header', () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() server.addResponseHandler(TEST_UUID, res) @@ -110,7 +111,6 @@ await describe('UIHttpServer', async () => { }) await it('should clean up response handlers after each response', () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res1 = new MockServerResponse() const res2 = new MockServerResponse() @@ -126,7 +126,6 @@ await describe('UIHttpServer', async () => { }) await it('should clear all handlers on server stop', () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() server.addResponseHandler(TEST_UUID, res) @@ -138,7 +137,6 @@ await describe('UIHttpServer', async () => { }) await it('should serialize response payload to JSON correctly', () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() const payload = { hashIdsSucceeded: ['station-1', 'station-2'], @@ -155,7 +153,6 @@ await describe('UIHttpServer', async () => { }) await it('should include error details in failure response', () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() const payload = { errorMessage: 'Test error', @@ -174,71 +171,77 @@ await describe('UIHttpServer', async () => { }) await it('should create server with valid HTTP configuration', () => { - const server = new UIHttpServer(createHttpServerConfig()) - expect(server).toBeDefined() }) await it('should create server with custom host and port', () => { - const config = createMockUIServerConfiguration({ - options: { - host: 'localhost', - port: 9090, - }, - type: ApplicationProtocol.HTTP, - }) - - const server = new UIHttpServer(config) - expect(server).toBeDefined() + const serverCustom = new UIHttpServer( + createMockUIServerConfiguration({ + options: { + host: 'localhost', + port: 9090, + }, + type: ApplicationProtocol.HTTP, + }) + ) + + expect(serverCustom).toBeDefined() }) await describe('Gzip compression', async () => { + let gzipServer: TestableUIHttpServer + + beforeEach(() => { + gzipServer = new TestableUIHttpServer(createHttpServerConfig()) + }) + + afterEach(() => { + mock.restoreAll() + standardCleanup() + }) + await it('should skip compression when acceptsGzip is false', () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() - server.addResponseHandler(TEST_UUID, res) - server.setAcceptsGzip(TEST_UUID, false) - server.sendResponse([TEST_UUID, createLargePayload()]) + gzipServer.addResponseHandler(TEST_UUID, res) + gzipServer.setAcceptsGzip(TEST_UUID, false) + gzipServer.sendResponse([TEST_UUID, createLargePayload()]) expect(res.headers['Content-Encoding']).toBeUndefined() expect(res.headers['Content-Type']).toBe('application/json') }) await it('should skip compression for small response payloads', () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() - server.addResponseHandler(TEST_UUID, res) - server.setAcceptsGzip(TEST_UUID, true) - server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }]) + gzipServer.addResponseHandler(TEST_UUID, res) + gzipServer.setAcceptsGzip(TEST_UUID, true) + gzipServer.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }]) expect(res.headers['Content-Encoding']).toBeUndefined() expect(res.headers['Content-Type']).toBe('application/json') }) await it('should skip compression when payload below threshold', () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() const smallPayload = { data: 'x'.repeat(100), status: ResponseStatus.SUCCESS, } - server.addResponseHandler(TEST_UUID, res) - server.setAcceptsGzip(TEST_UUID, true) - server.sendResponse([TEST_UUID, smallPayload]) + gzipServer.addResponseHandler(TEST_UUID, res) + gzipServer.setAcceptsGzip(TEST_UUID, true) + gzipServer.sendResponse([TEST_UUID, smallPayload]) expect(res.headers['Content-Encoding']).toBeUndefined() }) await it('should set gzip Content-Encoding header for large responses', async () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() - server.addResponseHandler(TEST_UUID, res) - server.setAcceptsGzip(TEST_UUID, true) - server.sendResponse([TEST_UUID, createLargePayload()]) + gzipServer.addResponseHandler(TEST_UUID, res) + gzipServer.setAcceptsGzip(TEST_UUID, true) + gzipServer.sendResponse([TEST_UUID, createLargePayload()]) await waitForStreamFlush(GZIP_STREAM_FLUSH_DELAY_MS) @@ -248,13 +251,12 @@ await describe('UIHttpServer', async () => { }) await it('should decompress gzip response to original payload', async () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() const payload = createLargePayload() - server.addResponseHandler(TEST_UUID, res) - server.setAcceptsGzip(TEST_UUID, true) - server.sendResponse([TEST_UUID, payload]) + gzipServer.addResponseHandler(TEST_UUID, res) + gzipServer.setAcceptsGzip(TEST_UUID, true) + gzipServer.sendResponse([TEST_UUID, payload]) await waitForStreamFlush(GZIP_STREAM_FLUSH_DELAY_MS) @@ -269,29 +271,27 @@ await describe('UIHttpServer', async () => { }) await it('should skip compression when acceptsGzip context missing', () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() - server.addResponseHandler(TEST_UUID, res) - server.sendResponse([TEST_UUID, createLargePayload()]) + gzipServer.addResponseHandler(TEST_UUID, res) + gzipServer.sendResponse([TEST_UUID, createLargePayload()]) expect(res.headers['Content-Encoding']).toBeUndefined() expect(res.headers['Content-Type']).toBe('application/json') }) await it('should cleanup acceptsGzip context after response sent', async () => { - const server = new TestableUIHttpServer(createHttpServerConfig()) const res = new MockServerResponse() - server.addResponseHandler(TEST_UUID, res) - server.setAcceptsGzip(TEST_UUID, true) - expect(server.getAcceptsGzip().has(TEST_UUID)).toBe(true) + gzipServer.addResponseHandler(TEST_UUID, res) + gzipServer.setAcceptsGzip(TEST_UUID, true) + expect(gzipServer.getAcceptsGzip().has(TEST_UUID)).toBe(true) - server.sendResponse([TEST_UUID, createLargePayload()]) + gzipServer.sendResponse([TEST_UUID, createLargePayload()]) await waitForStreamFlush(GZIP_STREAM_FLUSH_DELAY_MS) - expect(server.getAcceptsGzip().has(TEST_UUID)).toBe(false) + expect(gzipServer.getAcceptsGzip().has(TEST_UUID)).toBe(false) }) }) }) diff --git a/tests/charging-station/ui-server/UIServerSecurity.test.ts b/tests/charging-station/ui-server/UIServerSecurity.test.ts index ba1a062e..cd07d238 100644 --- a/tests/charging-station/ui-server/UIServerSecurity.test.ts +++ b/tests/charging-station/ui-server/UIServerSecurity.test.ts @@ -24,7 +24,7 @@ await describe('UIServerSecurity', async () => { mock.restoreAll() standardCleanup() }) - await describe('isValidCredential()', async () => { + await describe('IsValidCredential', async () => { await it('should return true for matching credentials', () => { expect(isValidCredential('myPassword123', 'myPassword123')).toBe(true) }) @@ -43,30 +43,33 @@ await describe('UIServerSecurity', async () => { }) }) - await describe('createBodySizeLimiter()', async () => { + await describe('CreateBodySizeLimiter', async () => { + let limiter: ReturnType + await it('should return true when bytes under limit', () => { - const limiter = createBodySizeLimiter(1000) + limiter = createBodySizeLimiter(1000) expect(limiter(500)).toBe(true) }) await it('should return false when accumulated bytes exceed limit', () => { - const limiter = createBodySizeLimiter(1000) + limiter = createBodySizeLimiter(1000) limiter(600) - expect(limiter(500)).toBe(false) }) await it('should return true at exact limit boundary', () => { - const limiter = createBodySizeLimiter(1000) + limiter = createBodySizeLimiter(1000) expect(limiter(1000)).toBe(true) }) }) - await describe('createRateLimiter()', async () => { + await describe('CreateRateLimiter', async () => { + let limiter: ReturnType + await it('should allow requests under rate limit', () => { - const limiter = createRateLimiter(5, 1000) + limiter = createRateLimiter(5, 1000) for (let i = 0; i < 5; i++) { expect(limiter('192.168.1.1')).toBe(true) @@ -74,16 +77,15 @@ await describe('UIServerSecurity', async () => { }) await it('should block requests exceeding rate limit', () => { - const limiter = createRateLimiter(3, 1000) + limiter = createRateLimiter(3, 1000) limiter('192.168.1.1') limiter('192.168.1.1') limiter('192.168.1.1') - expect(limiter('192.168.1.1')).toBe(false) }) await it('should reset window after time expires', async () => { - const limiter = createRateLimiter(2, 100) + limiter = createRateLimiter(2, 100) limiter('10.0.0.1') limiter('10.0.0.1') expect(limiter('10.0.0.1')).toBe(false) @@ -94,7 +96,7 @@ await describe('UIServerSecurity', async () => { }) await it('should reject new IPs when at max tracked capacity', () => { - const limiter = createRateLimiter(10, 60000, 3) + limiter = createRateLimiter(10, 60000, 3) expect(limiter('192.168.1.1')).toBe(true) expect(limiter('192.168.1.2')).toBe(true) @@ -103,7 +105,7 @@ await describe('UIServerSecurity', async () => { }) await it('should allow existing IPs when at max capacity', () => { - const limiter = createRateLimiter(10, 60000, 2) + limiter = createRateLimiter(10, 60000, 2) expect(limiter('192.168.1.1')).toBe(true) expect(limiter('192.168.1.2')).toBe(true) @@ -112,7 +114,7 @@ await describe('UIServerSecurity', async () => { }) await it('should cleanup expired entries when at capacity', async () => { - const limiter = createRateLimiter(10, 50, 2) + limiter = createRateLimiter(10, 50, 2) expect(limiter('192.168.1.1')).toBe(true) expect(limiter('192.168.1.2')).toBe(true) @@ -122,7 +124,7 @@ await describe('UIServerSecurity', async () => { }) }) - await describe('isValidNumberOfStations()', async () => { + await describe('IsValidNumberOfStations', async () => { await it('should return true for valid number within limit', () => { expect(isValidNumberOfStations(50, DEFAULT_MAX_STATIONS)).toBe(true) }) -- 2.53.0