* @description Unit tests for asynchronous lock utilities
*/
import { expect } from '@std/expect'
-import { randomInt } from 'node:crypto'
import { afterEach, describe, it } from 'node:test'
import { AsyncLock, AsyncLockType } from '../../src/utils/AsyncLock.js'
const executed: number[] = []
let count = 0
const asyncFn = async () => {
- await new Promise(resolve => {
- setTimeout(resolve, randomInt(1, 101))
+ // Yield to event loop without real timers
+ await new Promise<void>(resolve => {
+ queueMicrotask(resolve)
})
executed.push(++count)
}
await it('should isolate locks across different lock types', async () => {
const order: string[] = []
+ let resolveConfig!: () => void
const configPromise = AsyncLock.runExclusive(AsyncLockType.configuration, async () => {
- await new Promise(resolve => {
- setTimeout(resolve, 50)
+ await new Promise<void>(resolve => {
+ resolveConfig = resolve
})
order.push('configuration')
})
const perfPromise = AsyncLock.runExclusive(AsyncLockType.performance, () => {
order.push('performance')
})
- await Promise.all([configPromise, perfPromise])
+ await perfPromise
+ resolveConfig()
+ await configPromise
expect(order[0]).toBe('performance')
expect(order[1]).toBe('configuration')
})
--- /dev/null
+/**
+ * @file Tests for ChargingStationConfigurationUtils
+ * @description Unit tests for charging station configuration utility functions including
+ * buildConnectorsStatus, buildEvsesStatus, buildChargingStationAutomaticTransactionGeneratorConfiguration,
+ * and the OutputFormat enum.
+ */
+import { expect } from '@std/expect'
+import { afterEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
+import type { ConnectorStatus, EvseStatus } from '../../src/types/index.js'
+
+import { AvailabilityType } from '../../src/types/index.js'
+import {
+ buildChargingStationAutomaticTransactionGeneratorConfiguration,
+ buildConnectorsStatus,
+ buildEvsesStatus,
+ OutputFormat,
+} from '../../src/utils/ChargingStationConfigurationUtils.js'
+import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
+
+/**
+ * Creates a minimal mock ChargingStation for configuration utility tests.
+ * @param options - Mock station properties.
+ * @param options.automaticTransactionGenerator - ATG instance stub.
+ * @param options.connectors - Connectors map.
+ * @param options.evses - EVSEs map.
+ * @param options.getAutomaticTransactionGeneratorConfiguration - ATG config getter.
+ * @returns Partial ChargingStation cast for test use.
+ */
+function createMockStationForConfigUtils (options: {
+ automaticTransactionGenerator?: undefined | { connectorsStatus?: Map<number, unknown> }
+ connectors?: Map<number, ConnectorStatus>
+ evses?: Map<number, EvseStatus>
+ getAutomaticTransactionGeneratorConfiguration?: () => unknown
+}): ChargingStation {
+ return {
+ automaticTransactionGenerator: options.automaticTransactionGenerator ?? undefined,
+ connectors: options.connectors ?? new Map(),
+ evses: options.evses ?? new Map(),
+ getAutomaticTransactionGeneratorConfiguration:
+ options.getAutomaticTransactionGeneratorConfiguration ?? (() => undefined),
+ } as unknown as ChargingStation
+}
+
+await describe('ChargingStationConfigurationUtils', async () => {
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await describe('OutputFormat', async () => {
+ await it('should have correct enum values', () => {
+ expect(OutputFormat.configuration).toBe('configuration')
+ expect(OutputFormat.worker).toBe('worker')
+ })
+ })
+
+ await describe('buildConnectorsStatus', async () => {
+ await it('should strip internal transaction fields from connectors', () => {
+ const noop = (): void => {
+ /* noop */
+ }
+ const interval1 = setInterval(noop, 1000)
+ const interval2 = setInterval(noop, 1000)
+ try {
+ const connectors = new Map<number, ConnectorStatus>()
+ connectors.set(0, {
+ availability: AvailabilityType.Operative,
+ MeterValues: [],
+ } as ConnectorStatus)
+ connectors.set(1, {
+ availability: AvailabilityType.Operative,
+ bootStatus: 'Available',
+ MeterValues: [],
+ transactionEventQueue: [],
+ transactionSetInterval: interval1 as unknown as NodeJS.Timeout,
+ transactionTxUpdatedSetInterval: interval2 as unknown as NodeJS.Timeout,
+ } as unknown as ConnectorStatus)
+
+ const station = createMockStationForConfigUtils({ connectors })
+ const result = buildConnectorsStatus(station)
+
+ expect(result.length).toBe(2)
+ for (const connector of result) {
+ expect('transactionSetInterval' in connector).toBe(false)
+ expect('transactionEventQueue' in connector).toBe(false)
+ expect('transactionTxUpdatedSetInterval' in connector).toBe(false)
+ }
+ expect(result[1].availability).toBe(AvailabilityType.Operative)
+ } finally {
+ clearInterval(interval1)
+ clearInterval(interval2)
+ }
+ })
+
+ await it('should handle empty connectors map', () => {
+ const station = createMockStationForConfigUtils({ connectors: new Map() })
+ const result = buildConnectorsStatus(station)
+ expect(result.length).toBe(0)
+ })
+
+ await it('should preserve non-internal fields', () => {
+ const connectors = new Map<number, ConnectorStatus>()
+ connectors.set(1, {
+ availability: AvailabilityType.Operative,
+ bootStatus: 'Available',
+ MeterValues: [],
+ transactionEventQueue: undefined,
+ transactionId: 42,
+ transactionSetInterval: undefined,
+ transactionStarted: true,
+ transactionTxUpdatedSetInterval: undefined,
+ } as unknown as ConnectorStatus)
+
+ const station = createMockStationForConfigUtils({ connectors })
+ const result = buildConnectorsStatus(station)
+
+ expect(result.length).toBe(1)
+ expect(result[0].availability).toBe(AvailabilityType.Operative)
+ expect(result[0].transactionId).toBe(42)
+ expect(result[0].transactionStarted).toBe(true)
+ })
+ })
+
+ await describe('buildEvsesStatus', async () => {
+ await it('should return configuration format with connectorsStatus and without connectors', () => {
+ const evseConnectors = new Map<number, ConnectorStatus>()
+ evseConnectors.set(1, {
+ availability: AvailabilityType.Operative,
+ MeterValues: [],
+ transactionEventQueue: [],
+ transactionSetInterval: undefined,
+ transactionTxUpdatedSetInterval: undefined,
+ } as unknown as ConnectorStatus)
+
+ const evses = new Map<number, EvseStatus>()
+ evses.set(0, {
+ availability: AvailabilityType.Operative,
+ connectors: new Map<number, ConnectorStatus>(),
+ })
+ evses.set(1, {
+ availability: AvailabilityType.Operative,
+ connectors: evseConnectors,
+ })
+
+ const station = createMockStationForConfigUtils({ evses })
+ const result = buildEvsesStatus(station, OutputFormat.configuration)
+
+ expect(result.length).toBe(2)
+ const evse1 = result[1] as Record<string, unknown>
+ expect('connectorsStatus' in evse1).toBe(true)
+ expect('connectors' in evse1).toBe(false)
+ })
+
+ await it('should strip internal fields from evse connectors in configuration format', () => {
+ const evseConnectors = new Map<number, ConnectorStatus>()
+ evseConnectors.set(1, {
+ availability: AvailabilityType.Operative,
+ MeterValues: [],
+ transactionEventQueue: [],
+ transactionSetInterval: undefined,
+ transactionTxUpdatedSetInterval: undefined,
+ } as unknown as ConnectorStatus)
+
+ const evses = new Map<number, EvseStatus>()
+ evses.set(1, {
+ availability: AvailabilityType.Operative,
+ connectors: evseConnectors,
+ })
+
+ const station = createMockStationForConfigUtils({ evses })
+ const result = buildEvsesStatus(station, OutputFormat.configuration)
+ const evse1 = result[0] as Record<string, unknown>
+ const connectorsStatus = evse1.connectorsStatus as ConnectorStatus[]
+
+ expect(connectorsStatus.length).toBe(1)
+ expect('transactionSetInterval' in connectorsStatus[0]).toBe(false)
+ expect('transactionEventQueue' in connectorsStatus[0]).toBe(false)
+ expect('transactionTxUpdatedSetInterval' in connectorsStatus[0]).toBe(false)
+ })
+
+ await it('should return worker format with connectors array', () => {
+ const evseConnectors = new Map<number, ConnectorStatus>()
+ evseConnectors.set(1, {
+ availability: AvailabilityType.Operative,
+ MeterValues: [],
+ transactionEventQueue: undefined,
+ transactionSetInterval: undefined,
+ transactionTxUpdatedSetInterval: undefined,
+ } as unknown as ConnectorStatus)
+
+ const evses = new Map<number, EvseStatus>()
+ evses.set(0, {
+ availability: AvailabilityType.Operative,
+ connectors: new Map<number, ConnectorStatus>(),
+ })
+ evses.set(1, {
+ availability: AvailabilityType.Operative,
+ connectors: evseConnectors,
+ })
+
+ const station = createMockStationForConfigUtils({ evses })
+ const result = buildEvsesStatus(station, OutputFormat.worker)
+
+ expect(result.length).toBe(2)
+ const evse1 = result[1] as Record<string, unknown>
+ expect('connectors' in evse1).toBe(true)
+ expect(Array.isArray(evse1.connectors)).toBe(true)
+ })
+
+ await it('should default to configuration format when no format specified', () => {
+ const evses = new Map<number, EvseStatus>()
+ evses.set(1, {
+ availability: AvailabilityType.Operative,
+ connectors: new Map<number, ConnectorStatus>(),
+ })
+
+ const station = createMockStationForConfigUtils({ evses })
+ const result = buildEvsesStatus(station)
+
+ expect(result.length).toBe(1)
+ const evse = result[0] as Record<string, unknown>
+ expect('connectorsStatus' in evse).toBe(true)
+ expect('connectors' in evse).toBe(false)
+ })
+
+ await it('should handle empty evses map', () => {
+ const station = createMockStationForConfigUtils({ evses: new Map() })
+ const result = buildEvsesStatus(station, OutputFormat.configuration)
+ expect(result.length).toBe(0)
+ })
+
+ await it('should throw RangeError for unknown output format', () => {
+ const evses = new Map<number, EvseStatus>()
+ evses.set(1, {
+ availability: AvailabilityType.Operative,
+ connectors: new Map<number, ConnectorStatus>(),
+ })
+ const station = createMockStationForConfigUtils({ evses })
+
+ expect(() => {
+ buildEvsesStatus(station, 'unknown' as OutputFormat)
+ }).toThrow(RangeError)
+ })
+ })
+
+ await describe('buildChargingStationAutomaticTransactionGeneratorConfiguration', async () => {
+ await it('should return ATG configuration when present', () => {
+ const atgConfig = { enable: true, maxDuration: 120, minDuration: 60 }
+ const station = createMockStationForConfigUtils({
+ automaticTransactionGenerator: {
+ connectorsStatus: new Map([[1, { start: false }]]),
+ },
+ getAutomaticTransactionGeneratorConfiguration: () => atgConfig,
+ })
+ const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station)
+
+ expect(result.automaticTransactionGenerator).toStrictEqual(atgConfig)
+ expect(result.automaticTransactionGeneratorStatuses).toBeDefined()
+ expect(Array.isArray(result.automaticTransactionGeneratorStatuses)).toBe(true)
+ expect(result.automaticTransactionGeneratorStatuses?.length).toBe(1)
+ })
+
+ await it('should return ATG configuration without statuses when no ATG instance', () => {
+ const atgConfig = { enable: false }
+ const station = createMockStationForConfigUtils({
+ automaticTransactionGenerator: undefined,
+ getAutomaticTransactionGeneratorConfiguration: () => atgConfig,
+ })
+ const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station)
+
+ expect(result.automaticTransactionGenerator).toStrictEqual(atgConfig)
+ expect(result.automaticTransactionGeneratorStatuses).toBeUndefined()
+ })
+
+ await it('should return undefined ATG config when not configured', () => {
+ const station = createMockStationForConfigUtils({
+ automaticTransactionGenerator: undefined,
+ getAutomaticTransactionGeneratorConfiguration: () => undefined,
+ })
+ const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station)
+
+ expect(result.automaticTransactionGenerator).toBeUndefined()
+ expect(result.automaticTransactionGeneratorStatuses).toBeUndefined()
+ })
+
+ await it('should return ATG configuration without statuses when connectorsStatus is null', () => {
+ const atgConfig = { enable: true }
+ const station = createMockStationForConfigUtils({
+ automaticTransactionGenerator: {
+ connectorsStatus: undefined,
+ },
+ getAutomaticTransactionGeneratorConfiguration: () => atgConfig,
+ })
+ const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station)
+
+ expect(result.automaticTransactionGenerator).toStrictEqual(atgConfig)
+ expect(result.automaticTransactionGeneratorStatuses).toBeUndefined()
+ })
+ })
+})
WorkerConfiguration,
} from '../../src/types/index.js'
-import { ConfigurationSection, SupervisionUrlDistribution } from '../../src/types/index.js'
+import {
+ ApplicationProtocol,
+ ApplicationProtocolVersion,
+ ConfigurationSection,
+ StorageType,
+ SupervisionUrlDistribution,
+} from '../../src/types/index.js'
import { Configuration } from '../../src/utils/index.js'
import { WorkerProcessType } from '../../src/worker/WorkerTypes.js'
import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
ConfigurationSection.worker
)
expect(worker).toBeDefined()
- expect(worker.processType).toBeDefined()
- expect(worker.startDelay).toBeDefined()
- expect(worker.poolMinSize).toBeDefined()
- expect(worker.poolMaxSize).toBeDefined()
- expect(worker.elementsPerWorker).toBeDefined()
+ expect(worker.processType).toBe(WorkerProcessType.workerSet)
+ expect(worker.startDelay).toBe(500)
+ expect(typeof worker.poolMinSize).toBe('number')
+ expect(typeof worker.poolMaxSize).toBe('number')
+ expect(worker.elementsPerWorker).toBe('auto')
})
await it('should include default worker process type', () => {
ConfigurationSection.uiServer
)
expect(uiServer).toBeDefined()
- expect(typeof uiServer.enabled).toBe('boolean')
- expect(uiServer.type).toBeDefined()
- expect(uiServer.version).toBeDefined()
+ expect(uiServer.enabled).toBe(false)
+ expect(uiServer.type).toBe(ApplicationProtocol.WS)
+ expect(uiServer.version).toBe(ApplicationProtocolVersion.VERSION_11)
expect(uiServer.options).toBeDefined()
+ expect(typeof uiServer.options?.host).toBe('string')
+ expect(typeof uiServer.options?.port).toBe('number')
})
await it('should return performance storage configuration', () => {
ConfigurationSection.performanceStorage
)
expect(storage).toBeDefined()
- expect(typeof storage.enabled).toBe('boolean')
- expect(storage.type).toBeDefined()
+ expect(storage.enabled).toBe(true)
+ expect(storage.type).toBe(StorageType.NONE)
})
await it('should return station template URLs', () => {
await it('should return supervision URLs from configuration', () => {
const urls = Configuration.getSupervisionUrls()
- if (urls != null) {
- expect(typeof urls === 'string' || Array.isArray(urls)).toBe(true)
- }
+ expect(urls == null || typeof urls === 'string' || Array.isArray(urls)).toBe(true)
})
await it('should throw for unknown configuration section', () => {
import { expect } from '@std/expect'
import { afterEach, describe, it } from 'node:test'
-import { FileType, StorageType } from '../../src/types/index.js'
+import { StorageType } from '../../src/types/index.js'
import {
buildPerformanceUriFilePath,
checkWorkerElementsPerWorker,
getDefaultPerformanceStorageUri,
logPrefix,
} from '../../src/utils/ConfigurationUtils.js'
-import { handleFileException } from '../../src/utils/ErrorUtils.js'
import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
await describe('ConfigurationUtils', async () => {
}).toThrow(Error)
})
- await it('should throw and log error for file exceptions', t => {
- const mockConsoleError = t.mock.method(console, 'error')
- const error = new Error() as NodeJS.ErrnoException
- error.code = 'ENOENT'
- expect(() => {
- handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |', {
- consoleOut: true,
- })
- }).toThrow(error)
- expect(mockConsoleError.mock.calls.length).toBe(1)
- })
-
await it('should validate worker elements per worker configuration', () => {
// These calls should not throw exceptions
expect(() => {
expect(DCElectricUtils.power(0, 10)).toBe(0)
expect(DCElectricUtils.power(400, 0)).toBe(0)
})
+
+ // Realistic EV charging scenarios
+
+ await it('should calculate 7.4 kW single-phase AC home charger values', () => {
+ // 230V × 32A × 1 phase × cosPhi=1 = 7360W
+ expect(ACElectricUtils.powerPerPhase(230, 32)).toBe(7360)
+ expect(ACElectricUtils.powerTotal(1, 230, 32)).toBe(7360)
+ expect(ACElectricUtils.amperageTotalFromPower(7360, 230)).toBe(32)
+ expect(ACElectricUtils.amperagePerPhaseFromPower(1, 7360, 230)).toBe(32)
+ })
+
+ await it('should calculate 22 kW three-phase AC wall box values', () => {
+ // 230V × 32A × 3 phases × cosPhi=1 = 22080W
+ expect(ACElectricUtils.powerPerPhase(230, 32)).toBe(7360)
+ expect(ACElectricUtils.powerTotal(3, 230, 32)).toBe(22080)
+ expect(ACElectricUtils.amperageTotalFromPower(22080, 230)).toBe(96)
+ expect(ACElectricUtils.amperagePerPhaseFromPower(3, 22080, 230)).toBe(32)
+ })
+
+ await it('should calculate 50 kW DC fast charger values', () => {
+ expect(DCElectricUtils.power(400, 125)).toBe(50000)
+ expect(DCElectricUtils.amperage(50000, 400)).toBe(125)
+ })
+
+ await it('should calculate 150 kW DC high-power charger values', () => {
+ expect(DCElectricUtils.power(500, 300)).toBe(150000)
+ expect(DCElectricUtils.amperage(150000, 500)).toBe(300)
+ })
+
+ await it('should handle industrial cosPhi values for AC calculations', () => {
+ // cosPhi = 0.95 (typical industrial)
+ expect(ACElectricUtils.powerPerPhase(230, 32, 0.95)).toBe(6992)
+ expect(ACElectricUtils.powerTotal(3, 230, 32, 0.95)).toBe(20976)
+ expect(ACElectricUtils.amperageTotalFromPower(6992, 230, 0.95)).toBe(32)
+ // cosPhi = 0.9
+ expect(ACElectricUtils.powerPerPhase(230, 32, 0.9)).toBe(6624)
+ expect(ACElectricUtils.amperageTotalFromPower(6624, 230, 0.9)).toBe(32)
+ })
})
* @description Unit tests for error handling utilities
*/
import { expect } from '@std/expect'
-import { afterEach, describe, it } from 'node:test'
+import process from 'node:process'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
import {
FileType,
handleFileException,
handleIncomingRequestError,
handleSendMessageError,
+ handleUncaughtException,
+ handleUnhandledRejection,
} from '../../src/utils/ErrorUtils.js'
import { logger } from '../../src/utils/Logger.js'
import { TEST_CHARGING_STATION_BASE_NAME } from '../charging-station/ChargingStationTestConstants.js'
import { createMockChargingStation } from '../charging-station/ChargingStationTestUtils.js'
-import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
+import {
+ createConsoleMocks,
+ createLoggerMocks,
+ standardCleanup,
+} from '../helpers/TestLifecycleHelpers.js'
await describe('ErrorUtils', async () => {
- const { station: chargingStation } = createMockChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
+ let chargingStation: ChargingStation
+
+ beforeEach(() => {
+ const { station } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ })
+ chargingStation = station
})
afterEach(() => {
standardCleanup()
})
- await it('should throw or warn based on error code and options', t => {
- const consoleWarnMock = t.mock.method(console, 'warn')
- const consoleErrorMock = t.mock.method(console, 'error')
- const warnMock = t.mock.method(logger, 'warn')
- const errorMock = t.mock.method(logger, 'error')
+ await it('should throw error with logger output when throwError is true', t => {
+ const { errorMock } = createLoggerMocks(t, logger)
const error = new Error() as NodeJS.ErrnoException
error.code = 'ENOENT'
expect(() => {
handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |', {})
}).toThrow(error)
+ expect(errorMock.mock.calls.length).toBe(1)
+ })
+
+ await it('should log warning with logger when throwError is false', t => {
+ const { warnMock } = createLoggerMocks(t, logger)
+ const error = new Error() as NodeJS.ErrnoException
+ error.code = 'ENOENT'
expect(() => {
handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |', {
throwError: false,
})
}).not.toThrow()
expect(warnMock.mock.calls.length).toBe(1)
- expect(errorMock.mock.calls.length).toBe(1)
+ })
+
+ await it('should throw error with console output when consoleOut is true', t => {
+ const { errorMock } = createConsoleMocks(t, { error: true })
+ const error = new Error() as NodeJS.ErrnoException
+ error.code = 'ENOENT'
expect(() => {
handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |', {
consoleOut: true,
})
}).toThrow(error)
+ expect(errorMock?.mock.calls.length).toBe(1)
+ })
+
+ await it('should log console warning when consoleOut and throwError are false', t => {
+ const { warnMock } = createConsoleMocks(t, { error: true, warn: true })
+ const error = new Error() as NodeJS.ErrnoException
+ error.code = 'ENOENT'
expect(() => {
handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |', {
consoleOut: true,
throwError: false,
})
}).not.toThrow()
- expect(consoleWarnMock.mock.calls.length).toBe(1)
- expect(consoleErrorMock.mock.calls.length).toBe(1)
+ expect(warnMock?.mock.calls.length).toBe(1)
})
- await it('should log error and optionally throw for send message errors', t => {
- const errorMock = t.mock.method(logger, 'error')
- const logPrefixMock = t.mock.method(chargingStation, 'logPrefix')
+ await it('should produce correct log message for each error code', t => {
+ const { warnMock } = createLoggerMocks(t, logger)
+ const errorCodes = [
+ { code: 'ENOENT', expectedSubstring: 'not found' },
+ { code: 'EACCES', expectedSubstring: 'access denied' },
+ { code: 'EPERM', expectedSubstring: 'permission denied' },
+ { code: 'EISDIR', expectedSubstring: 'is a directory' },
+ { code: 'ENOSPC', expectedSubstring: 'no space left' },
+ { code: 'ENOTDIR', expectedSubstring: 'not a directory' },
+ { code: 'EEXIST', expectedSubstring: 'already exists' },
+ { code: 'EROFS', expectedSubstring: 'read-only' },
+ { code: 'UNKNOWN_CODE', expectedSubstring: 'error' },
+ ]
+ for (const { code } of errorCodes) {
+ const error = new Error() as NodeJS.ErrnoException
+ error.code = code
+ handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |', {
+ throwError: false,
+ })
+ }
+ expect(warnMock.mock.calls.length).toBe(errorCodes.length)
+ for (let i = 0; i < errorCodes.length; i++) {
+ const call = warnMock.mock.calls[i] as unknown as { arguments: unknown[] }
+ const logMessage = String(call.arguments[0]).toLowerCase()
+ expect(logMessage.includes(errorCodes[i].expectedSubstring)).toBe(true)
+ }
+ })
+
+ await it('should register uncaught exception handler on process', t => {
+ const onMock = t.mock.method(process, 'on')
+ handleUncaughtException()
+ expect(onMock.mock.calls.length).toBe(1)
+ expect(onMock.mock.calls[0].arguments[0]).toBe('uncaughtException')
+ })
+
+ await it('should register unhandled rejection handler on process', t => {
+ const onMock = t.mock.method(process, 'on')
+ handleUnhandledRejection()
+ expect(onMock.mock.calls.length).toBe(1)
+ expect(onMock.mock.calls[0].arguments[0]).toBe('unhandledRejection')
+ })
+
+ await it('should log error and not throw for send message errors by default', t => {
+ const { errorMock } = createLoggerMocks(t, logger)
+ t.mock.method(chargingStation, 'logPrefix')
const error = new Error()
expect(() => {
handleSendMessageError(
error
)
}).not.toThrow()
+ expect(errorMock.mock.calls.length).toBe(1)
+ })
+
+ await it('should throw for send message errors when throwError is true', t => {
+ const { errorMock } = createLoggerMocks(t, logger)
+ t.mock.method(chargingStation, 'logPrefix')
+ const error = new Error()
expect(() => {
handleSendMessageError(
chargingStation,
{ throwError: true }
)
}).toThrow(error)
- expect(logPrefixMock.mock.calls.length).toBe(2)
- expect(errorMock.mock.calls.length).toBe(2)
+ expect(errorMock.mock.calls.length).toBe(1)
})
- await it('should log error and return error response for incoming requests', t => {
- const errorMock = t.mock.method(logger, 'error')
- const logPrefixMock = t.mock.method(chargingStation, 'logPrefix')
+ await it('should log error and not throw for incoming request errors by default', t => {
+ const { errorMock } = createLoggerMocks(t, logger)
+ t.mock.method(chargingStation, 'logPrefix')
const error = new Error()
expect(() => {
handleIncomingRequestError(chargingStation, IncomingRequestCommand.CLEAR_CACHE, error)
}).not.toThrow(error)
+ expect(errorMock.mock.calls.length).toBe(1)
+ })
+
+ await it('should throw for incoming request errors when throwError is true', t => {
+ createLoggerMocks(t, logger)
+ t.mock.method(chargingStation, 'logPrefix')
+ const error = new Error()
expect(() => {
handleIncomingRequestError(chargingStation, IncomingRequestCommand.CLEAR_CACHE, error, {
throwError: true,
})
}).toThrow()
+ })
+
+ await it('should return error response for incoming request errors', t => {
+ const { errorMock } = createLoggerMocks(t, logger)
+ t.mock.method(chargingStation, 'logPrefix')
+ const error = new Error()
const errorResponse = {
status: GenericStatus.Rejected,
}
errorResponse,
})
).toStrictEqual(errorResponse)
- expect(logPrefixMock.mock.calls.length).toBe(3)
- expect(errorMock.mock.calls.length).toBe(3)
+ expect(errorMock.mock.calls.length).toBe(1)
})
})
--- /dev/null
+/**
+ * @file Tests for FileUtils
+ * @description Unit tests for file watching utility functions
+ */
+import { expect } from '@std/expect'
+import { mkdtempSync, rmSync, type WatchListener, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, it } from 'node:test'
+
+import { FileType } from '../../src/types/index.js'
+import { watchJsonFile } from '../../src/utils/FileUtils.js'
+import { logger } from '../../src/utils/Logger.js'
+import { createLoggerMocks, standardCleanup } from '../helpers/TestLifecycleHelpers.js'
+
+const noop: WatchListener<string> = () => {
+ /* intentionally empty */
+}
+
+await describe('FileUtils', async () => {
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should return undefined and log info for empty file path', t => {
+ const infoMock = t.mock.method(logger, 'info')
+
+ const result = watchJsonFile('', FileType.Authorization, 'test prefix |', noop)
+
+ expect(result).toBeUndefined()
+ expect(infoMock.mock.calls.length).toBe(1)
+ })
+
+ await it('should include file type and log prefix in info log message for empty path', t => {
+ const infoMock = t.mock.method(logger, 'info')
+
+ watchJsonFile('', FileType.ChargingStationConfiguration, 'CS-001 |', noop)
+
+ expect(infoMock.mock.calls.length).toBe(1)
+ const logMessage = infoMock.mock.calls[0].arguments[0] as unknown as string
+ expect(logMessage.includes(FileType.ChargingStationConfiguration)).toBe(true)
+ expect(logMessage.includes('CS-001 |')).toBe(true)
+ })
+
+ await it('should handle watch error and return undefined for nonexistent file', t => {
+ const { warnMock } = createLoggerMocks(t, logger)
+
+ const result = watchJsonFile(
+ '/nonexistent/path/to/file.json',
+ FileType.Authorization,
+ 'test prefix |',
+ noop
+ )
+
+ expect(result).toBeUndefined()
+ expect(warnMock.mock.calls.length).toBe(1)
+ })
+
+ await it('should return FSWatcher for valid file path', () => {
+ const tmpDir = mkdtempSync(join(tmpdir(), 'fileutils-test-'))
+ const tmpFile = join(tmpDir, 'test.json')
+ writeFileSync(tmpFile, '{}')
+
+ try {
+ const result = watchJsonFile(tmpFile, FileType.Authorization, 'test |', noop)
+
+ expect(result).toBeDefined()
+ result?.close()
+ } finally {
+ rmSync(tmpDir, { recursive: true })
+ }
+ })
+
+ await it('should call watch with file and listener arguments', () => {
+ const tmpDir = mkdtempSync(join(tmpdir(), 'fileutils-test-'))
+ const tmpFile = join(tmpDir, 'test.json')
+ writeFileSync(tmpFile, '{}')
+
+ try {
+ let receivedEvent = false
+ const listener: WatchListener<string> = () => {
+ receivedEvent = true
+ }
+
+ const result = watchJsonFile(tmpFile, FileType.Authorization, 'test |', listener)
+
+ expect(result).toBeDefined()
+ expect(typeof result?.close).toBe('function')
+ result?.close()
+ expect(receivedEvent).toBe(false)
+ } finally {
+ rmSync(tmpDir, { recursive: true })
+ }
+ })
+})
--- /dev/null
+/**
+ * @file Tests for MessageChannelUtils
+ * @description Unit tests for charging station worker message builders and performance statistics conversion
+ */
+import { expect } from '@std/expect'
+import { CircularBuffer } from 'mnemonist'
+import { afterEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
+import type { Statistics, TimestampedData } from '../../src/types/index.js'
+
+import { ChargingStationWorkerMessageEvents } from '../../src/types/index.js'
+import {
+ buildAddedMessage,
+ buildDeletedMessage,
+ buildPerformanceStatisticsMessage,
+ buildStartedMessage,
+ buildStoppedMessage,
+ buildUpdatedMessage,
+} from '../../src/utils/MessageChannelUtils.js'
+import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
+
+/**
+ * Creates a minimal mock station with properties needed by MessageChannelUtils builders.
+ * @returns Mock charging station instance
+ */
+function createMockStationForMessages (): ChargingStation {
+ return {
+ automaticTransactionGenerator: undefined,
+ bootNotificationResponse: {
+ currentTime: new Date('2024-01-01T00:00:00Z'),
+ interval: 300,
+ status: 'Accepted',
+ },
+ connectors: new Map([
+ [
+ 0,
+ {
+ availability: 'Operative',
+ MeterValues: [],
+ },
+ ],
+ [
+ 1,
+ {
+ availability: 'Operative',
+ MeterValues: [],
+ },
+ ],
+ ]),
+ evses: new Map(),
+ getAutomaticTransactionGeneratorConfiguration: () => undefined,
+ ocppConfiguration: { configurationKey: [] },
+ started: true,
+ stationInfo: {
+ baseName: 'CS-TEST',
+ chargingStationId: 'CS-TEST-00001',
+ hashId: 'test-hash',
+ templateIndex: 1,
+ templateName: 'test-template.json',
+ },
+ wsConnection: { readyState: 1 },
+ wsConnectionUrl: new URL('ws://localhost:8080/CS-TEST-00001'),
+ } as unknown as ChargingStation
+}
+
+await describe('MessageChannelUtils', async () => {
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should build added message with correct event and data', () => {
+ const station = createMockStationForMessages()
+ const message = buildAddedMessage(station)
+
+ expect(message.event).toBe(ChargingStationWorkerMessageEvents.added)
+ expect(message.data).toBeDefined()
+ expect(message.data.started).toBe(true)
+ expect(message.data.stationInfo.chargingStationId).toBe('CS-TEST-00001')
+ expect(message.data.supervisionUrl).toBe('ws://localhost:8080/CS-TEST-00001')
+ expect(typeof message.data.timestamp).toBe('number')
+ })
+
+ await it('should build deleted message with correct event', () => {
+ const station = createMockStationForMessages()
+ const message = buildDeletedMessage(station)
+
+ expect(message.event).toBe(ChargingStationWorkerMessageEvents.deleted)
+ expect(message.data).toBeDefined()
+ expect(message.data.stationInfo.chargingStationId).toBe('CS-TEST-00001')
+ })
+
+ await it('should build started message with correct event', () => {
+ const station = createMockStationForMessages()
+ const message = buildStartedMessage(station)
+
+ expect(message.event).toBe(ChargingStationWorkerMessageEvents.started)
+ expect(message.data.started).toBe(true)
+ })
+
+ await it('should build stopped message with correct event', () => {
+ const station = createMockStationForMessages()
+ const message = buildStoppedMessage(station)
+
+ expect(message.event).toBe(ChargingStationWorkerMessageEvents.stopped)
+ expect(message.data).toBeDefined()
+ expect(message.data.supervisionUrl).toBe('ws://localhost:8080/CS-TEST-00001')
+ })
+
+ await it('should build updated message with correct event', () => {
+ const station = createMockStationForMessages()
+ const message = buildUpdatedMessage(station)
+
+ expect(message.event).toBe(ChargingStationWorkerMessageEvents.updated)
+ expect(message.data).toBeDefined()
+ expect(message.data.stationInfo.chargingStationId).toBe('CS-TEST-00001')
+ })
+
+ await it('should include ws state in station messages', () => {
+ const station = createMockStationForMessages()
+ const message = buildAddedMessage(station)
+
+ expect(message.data.wsState).toBe(1)
+ })
+
+ await it('should include connectors status in station messages', () => {
+ const station = createMockStationForMessages()
+ const message = buildAddedMessage(station)
+
+ expect(Array.isArray(message.data.connectors)).toBe(true)
+ expect(message.data.connectors.length).toBe(2)
+ })
+
+ await it('should convert CircularBuffer to array in statistics data', () => {
+ const buffer = new CircularBuffer<TimestampedData>(Array, 10)
+ buffer.push({ timestamp: 1000, value: 42 })
+ buffer.push({ timestamp: 2000, value: 84 })
+
+ const statistics: Statistics = {
+ createdAt: new Date('2024-01-01T00:00:00Z'),
+ id: 'test-station-id',
+ name: 'test-station',
+ statisticsData: new Map([
+ [
+ 'Heartbeat',
+ {
+ measurementTimeSeries: buffer,
+ requestCount: 5,
+ responseCount: 5,
+ },
+ ],
+ ]),
+ uri: 'ws://localhost:8080',
+ }
+
+ const message = buildPerformanceStatisticsMessage(statistics)
+
+ expect(message.event).toBe(ChargingStationWorkerMessageEvents.performanceStatistics)
+ expect(message.data.id).toBe('test-station-id')
+ expect(message.data.name).toBe('test-station')
+
+ const heartbeatStats = message.data.statisticsData.get('Heartbeat')
+ expect(heartbeatStats).toBeDefined()
+ expect(Array.isArray(heartbeatStats?.measurementTimeSeries)).toBe(true)
+ const timeSeries = heartbeatStats?.measurementTimeSeries as TimestampedData[]
+ expect(timeSeries.length).toBe(2)
+ expect(timeSeries[0].value).toBe(42)
+ expect(timeSeries[1].value).toBe(84)
+ })
+
+ await it('should preserve non-CircularBuffer measurement time series', () => {
+ const statistics: Statistics = {
+ createdAt: new Date('2024-01-01T00:00:00Z'),
+ id: 'test-id',
+ name: 'test-station',
+ statisticsData: new Map([
+ [
+ 'Heartbeat',
+ {
+ measurementTimeSeries: [{ timestamp: 1000, value: 10 }],
+ requestCount: 3,
+ },
+ ],
+ ]),
+ uri: 'ws://localhost:8080',
+ }
+
+ const message = buildPerformanceStatisticsMessage(statistics)
+ const heartbeat = message.data.statisticsData.get('Heartbeat')
+ expect(Array.isArray(heartbeat?.measurementTimeSeries)).toBe(true)
+ })
+
+ await it('should preserve statistics metadata in performance message', () => {
+ const createdAt = new Date('2024-01-01T00:00:00Z')
+ const updatedAt = new Date('2024-01-02T00:00:00Z')
+
+ const statistics: Statistics = {
+ createdAt,
+ id: 'station-001',
+ name: 'station-name',
+ statisticsData: new Map(),
+ updatedAt,
+ uri: 'ws://localhost:8080',
+ }
+
+ const message = buildPerformanceStatisticsMessage(statistics)
+
+ expect(message.data.createdAt).toBe(createdAt)
+ expect(message.data.updatedAt).toBe(updatedAt)
+ expect(message.data.uri).toBe('ws://localhost:8080')
+ })
+})
import { hoursToMilliseconds, hoursToSeconds } from 'date-fns'
import { CircularBuffer } from 'mnemonist'
import { randomInt } from 'node:crypto'
+import process from 'node:process'
import { version } from 'node:process'
import { afterEach, describe, it } from 'node:test'
import { satisfies } from 'semver'
import type { TimestampedData } from '../../src/types/index.js'
import { JSRuntime, runtime } from '../../scripts/runtime.js'
+import { MapStringifyFormat } from '../../src/types/index.js'
import { Constants } from '../../src/utils/Constants.js'
import {
clampToSafeTimerValue,
formatDurationSeconds,
generateUUID,
getRandomFloat,
+ getRandomFloatFluctuatedRounded,
+ getRandomFloatRounded,
+ getWebSocketCloseEventStatusString,
has,
insertAt,
isArraySorted,
isAsyncFunction,
+ isCFEnvironment,
isEmpty,
isNotEmptyArray,
isNotEmptyString,
isValidDate,
+ JSONStringify,
+ logPrefix,
+ mergeDeepRight,
once,
+ queueMicrotaskErrorThrowing,
roundTo,
secureRandom,
sleep,
await it('should generate valid UUIDs and validate them correctly', () => {
const uuid = generateUUID()
expect(uuid).toBeDefined()
- expect(uuid.length).toStrictEqual(36)
+ expect(uuid.length).toBe(36)
expect(validateUUID(uuid)).toBe(true)
expect(validateUUID('abcdef00-0000-4000-9000-000000000000')).toBe(true)
expect(validateUUID('abcdef00-0000-4000-a000-000000000000')).toBe(true)
expect(convertToInt(null)).toBe(0)
expect(convertToInt(0)).toBe(0)
const randomInteger = randomInt(Constants.MAX_RANDOM_INTEGER)
- expect(convertToInt(randomInteger)).toStrictEqual(randomInteger)
+ expect(convertToInt(randomInteger)).toBe(randomInteger)
expect(convertToInt('-1')).toBe(-1)
expect(convertToInt('1')).toBe(1)
expect(convertToInt('1.1')).toBe(1)
expect(convertToFloat(null)).toBe(0)
expect(convertToFloat(0)).toBe(0)
const randomFloat = getRandomFloat()
- expect(convertToFloat(randomFloat)).toStrictEqual(randomFloat)
+ expect(convertToFloat(randomFloat)).toBe(randomFloat)
expect(convertToFloat('-1')).toBe(-1)
expect(convertToFloat('1')).toBe(1)
expect(convertToFloat('1.1')).toBe(1.1)
})
await it('should correctly identify async functions from other types', () => {
- expect(isAsyncFunction(null)).toBe(false)
- expect(isAsyncFunction(undefined)).toBe(false)
- expect(isAsyncFunction(true)).toBe(false)
- expect(isAsyncFunction(false)).toBe(false)
- expect(isAsyncFunction(0)).toBe(false)
- expect(isAsyncFunction('')).toBe(false)
- expect(isAsyncFunction([])).toBe(false)
- expect(isAsyncFunction(new Date())).toBe(false)
- expect(isAsyncFunction(/[a-z]/i)).toBe(false)
- expect(isAsyncFunction(new Error())).toBe(false)
- expect(isAsyncFunction(new Map())).toBe(false)
- expect(isAsyncFunction(new Set())).toBe(false)
- expect(isAsyncFunction(new WeakMap())).toBe(false)
- expect(isAsyncFunction(new WeakSet())).toBe(false)
- expect(isAsyncFunction(new Int8Array())).toBe(false)
- expect(isAsyncFunction(new Uint8Array())).toBe(false)
- expect(isAsyncFunction(new Uint8ClampedArray())).toBe(false)
- expect(isAsyncFunction(new Int16Array())).toBe(false)
- expect(isAsyncFunction(new Uint16Array())).toBe(false)
- expect(isAsyncFunction(new Int32Array())).toBe(false)
- expect(isAsyncFunction(new Uint32Array())).toBe(false)
- expect(isAsyncFunction(new Float32Array())).toBe(false)
- expect(isAsyncFunction(new Float64Array())).toBe(false)
- expect(isAsyncFunction(new BigInt64Array())).toBe(false)
- expect(isAsyncFunction(new BigUint64Array())).toBe(false)
/* eslint-disable @typescript-eslint/no-empty-function -- Testing with empty functions to verify isAsyncFunction correctly identifies async vs sync */
- expect(isAsyncFunction(new Promise(() => {}))).toBe(false)
- expect(isAsyncFunction(new WeakRef({}))).toBe(false)
- expect(isAsyncFunction(new FinalizationRegistry(() => {}))).toBe(false)
- expect(isAsyncFunction(new ArrayBuffer(16))).toBe(false)
- expect(isAsyncFunction(new SharedArrayBuffer(16))).toBe(false)
- expect(isAsyncFunction(new DataView(new ArrayBuffer(16)))).toBe(false)
- expect(isAsyncFunction({})).toBe(false)
- expect(isAsyncFunction({ a: 1 })).toBe(false)
- expect(isAsyncFunction(() => {})).toBe(false)
- expect(isAsyncFunction(function () {})).toBe(false)
- expect(isAsyncFunction(function named () {})).toBe(false)
- expect(isAsyncFunction(async () => {})).toBe(true)
- expect(isAsyncFunction(async function () {})).toBe(true)
- expect(isAsyncFunction(async function named () {})).toBe(true)
+ const nonAsyncValues: unknown[] = [
+ null,
+ undefined,
+ true,
+ false,
+ 0,
+ '',
+ [],
+ new Date(),
+ /[a-z]/i,
+ new Error(),
+ new Map(),
+ new Set(),
+ new WeakMap(),
+ new WeakSet(),
+ new Int8Array(),
+ new Uint8Array(),
+ new Uint8ClampedArray(),
+ new Int16Array(),
+ new Uint16Array(),
+ new Int32Array(),
+ new Uint32Array(),
+ new Float32Array(),
+ new Float64Array(),
+ new BigInt64Array(),
+ new BigUint64Array(),
+ new Promise(() => {}),
+ new WeakRef({}),
+ new FinalizationRegistry(() => {}),
+ new ArrayBuffer(16),
+ new SharedArrayBuffer(16),
+ new DataView(new ArrayBuffer(16)),
+ {},
+ { a: 1 },
+ () => {},
+ function () {},
+ function named () {},
+ ]
+ for (const value of nonAsyncValues) {
+ expect(isAsyncFunction(value)).toBe(false)
+ }
+
+ const asyncValues: unknown[] = [async () => {}, async function () {}, async function named () {}]
+ for (const value of asyncValues) {
+ expect(isAsyncFunction(value)).toBe(true)
+ }
/* eslint-enable @typescript-eslint/no-empty-function */
+
class TestClass {
/* eslint-disable @typescript-eslint/no-empty-function -- Testing class methods and properties */
public static async testStaticAsync (): Promise<void> {}
expect(maxDelay).toBeGreaterThanOrEqual(102400) // ~102 seconds
expect(maxDelay).toBeLessThanOrEqual(122880)
})
+
+ await it('should return timestamped log prefix with optional string', () => {
+ const result = logPrefix()
+ expect(typeof result).toBe('string')
+ expect(result.length).toBeGreaterThan(0)
+ const withPrefix = logPrefix(' Test |')
+ expect(withPrefix).toContain(' Test |')
+ })
+
+ await it('should deep merge objects with source overriding target', () => {
+ // Simple merge
+ expect(mergeDeepRight({ a: 1 }, { b: 2 })).toStrictEqual({ a: 1, b: 2 })
+ // Source overrides target
+ expect(mergeDeepRight({ a: 1 }, { a: 2 })).toStrictEqual({ a: 2 })
+ // Nested merge
+ expect(mergeDeepRight({ a: { b: 1, c: 2 } }, { a: { c: 3, d: 4 } })).toStrictEqual({
+ a: { b: 1, c: 3, d: 4 },
+ })
+ // Deeply nested
+ expect(mergeDeepRight({ a: { b: { c: 1 } } }, { a: { b: { d: 2 } } })).toStrictEqual({
+ a: { b: { c: 1, d: 2 } },
+ })
+ // Non-object source value replaces target object
+ expect(mergeDeepRight({ a: { b: 1 } }, { a: 'string' })).toStrictEqual({ a: 'string' })
+ // Empty objects
+ expect(mergeDeepRight({}, { a: 1 })).toStrictEqual({ a: 1 })
+ expect(mergeDeepRight({ a: 1 }, {})).toStrictEqual({ a: 1 })
+ })
+
+ await it('should stringify objects with Map and Set support', () => {
+ // Basic object
+ expect(JSONStringify({ a: 1 })).toBe('{"a":1}')
+ // Map as array (default)
+ const map = new Map([['key', { value: 1 }]])
+ expect(JSONStringify(map)).toBe('[["key",{"value":1}]]')
+ // Map as object
+ expect(JSONStringify(map, undefined, MapStringifyFormat.object)).toBe('{"key":{"value":1}}')
+ // Set
+ const set = new Set([{ a: 1 }])
+ expect(JSONStringify(set)).toBe('[{"a":1}]')
+ // With space formatting
+ expect(JSONStringify({ a: 1 }, 2)).toBe('{\n "a": 1\n}')
+ })
+
+ await it('should return human readable string for websocket close codes', () => {
+ // Known codes
+ expect(getWebSocketCloseEventStatusString(1000)).toBe('Normal Closure')
+ expect(getWebSocketCloseEventStatusString(1001)).toBe('Going Away')
+ expect(getWebSocketCloseEventStatusString(1006)).toBe('Abnormal Closure')
+ expect(getWebSocketCloseEventStatusString(1011)).toBe('Server Internal Error')
+ // Ranges
+ expect(getWebSocketCloseEventStatusString(0)).toBe('(Unused)')
+ expect(getWebSocketCloseEventStatusString(999)).toBe('(Unused)')
+ expect(getWebSocketCloseEventStatusString(1016)).toBe('(For WebSocket standard)')
+ expect(getWebSocketCloseEventStatusString(1999)).toBe('(For WebSocket standard)')
+ expect(getWebSocketCloseEventStatusString(2000)).toBe('(For WebSocket extensions)')
+ expect(getWebSocketCloseEventStatusString(2999)).toBe('(For WebSocket extensions)')
+ expect(getWebSocketCloseEventStatusString(3000)).toBe('(For libraries and frameworks)')
+ expect(getWebSocketCloseEventStatusString(3999)).toBe('(For libraries and frameworks)')
+ expect(getWebSocketCloseEventStatusString(4000)).toBe('(For applications)')
+ expect(getWebSocketCloseEventStatusString(4999)).toBe('(For applications)')
+ // Unknown
+ expect(getWebSocketCloseEventStatusString(5000)).toBe('(Unknown)')
+ })
+
+ await it('should generate random float rounded to specified scale', () => {
+ const result = getRandomFloatRounded(10, 0, 2)
+ expect(result).toBeGreaterThanOrEqual(0)
+ expect(result).toBeLessThanOrEqual(10)
+ // Check rounding to 2 decimal places
+ const decimalStr = result.toString()
+ if (decimalStr.includes('.')) {
+ expect(decimalStr.split('.')[1].length).toBeLessThanOrEqual(2)
+ }
+ // Default scale
+ const defaultScale = getRandomFloatRounded(10, 0)
+ expect(defaultScale).toBeGreaterThanOrEqual(0)
+ expect(defaultScale).toBeLessThanOrEqual(10)
+ })
+
+ await it('should generate fluctuated random float within percentage range', () => {
+ // 0% fluctuation returns static value rounded
+ expect(getRandomFloatFluctuatedRounded(100, 0)).toBe(100)
+ // 10% fluctuation: 100 ± 10
+ const result = getRandomFloatFluctuatedRounded(100, 10)
+ expect(result).toBeGreaterThanOrEqual(90)
+ expect(result).toBeLessThanOrEqual(110)
+ // Invalid fluctuation percent
+ expect(() => getRandomFloatFluctuatedRounded(100, -1)).toThrow(RangeError)
+ expect(() => getRandomFloatFluctuatedRounded(100, 101)).toThrow(RangeError)
+ // Negative static value with fluctuation
+ const negResult = getRandomFloatFluctuatedRounded(-100, 10)
+ expect(negResult).toBeGreaterThanOrEqual(-110)
+ expect(negResult).toBeLessThanOrEqual(-90)
+ })
+
+ await it('should detect Cloud Foundry environment from VCAP_APPLICATION', () => {
+ const originalVcap = process.env.VCAP_APPLICATION
+ try {
+ delete process.env.VCAP_APPLICATION
+ expect(isCFEnvironment()).toBe(false)
+ process.env.VCAP_APPLICATION = '{}'
+ expect(isCFEnvironment()).toBe(true)
+ } finally {
+ if (originalVcap != null) {
+ process.env.VCAP_APPLICATION = originalVcap
+ } else {
+ delete process.env.VCAP_APPLICATION
+ }
+ }
+ })
+
+ await it('should queue microtask that throws the given error', t => {
+ const error = new Error('test microtask error')
+ // eslint-disable-next-line @typescript-eslint/no-empty-function -- Mock queueMicrotask with no-op to prevent actual throw
+ const mockFn = t.mock.method(globalThis, 'queueMicrotask', () => {})
+ queueMicrotaskErrorThrowing(error)
+ expect(mockFn.mock.callCount()).toBe(1)
+ const callback = mockFn.mock.calls[0].arguments[0] as () => void
+ expect(() => {
+ callback()
+ }).toThrow(error)
+ })
})