]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
fix(simulator): seed autoRegister and ocppProtocol defaults into stationInfo (#2082)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Fri, 14 Aug 2026 13:39:35 +0000 (15:39 +0200)
committerGitHub <noreply@github.com>
Fri, 14 Aug 2026 13:39:35 +0000 (15:39 +0200)
* fix(simulator): seed autoRegister and ocppProtocol defaults into stationInfo

autoRegister and ocppProtocol had no default, so a template omitting them
left stationInfo carrying undefined for both. Raw consumers of stationInfo
— the UI data payload (buildChargingStationDataPayload) and the persisted
configuration — therefore received undefined, so the Web UI station details
showed an empty placeholder for Auto Register and OCPP Protocol.

Both are static defaults (no derivation), so seed them in DEFAULT_STATION_INFO
(autoRegister: false, ocppProtocol: OCPPProtocol.JSON) alongside currentOutType
and ocppVersion. getStationInfo applies them via
mergeDeepRight(DEFAULT_STATION_INFO, stationInfo), so an explicit template/file
value or option still wins (idempotent, no clobber) and a persisted config
predating the fields is backfilled on reload. Export OCPPProtocol from the
types barrel to keep Constants.ts importing types from a single source, as
OCPPVersion already does.

Runtime-neutral: ocppProtocol has no runtime read; the autoRegister === true
guards are unaffected by false vs undefined; the getHeartbeatInterval warn
guarded by === false is unreachable in the initialized pipeline because
initializeOcppConfiguration always seeds the HeartbeatInterval key first.

* refactor(test): extract persisted-config resolution into realStation helpers

The persisted configuration file/dir resolution from a real-station template
was inlined and duplicated across three suites (AutoRegisterOcppProtocol,
NumberOfPhases, ResetIdentity). Extract persistedConfigurationDir and
resolvePersistedConfigurationFile into StationHelpers.realStation.ts (single
source of truth for the temp-dir layout) and migrate all three call sites.
resolvePersistedConfigurationFile throws a descriptive error when no config
exists yet, replacing the silent `?? ''` fallback (which degraded into EISDIR).

Group the helper by concern (temp-dir lifecycle / construction / persisted-config
resolution) with section headers, and document the two new helpers in
TEST_STYLE_GUIDE.md.

* docs(test): harmonize helper-table param placeholders in TEST_STYLE_GUIDE

src/types/index.ts
src/utils/Constants.ts
tests/TEST_STYLE_GUIDE.md
tests/charging-station/ChargingStation-AutoRegisterOcppProtocol.test.ts [new file with mode: 0644]
tests/charging-station/ChargingStation-NumberOfPhases.test.ts
tests/charging-station/ChargingStation-ResetIdentity.test.ts
tests/charging-station/helpers/StationHelpers.realStation.ts

index e832da5d646bbf7fbf5d31925b9ae65dd54e8093..d371d6cef630f8bc9f57d9674bbc3444cda01d64 100644 (file)
@@ -373,6 +373,7 @@ export {
   MeterValueUnit,
   type SampledValue,
 } from './ocpp/MeterValues.js'
+export { OCPPProtocol } from './ocpp/OCPPProtocol.js'
 export { OCPPVersion } from './ocpp/OCPPVersion.js'
 export {
   AvailabilityType,
index d2f05726fc38c72018a83ab02eeab784c665e7d0..7e277aa854a9c042191bccfb2ce170af911ada80 100644 (file)
@@ -2,6 +2,7 @@ import {
   type AutomaticTransactionGeneratorConfiguration,
   type ChargingStationInfo,
   CurrentType,
+  OCPPProtocol,
   OCPPVersion,
   VendorParametersKey,
 } from '../types/index.js'
@@ -81,6 +82,7 @@ export class Constants {
   static readonly DEFAULT_STATION_INFO: Readonly<Partial<ChargingStationInfo>> = Object.freeze({
     automaticTransactionGeneratorPersistentConfiguration: true,
     autoReconnectMaxRetries: -1,
+    autoRegister: false,
     autoStart: true,
     beginEndMeterValues: false,
     currentOutType: CurrentType.AC,
@@ -99,6 +101,7 @@ export class Constants {
     mainVoltageMeterValues: true,
     meteringPerTransaction: true,
     ocppPersistentConfiguration: true,
+    ocppProtocol: OCPPProtocol.JSON,
     ocppStrictCompliance: true,
     ocppVersion: OCPPVersion.VERSION_16,
     outOfOrderEndMeterValues: false,
index 6d36002f508165b2460bd2436664e9704a272be9..37d50b2e0b2c70059d33ef91e83bb7313d3d8af7 100644 (file)
@@ -327,12 +327,14 @@ Tests exercising the real construction pipeline (persistence, reset, reconnect,
 parsing) MUST build a real station from a template file via
 `helpers/StationHelpers.realStation.ts` — never re-implement the temp-dir scaffolding:
 
-| Helper                        | Purpose                                                     |
-| ----------------------------- | ----------------------------------------------------------- |
-| `writeStationTemplate(obj)`   | Write an inline template object into an isolated temp dir   |
-| `copyStationTemplate(ovr?)`   | Copy a bundled asset template, optionally merging overrides |
-| `createStationFromTemplate()` | Construct a real `ChargingStation` from a template file     |
-| `cleanupStationTemplates()`   | Remove temp template dirs (call in `afterEach`)             |
+| Helper                                    | Purpose                                                     |
+| ----------------------------------------- | ----------------------------------------------------------- |
+| `writeStationTemplate(obj)`               | Write an inline template object into an isolated temp dir   |
+| `copyStationTemplate(ovr?)`               | Copy a bundled asset template, optionally merging overrides |
+| `cleanupStationTemplates()`               | Remove temp template dirs (call in `afterEach`)             |
+| `createStationFromTemplate()`             | Construct a real `ChargingStation` from a template file     |
+| `persistedConfigurationDir(file)`         | Resolve the sibling `configurations` dir of a template file |
+| `resolvePersistedConfigurationFile(file)` | Resolve the persisted config file (throws if none yet)      |
 
 ```typescript
 afterEach(() => {
diff --git a/tests/charging-station/ChargingStation-AutoRegisterOcppProtocol.test.ts b/tests/charging-station/ChargingStation-AutoRegisterOcppProtocol.test.ts
new file mode 100644 (file)
index 0000000..64df91d
--- /dev/null
@@ -0,0 +1,113 @@
+/**
+ * @file Tests for `autoRegister` and `ocppProtocol` default seeding into `stationInfo`.
+ * @description Both fields are static defaults with no derivation, so they live in
+ * `DEFAULT_STATION_INFO` (`autoRegister: false`, `ocppProtocol: OCPPProtocol.JSON`)
+ * and are applied by `getStationInfo` via `mergeDeepRight(DEFAULT_STATION_INFO, stationInfo)`.
+ * A template omitting them must yield the seeded values in `stationInfo` and in the UI
+ * data payload (`buildAddedMessage`) instead of `undefined` (the empty Web UI placeholder).
+ * An explicit template value is preserved (idempotent, no clobber), and a persisted
+ * configuration predating the fields is backfilled on reload. `ocppProtocol` preservation
+ * is not tested: the enum has a single value, so an explicit value is indistinguishable
+ * from the default (non-discriminant).
+ */
+import assert from 'node:assert/strict'
+import { readFileSync, writeFileSync } from 'node:fs'
+import { afterEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
+
+import { OCPPProtocol } from '../../src/types/index.js'
+import { buildAddedMessage } from '../../src/utils/MessageChannelUtils.js'
+import { flushMicrotasks, standardCleanup } from '../helpers/TestLifecycleHelpers.js'
+import {
+  cleanupStationTemplates,
+  createStationFromTemplate,
+  resolvePersistedConfigurationFile,
+  writeStationTemplate,
+} from './helpers/StationHelpers.realStation.js'
+
+const POWER_W = 22000
+
+interface TemplateOverrides {
+  autoRegister?: boolean
+}
+
+const buildTemplate = (overrides: TemplateOverrides = {}): Record<string, unknown> => ({
+  $schemaVersion: 1,
+  baseName: 'TEST-AUTO-REGISTER-OCPP-PROTOCOL',
+  chargePointModel: 'Simulator simple',
+  chargePointVendor: 'Simulator',
+  Connectors: {
+    0: {},
+    1: { bootStatus: 'Available' },
+  },
+  currentOutType: 'AC',
+  numberOfConnectors: 1,
+  power: POWER_W,
+  powerUnit: 'W',
+  randomConnectors: false,
+  ...(overrides.autoRegister != null ? { autoRegister: overrides.autoRegister } : {}),
+})
+
+const makeStation = (templateFile: string, persistentConfiguration = false): ChargingStation =>
+  createStationFromTemplate(templateFile, {
+    baseName: 'TEST-AUTO-REGISTER-OCPP-PROTOCOL',
+    fixedName: true,
+    persistentConfiguration,
+  })
+
+const newStation = (overrides: TemplateOverrides = {}): ChargingStation =>
+  makeStation(writeStationTemplate(buildTemplate(overrides)))
+
+await describe('ChargingStation autoRegister/ocppProtocol seeding', async () => {
+  afterEach(() => {
+    standardCleanup()
+    cleanupStationTemplates()
+  })
+
+  await it('should seed autoRegister to false for a template omitting the field', () => {
+    const station = newStation()
+    assert.strictEqual(station.stationInfo?.autoRegister, false)
+  })
+
+  await it('should seed ocppProtocol to json for a template omitting the field', () => {
+    const station = newStation()
+    assert.strictEqual(station.stationInfo?.ocppProtocol, OCPPProtocol.JSON)
+  })
+
+  await it('should transmit the seeded defaults in the UI data payload', () => {
+    const station = newStation()
+    const { stationInfo } = buildAddedMessage(station).data
+    assert.strictEqual(stationInfo.autoRegister, false)
+    assert.strictEqual(stationInfo.ocppProtocol, OCPPProtocol.JSON)
+  })
+
+  await it('should preserve an explicit autoRegister template value', () => {
+    const station = newStation({ autoRegister: true })
+    assert.strictEqual(station.stationInfo?.autoRegister, true)
+  })
+
+  await it('should backfill the defaults into a persisted config that predates the fields', async () => {
+    const templateFile = writeStationTemplate(buildTemplate())
+    // The persisted config write runs under an async lock; flush before reading it back.
+    makeStation(templateFile, true)
+    await flushMicrotasks()
+    // Simulate a legacy configuration written before the fields were seeded.
+    const configurationFile = resolvePersistedConfigurationFile(templateFile)
+    const configuration = JSON.parse(readFileSync(configurationFile, 'utf8')) as {
+      stationInfo: { autoRegister?: boolean; ocppProtocol?: string }
+    }
+    assert.strictEqual(configuration.stationInfo.autoRegister, false)
+    assert.strictEqual(configuration.stationInfo.ocppProtocol, OCPPProtocol.JSON)
+    delete configuration.stationInfo.autoRegister
+    delete configuration.stationInfo.ocppProtocol
+    writeFileSync(configurationFile, JSON.stringify(configuration), 'utf8')
+    // A fresh station starts with an empty configurationFileHash, so getConfigurationFromFile
+    // bypasses the shared cache and reads the file from disk; the file-sourced stationInfo
+    // is backfilled by mergeDeepRight(DEFAULT_STATION_INFO, stationInfo) in getStationInfo.
+    const reloaded = makeStation(templateFile, true)
+    assert.ok(reloaded.stationInfo)
+    assert.strictEqual(reloaded.stationInfo.autoRegister, false)
+    assert.strictEqual(reloaded.stationInfo.ocppProtocol, OCPPProtocol.JSON)
+  })
+})
index 920089c77dcc1cae5d5aad9745c6a3aab844dfdf..db6e0907879712444345d7f0026be735d82315e5 100644 (file)
@@ -10,8 +10,7 @@
  * backfilled on reload.
  */
 import assert from 'node:assert/strict'
-import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
-import { dirname, join } from 'node:path'
+import { readFileSync, writeFileSync } from 'node:fs'
 import { afterEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
@@ -21,6 +20,7 @@ import { flushMicrotasks, standardCleanup } from '../helpers/TestLifecycleHelper
 import {
   cleanupStationTemplates,
   createStationFromTemplate,
+  resolvePersistedConfigurationFile,
   writeStationTemplate,
 } from './helpers/StationHelpers.realStation.js'
 
@@ -103,11 +103,7 @@ await describe('ChargingStation numberOfPhases seeding', async () => {
     makeStation(templateFile, true)
     await flushMicrotasks()
     // Simulate a legacy configuration written before numberOfPhases was seeded.
-    const configurationDir = join(dirname(dirname(templateFile)), 'configurations')
-    const configurationFile = join(
-      configurationDir,
-      readdirSync(configurationDir).find(entry => entry.endsWith('.json')) ?? ''
-    )
+    const configurationFile = resolvePersistedConfigurationFile(templateFile)
     const configuration = JSON.parse(readFileSync(configurationFile, 'utf8')) as {
       stationInfo: { numberOfPhases?: number }
     }
index 9d0ff0b854e287ed22cd0dc6c8bd5c2e81185df9..1a2fc72a766065982d0795bfbba2cb927ad5949b 100644 (file)
@@ -6,7 +6,7 @@
  */
 import assert from 'node:assert/strict'
 import { existsSync, readdirSync, readFileSync } from 'node:fs'
-import { dirname, join } from 'node:path'
+import { join } from 'node:path'
 import { afterEach, describe, it } from 'node:test'
 import { setTimeout as sleep } from 'node:timers/promises'
 
@@ -19,6 +19,7 @@ import {
   cleanupStationTemplates,
   copyStationTemplate,
   createStationFromTemplate,
+  persistedConfigurationDir,
 } from './helpers/StationHelpers.realStation.js'
 
 // The identity logic lives in initialize(); the identity tests call it directly
@@ -41,7 +42,7 @@ const waitForPersistedId = async (
   templateFile: string,
   chargingStationId: string
 ): Promise<boolean> => {
-  const configurationsDir = dirname(templateFile.replace('station-templates', 'configurations'))
+  const configurationsDir = persistedConfigurationDir(templateFile)
   for (let attempt = 0; attempt < 100; attempt++) {
     if (existsSync(configurationsDir)) {
       for (const file of readdirSync(configurationsDir)) {
index c58655852915638e0fb9491a616b3528487d2cd2..87e32a70b534b8390c0333cf5bb06d4fdaacdfbe 100644 (file)
@@ -2,15 +2,25 @@
  * @file Helpers to build a real ChargingStation from a template file.
  * @description Tests that exercise the real construction pipeline
  * (`initialize()`/`getStationInfo()`, persistence, reset, reconnect) cannot use
- * the `createMockChargingStation` stub, which bypasses it. These helpers write a
- * template into an isolated temp `station-templates` dir and construct a real
- * `ChargingStation` from it, tracking temp roots for a single `afterEach`
- * cleanup. Two template sources are supported: an inline object
- * (`writeStationTemplate`) and a bundled asset (`copyStationTemplate`).
+ * the `createMockChargingStation` stub, which bypasses it. Grouped by concern:
+ * (1) temp `station-templates` dir lifecycle — write an inline template
+ * (`writeStationTemplate`) or copy a bundled asset (`copyStationTemplate`),
+ * removed by a single `afterEach` `cleanupStationTemplates`; (2) real
+ * `ChargingStation` construction (`createStationFromTemplate`); (3) resolution of
+ * the persisted configuration the station writes (`persistedConfigurationDir`,
+ * `resolvePersistedConfigurationFile`).
  */
-import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import {
+  copyFileSync,
+  mkdirSync,
+  mkdtempSync,
+  readdirSync,
+  readFileSync,
+  rmSync,
+  writeFileSync,
+} from 'node:fs'
 import { tmpdir } from 'node:os'
-import { join } from 'node:path'
+import { dirname, join } from 'node:path'
 
 import type { ChargingStationOptions } from '../../../src/types/index.js'
 
@@ -29,6 +39,10 @@ const freshTemplateDir = (): string => {
   return root
 }
 
+// ---------------------------------------------------------------
+// Temp template dir lifecycle
+// ---------------------------------------------------------------
+
 /**
  * Writes an inline template object into a fresh isolated `station-templates` dir.
  * @param template - Template object to serialize.
@@ -66,6 +80,20 @@ export const copyStationTemplate = (
   return file
 }
 
+/**
+ * Removes every temp template dir created by `writeStationTemplate`/`copyStationTemplate`.
+ * Call in `afterEach`, alongside `standardCleanup()`.
+ */
+export const cleanupStationTemplates = (): void => {
+  for (const root of templateRoots.splice(0)) {
+    rmSync(root, { force: true, recursive: true })
+  }
+}
+
+// ---------------------------------------------------------------
+// Real station construction
+// ---------------------------------------------------------------
+
 /**
  * Constructs a real ChargingStation from a template file with test defaults
  * (`autoStart` off, local supervision URL); caller options take precedence.
@@ -85,12 +113,31 @@ export const createStationFromTemplate = (
     ...options,
   })
 
+// ---------------------------------------------------------------
+// Persisted configuration resolution
+// ---------------------------------------------------------------
+
 /**
- * Removes every temp template dir created by `writeStationTemplate`/`copyStationTemplate`.
- * Call in `afterEach`, alongside `standardCleanup()`.
+ * Resolves the isolated `configurations` dir sitting beside the
+ * `station-templates` dir of a template file — where a persisted
+ * ChargingStation writes its configuration.
+ * @param templateFile - Path returned by `writeStationTemplate`/`copyStationTemplate`.
+ * @returns Absolute path to the sibling `configurations` dir.
  */
-export const cleanupStationTemplates = (): void => {
-  for (const root of templateRoots.splice(0)) {
-    rmSync(root, { force: true, recursive: true })
+export const persistedConfigurationDir = (templateFile: string): string =>
+  join(dirname(dirname(templateFile)), 'configurations')
+
+/**
+ * Resolves the single persisted configuration file written by a station built
+ * from `templateFile`. Throws when none exists yet (flush the async write first).
+ * @param templateFile - Path returned by `writeStationTemplate`/`copyStationTemplate`.
+ * @returns Absolute path to the persisted configuration `.json` file.
+ */
+export const resolvePersistedConfigurationFile = (templateFile: string): string => {
+  const configurationDir = persistedConfigurationDir(templateFile)
+  const configurationFile = readdirSync(configurationDir).find(entry => entry.endsWith('.json'))
+  if (configurationFile == null) {
+    throw new Error(`No persisted configuration file found in ${configurationDir}`)
   }
+  return join(configurationDir, configurationFile)
 }