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
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)
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
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
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
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
await it('should call saveOcppConfiguration without throwing', () => {
// Arrange
- const result = createRealChargingStation()
+ const result = createMockChargingStation()
station = result.station
// Act & Assert - should not throw
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
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
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
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
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
await it('should preserve station info properties for persistence', () => {
// Arrange
- const result = createRealChargingStation({
+ const result = createMockChargingStation({
baseName: 'PERSIST-CS',
index: 5,
})
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
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
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(() => {
})
})
- await describe('Connector 0 (Shared Power) Tests', async () => {
+ await describe('Connector 0 (Shared Power)', async () => {
let station: ChargingStation | undefined
beforeEach(() => {
})
})
- await describe('EVSE Query Tests (non-EVSE mode)', async () => {
+ await describe('EVSE Query (non-EVSE mode)', async () => {
let station: ChargingStation | undefined
beforeEach(() => {
})
})
- await describe('EVSE Mode Tests', async () => {
+ await describe('EVSE Mode', async () => {
let station: ChargingStation | undefined
beforeEach(() => {
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
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
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
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
await it('should allow state transitions from PENDING to ACCEPTED', () => {
// Arrange
- const result = createRealChargingStation({
+ const result = createMockChargingStation({
bootNotificationStatus: RegistrationStatusEnumType.PENDING,
})
station = result.station
await it('should allow state transitions from PENDING to REJECTED', () => {
// Arrange
- const result = createRealChargingStation({
+ const result = createMockChargingStation({
bootNotificationStatus: RegistrationStatusEnumType.PENDING,
})
station = result.station
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,
}
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
}
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,
}
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,
}
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,
}
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,
}
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,
}
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,
}
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,
}
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 () => {
// 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)
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(() => {
})
})
- await describe('Energy Meter Tests', async () => {
+ await describe('Energy Meter', async () => {
let station: ChargingStation | undefined
beforeEach(() => {
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
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
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
* - 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'
import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
import {
TEST_ID_TAG,
+ TEST_ONE_HOUR_MS,
TEST_TRANSACTION_ENERGY_WH,
TEST_TRANSACTION_ID,
} from './ChargingStationTestConstants.js'
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()
})
})
})
- 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 })
// 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,
}
})
})
- 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()
* 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
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<ChargingStationOcppConfiguration>
+
+ // 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<ChargingStationOcppConfiguration>
+
+ // 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
})
await it('should add new key with custom options', () => {
+ // Arrange
const { station: cs } = createMockChargingStation()
+
+ // Act
addConfigurationKey(
cs,
TEST_KEY_1,
{ 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,
{ 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)
})
await it('should log error and leave key untouched when identical options & value attempted (overwrite=false)', t => {
+ // Arrange
const { station: cs } = createMockChargingStation()
addConfigurationKey(
cs,
{ save: false }
)
const errorMock = t.mock.method(logger, 'error')
+
+ // Act
// Attempt to add same key with identical value and options
addConfigurationKey(
cs,
{ 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)
})
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,
{ 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)
})
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(),
{ 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,
{ 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<ChargingStationOcppConfiguration>
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<ChargingStationOcppConfiguration>
+
+ // 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)
})
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()
})
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()
})
*/
import { expect } from '@std/expect'
-import { afterEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it } from 'node:test'
import {
checkChargingStationState,
} 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()
}) 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: {
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: {
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: {
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: {
templateIndex: 0,
},
})
+
+ // Act & Assert
expect(() => {
validateStationInfo(stationInvalidTemplate)
}).toThrow(
})
await it('should throw when templateName is undefined', () => {
+ // Arrange
const { station: stationMissingName } = createMockChargingStation({
baseName,
stationInfo: {
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: {
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: {
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: {
templateName: 'test-template.json',
},
})
+
+ // Act & Assert
expect(() => {
validateStationInfo(stationInvalidPower)
}).toThrow(
})
await it('should throw when maximumAmperage is undefined', () => {
+ // Arrange
const { station: stationMissingAmperage } = createMockChargingStation({
baseName,
stationInfo: {
templateName: 'test-template.json',
},
})
+
+ // Act & Assert
expect(() => {
validateStationInfo(stationMissingAmperage)
}).toThrow(
})
await it('should throw when maximumAmperage is zero', () => {
+ // Arrange
const { station: stationInvalidAmperage } = createMockChargingStation({
baseName,
stationInfo: {
templateName: 'test-template.json',
},
})
+
+ // Act & Assert
expect(() => {
validateStationInfo(stationInvalidAmperage)
}).toThrow(
})
await it('should pass validation with complete valid configuration', () => {
+ // Arrange
const { station: validStation } = createMockChargingStation({
baseName,
stationInfo: {
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
templateName: 'test-template.json',
},
})
+
+ // Act & Assert
expect(() => {
validateStationInfo(stationOcpp20)
}).toThrow(
})
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
templateName: 'test-template.json',
},
})
+
+ // Act & Assert
expect(() => {
validateStationInfo(stationOcpp201)
}).toThrow(
})
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')
})
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'))
})
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(
})
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,
})
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 },
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 },
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
)
// 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,
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,
if (firstConnector != null) {
firstConnector.reservation = createTestReservation(true)
}
+
+ // Act & Assert
expect(hasPendingReservations(chargingStation)).toBe(false)
})
})
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 {
})
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,
})
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,
})
await it('should reject empty certificate data', async () => {
- const manager = new OCPP20CertificateManager()
-
const result = await manager.storeCertificate(
TEST_STATION_HASH_ID,
TEST_CERT_TYPE,
})
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,
})
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',
})
await it('should return NotFound for non-existent certificate', async () => {
- const manager = new OCPP20CertificateManager()
-
const hashData: CertificateHashDataType = {
hashAlgorithm: HashAlgorithmEnumType.SHA256,
issuerKeyHash: 'non-existent-hash',
})
await it('should handle filesystem errors gracefully', async () => {
- const manager = new OCPP20CertificateManager()
-
const hashData: CertificateHashDataType = {
hashAlgorithm: HashAlgorithmEnumType.SHA256,
issuerKeyHash: 'valid-hash',
})
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()
})
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)
})
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()
})
await it('should support multiple certificate type filters', async () => {
- const manager = new OCPP20CertificateManager()
-
const filterTypes = [
InstallCertificateUseEnumType.CSMSRootCertificate,
InstallCertificateUseEnumType.V2GRootCertificate,
})
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()
})
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]+$/
})
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
})
await it('should support SHA512 hash algorithm', () => {
- const manager = new OCPP20CertificateManager()
-
const hashData = manager.computeCertificateHash(
VALID_PEM_CERTIFICATE,
HashAlgorithmEnumType.SHA512
})
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
})
await it('should return true for certificate with extra whitespace', () => {
- const manager = new OCPP20CertificateManager()
-
const pemWithWhitespace = `
-----BEGIN CERTIFICATE-----
})
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()
})
await it('should handle special characters in serial number', () => {
- const manager = new OCPP20CertificateManager()
-
const path = manager.getCertificatePath(
TEST_STATION_HASH_ID,
TEST_CERT_TYPE,
})
await it('should return different paths for different certificate types', () => {
- const manager = new OCPP20CertificateManager()
-
const csmsPath = manager.getCertificatePath(
TEST_STATION_HASH_ID,
InstallCertificateUseEnumType.CSMSRootCertificate,
})
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/)
})
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,
})
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)
})
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')
}
})
- 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)
} 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 () => {
})
await describe('B11 - Reset - Without Ongoing Transaction', async () => {
+ let b11MockChargingStation: ChargingStation
+ let b11MockStation: ChargingStation & {
+ getNumberOfRunningTransactions: () => number
+ reset: () => Promise<void>
+ }
+
+ 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<void>
+ }
+ 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 = {
}
const response: OCPP20ResetResponse = await testableService.handleRequestReset(
- mockChargingStation,
+ b11MockChargingStation,
resetRequest
)
}
const response: OCPP20ResetResponse = await testableService.handleRequestReset(
- mockChargingStation,
+ b11MockChargingStation,
resetRequest
)
}
const response: OCPP20ResetResponse = await testableService.handleRequestReset(
- mockChargingStation,
+ b11MockChargingStation,
resetRequest
)
}
const response: OCPP20ResetResponse = await testableService.handleRequestReset(
- mockChargingStation,
+ b11MockChargingStation,
resetRequest
)
}
const response: OCPP20ResetResponse = await testableService.handleRequestReset(
- mockChargingStation,
+ b11MockChargingStation,
resetRequest
)
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)
}
})
}
const response: OCPP20ResetResponse = await testableService.handleRequestReset(
- mockChargingStation,
+ b11MockChargingStation,
resetRequest
)
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 () => {
}
const response: OCPP20ResetResponse = await testableService.handleRequestReset(
- mockChargingStation,
+ b11MockChargingStation,
resetRequest
)
})
await describe('B12 - Reset - With Ongoing Transaction', async () => {
+ let b12MockChargingStation: ChargingStation
+ let b12MockStation: ChargingStation & {
+ getNumberOfRunningTransactions: () => number
+ reset: () => Promise<void>
+ }
+
+ 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<void>
+ }
+ 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,
}
const response: OCPP20ResetResponse = await testableService.handleRequestReset(
- mockChargingStation,
+ b12MockChargingStation,
resetRequest
)
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,
}
const response: OCPP20ResetResponse = await testableService.handleRequestReset(
- mockChargingStation,
+ b12MockChargingStation,
resetRequest
)
)
// 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
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<Reservation> = {
expiryDate: futureExpiryDate,
id: 1,
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<Reservation> = {
expiryDate: pastExpiryDate,
id: 1,
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<Reservation> = {
expiryDate: futureExpiryDate,
id: 1,
})
afterEach(() => {
+ standardCleanup()
mock.restoreAll()
})
})
afterEach(() => {
+ standardCleanup()
mock.restoreAll()
})
// ==========================================================================
// E02 Cable-First Specific Tests
// ==========================================================================
- await describe('E02 - Cable-First Transaction Flow', async () => {
+ await describe('E02 - Cable-First Transaction', async () => {
beforeEach(() => {
resetConnectorTransactionState(mockChargingStation)
})
})
})
- 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()
// ==========================================================================
// 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)
})
})
})
- 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',
})
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[] = [
{
})
await describe('Component validation tests', async () => {
- const manager = OCPP20VariableManager.getInstance()
- const testable = createTestableVariableManager(manager)
+ let manager: OCPP20VariableManager
+ let testable: ReturnType<typeof createTestableVariableManager>
+ 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.
})
await describe('Variable support validation tests', async () => {
- const manager = OCPP20VariableManager.getInstance()
- const testable = createTestableVariableManager(manager)
+ let manager: OCPP20VariableManager
+ let testable: ReturnType<typeof createTestableVariableManager>
+ 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 }
})
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[] = [
{
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
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)
})
await it('should handle multiple auth contexts', async () => {
- const authService = new OCPPAuthServiceImpl(mockChargingStation16)
const contexts = [
AuthContext.TRANSACTION_START,
AuthContext.TRANSACTION_STOP,
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)
})
await it('should handle all auth contexts', async () => {
- const authService = new OCPPAuthServiceImpl(mockChargingStation20)
const contexts = [
AuthContext.TRANSACTION_START,
AuthContext.TRANSACTION_STOP,
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)
}
})
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,
},
})
- 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()
})
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 = []
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)
})
afterEach(() => {
+ standardCleanup()
mock.reset()
})
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 () => {
const config: AuthConfiguration = {
allowOfflineTxForUnknownId: false,
authorizationCacheEnabled: true,
- authorizationTimeout: 30000,
+ authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS,
certificateAuthEnabled: false,
localAuthListEnabled: false,
localPreAuthorize: false,
const config: AuthConfiguration = {
allowOfflineTxForUnknownId: false,
authorizationCacheEnabled: false,
- authorizationTimeout: 30000,
+ authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS,
certificateAuthEnabled: false,
localAuthListEnabled: true,
localPreAuthorize: false,
const config: AuthConfiguration = {
allowOfflineTxForUnknownId: false,
authorizationCacheEnabled: false,
- authorizationTimeout: 30000,
+ authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS,
certificateAuthEnabled: false,
localAuthListEnabled: false,
localPreAuthorize: false,
const config: AuthConfiguration = {
allowOfflineTxForUnknownId: false,
authorizationCacheEnabled: false,
- authorizationTimeout: 30000,
+ authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS,
certificateAuthEnabled: false,
localAuthListEnabled: true,
localPreAuthorize: false,
const config: AuthConfiguration = {
allowOfflineTxForUnknownId: false,
authorizationCacheEnabled: false,
- authorizationTimeout: 30000,
+ authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS,
certificateAuthEnabled: false,
localAuthListEnabled: false,
localPreAuthorize: false,
const config: AuthConfiguration = {
allowOfflineTxForUnknownId: false,
authorizationCacheEnabled: false,
- authorizationTimeout: 30000,
+ authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS,
certificateAuthEnabled: false,
localAuthListEnabled: false,
localPreAuthorize: false,
const config: AuthConfiguration = {
allowOfflineTxForUnknownId: false,
authorizationCacheEnabled: false,
- authorizationTimeout: 30000,
+ authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS,
certificateAuthEnabled: true,
localAuthListEnabled: false,
localPreAuthorize: false,
const config: AuthConfiguration = {
allowOfflineTxForUnknownId: false,
authorizationCacheEnabled: false,
- authorizationTimeout: 30000,
+ authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS,
certificateAuthEnabled: false,
localAuthListEnabled: false,
localPreAuthorize: false,
const config: AuthConfiguration = {
allowOfflineTxForUnknownId: false,
authorizationCacheEnabled: false,
- authorizationTimeout: 30000,
+ authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS,
certificateAuthEnabled: false,
localAuthListEnabled: true,
localPreAuthorize: false,
allowOfflineTxForUnknownId: false,
authorizationCacheEnabled: true,
authorizationCacheLifetime: 600,
- authorizationTimeout: 30000,
+ authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS,
certificateAuthEnabled: false,
localAuthListEnabled: true,
localPreAuthorize: false,
allowOfflineTxForUnknownId: false,
authorizationCacheEnabled: true,
authorizationCacheLifetime: -1, // Invalid
- authorizationTimeout: 30000,
+ authorizationTimeout: TEST_AUTHORIZATION_TIMEOUT_MS,
certificateAuthEnabled: false,
localAuthListEnabled: false,
localPreAuthorize: false,
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({
// 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'
})
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)
})
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)
})
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')
})
await it('should set application/json Content-Type header', () => {
- const server = new TestableUIHttpServer(createHttpServerConfig())
const res = new MockServerResponse()
server.addResponseHandler(TEST_UUID, res)
})
await it('should clean up response handlers after each response', () => {
- const server = new TestableUIHttpServer(createHttpServerConfig())
const res1 = new MockServerResponse()
const res2 = new MockServerResponse()
})
await it('should clear all handlers on server stop', () => {
- const server = new TestableUIHttpServer(createHttpServerConfig())
const res = new MockServerResponse()
server.addResponseHandler(TEST_UUID, res)
})
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'],
})
await it('should include error details in failure response', () => {
- const server = new TestableUIHttpServer(createHttpServerConfig())
const res = new MockServerResponse()
const payload = {
errorMessage: 'Test error',
})
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)
})
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)
})
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)
})
})
})
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)
})
})
})
- await describe('createBodySizeLimiter()', async () => {
+ await describe('CreateBodySizeLimiter', async () => {
+ let limiter: ReturnType<typeof createBodySizeLimiter>
+
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<typeof createRateLimiter>
+
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)
})
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)
})
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)
})
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)
})
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)
})
})
- 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)
})