]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
test: add AutomaticTransactionGenerator tests
authorJérôme Benoit <jerome.benoit@sap.com>
Tue, 3 Mar 2026 19:51:03 +0000 (20:51 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Tue, 3 Mar 2026 19:51:03 +0000 (20:51 +0100)
Add tests for the ATG singleton covering instance lifecycle (create,
delete, same-instance), start/stop state machine with guards, connector
stop behavior, and handleStartTransactionResponse counter updates for
accepted and rejected transactions.

tests/charging-station/AutomaticTransactionGenerator.test.ts [new file with mode: 0644]

diff --git a/tests/charging-station/AutomaticTransactionGenerator.test.ts b/tests/charging-station/AutomaticTransactionGenerator.test.ts
new file mode 100644 (file)
index 0000000..a8aaa0a
--- /dev/null
@@ -0,0 +1,271 @@
+/**
+ * @file Tests for AutomaticTransactionGenerator
+ * @description Verifies the ATG singleton management, lifecycle state machine, and connector status handling
+ *
+ * Covers:
+ * - Singleton pattern (getInstance / deleteInstance)
+ * - Lifecycle state machine (start / stop / starting / stopping guards)
+ * - Connector status management (startConnector / stopConnector)
+ * - handleStartTransactionResponse — transaction counter updates
+ * - initializeConnectorsStatus — connector status initialization
+ *
+ * Note: The async transaction loop (internalStartConnector, startTransaction, stopTransaction)
+ * is NOT tested here because it involves real timers (sleep), random delays, and deep
+ * ChargingStation interaction. Those are integration-level concerns.
+ */
+
+import { expect } from '@std/expect'
+import { afterEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
+
+import { AutomaticTransactionGenerator } from '../../src/charging-station/AutomaticTransactionGenerator.js'
+import { BaseError } from '../../src/exception/index.js'
+import { AuthorizationStatus, type StartTransactionResponse } from '../../src/types/index.js'
+import { createMockChargingStation, standardCleanup } from './ChargingStationTestUtils.js'
+
+/**
+ *
+ * @param station
+ */
+function addATGMethodsToStation (station: ChargingStation): void {
+  const stationExt = station as unknown as {
+    getAutomaticTransactionGeneratorConfiguration: () => Record<string, unknown> | undefined
+    getAutomaticTransactionGeneratorStatuses: () => undefined | unknown[]
+  }
+  stationExt.getAutomaticTransactionGeneratorConfiguration = () => ({
+    enable: true,
+    idTagDistribution: 'random',
+    maxDelayBetweenTwoTransactions: 30,
+    maxDuration: 120,
+    minDelayBetweenTwoTransactions: 15,
+    minDuration: 60,
+    probabilityOfStart: 1,
+    requireAuthorize: false,
+    stopAbsoluteDuration: false,
+    stopAfterHours: 1,
+  })
+  stationExt.getAutomaticTransactionGeneratorStatuses = () => undefined
+}
+
+/**
+ *
+ * @param started
+ */
+function createStationForATG (started = true): ChargingStation {
+  const { station } = createMockChargingStation({ started })
+  addATGMethodsToStation(station)
+  return station
+}
+
+/**
+ *
+ * @param station
+ */
+function getDefinedATG (station: ChargingStation): AutomaticTransactionGenerator {
+  const atg = AutomaticTransactionGenerator.getInstance(station)
+  expect(atg).toBeDefined()
+  if (atg == null) {
+    throw new BaseError('ATG instance unexpectedly undefined')
+  }
+  return atg
+}
+
+/**
+ *
+ * @param atg
+ */
+function mockInternalStartConnector (atg: AutomaticTransactionGenerator): void {
+  const atgPrivate = atg as unknown as {
+    internalStartConnector: (...args: unknown[]) => Promise<void>
+  }
+  atgPrivate.internalStartConnector = async () => {
+    await Promise.resolve()
+  }
+}
+
+/**
+ *
+ */
+function resetATGInstances (): void {
+  const atgClass = AutomaticTransactionGenerator as unknown as {
+    instances: Map<string, AutomaticTransactionGenerator>
+  }
+  atgClass.instances.clear()
+}
+
+await describe('AutomaticTransactionGenerator', async () => {
+  afterEach(() => {
+    standardCleanup()
+    resetATGInstances()
+  })
+
+  await describe('singleton management', async () => {
+    await it('should create an instance for a charging station', () => {
+      const station = createStationForATG()
+
+      const atg = getDefinedATG(station)
+
+      expect(atg.connectorsStatus.size).toBe(2)
+    })
+
+    await it('should return the same instance for the same station', () => {
+      const station = createStationForATG()
+
+      const atg1 = AutomaticTransactionGenerator.getInstance(station)
+      const atg2 = AutomaticTransactionGenerator.getInstance(station)
+
+      expect(atg1).toBe(atg2)
+    })
+
+    await it('should delete an instance', () => {
+      const station = createStationForATG()
+
+      const atg1 = AutomaticTransactionGenerator.getInstance(station)
+      AutomaticTransactionGenerator.deleteInstance(station)
+      const atg2 = AutomaticTransactionGenerator.getInstance(station)
+
+      expect(atg1).not.toBe(atg2)
+    })
+  })
+
+  await describe('lifecycle — start', async () => {
+    await it('should start the ATG and set started to true', () => {
+      const station = createStationForATG()
+      const atg = getDefinedATG(station)
+      mockInternalStartConnector(atg)
+
+      atg.start()
+
+      expect(atg.started).toBe(true)
+    })
+
+    await it('should not start when station is not started', () => {
+      const station = createStationForATG(false)
+      const atg = getDefinedATG(station)
+
+      atg.start()
+
+      expect(atg.started).toBe(false)
+    })
+
+    await it('should warn and not restart when already started', () => {
+      const station = createStationForATG()
+      const atg = getDefinedATG(station)
+      mockInternalStartConnector(atg)
+
+      atg.start()
+      atg.start()
+
+      expect(atg.started).toBe(true)
+    })
+  })
+
+  await describe('lifecycle — stop', async () => {
+    await it('should stop the ATG and set started to false', () => {
+      const station = createStationForATG()
+      const atg = getDefinedATG(station)
+      mockInternalStartConnector(atg)
+
+      atg.start()
+      expect(atg.started).toBe(true)
+
+      atg.stop()
+      expect(atg.started).toBe(false)
+    })
+
+    await it('should warn when stopping an already stopped ATG', () => {
+      const station = createStationForATG()
+      const atg = getDefinedATG(station)
+
+      atg.stop()
+
+      expect(atg.started).toBe(false)
+    })
+  })
+
+  await describe('connector management', async () => {
+    await it('should stop a running connector', () => {
+      const station = createStationForATG()
+      const atg = getDefinedATG(station)
+
+      const connectorStatus = atg.connectorsStatus.get(1)
+      expect(connectorStatus).toBeDefined()
+      if (connectorStatus == null) {
+        throw new BaseError('Connector status unexpectedly undefined')
+      }
+      connectorStatus.start = true
+
+      atg.stopConnector(1)
+
+      expect(connectorStatus.start).toBe(false)
+    })
+
+    await it('should throw when stopping a non-existent connector', () => {
+      const station = createStationForATG()
+      const atg = getDefinedATG(station)
+
+      expect(() => {
+        atg.stopConnector(99)
+      }).toThrow(BaseError)
+    })
+  })
+
+  await describe('handleStartTransactionResponse', async () => {
+    await it('should increment accepted counters on accepted start response', () => {
+      const station = createStationForATG()
+      const atg = getDefinedATG(station)
+
+      const connectorStatus = atg.connectorsStatus.get(1)
+      expect(connectorStatus).toBeDefined()
+      if (connectorStatus == null) {
+        throw new BaseError('Connector status unexpectedly undefined')
+      }
+      const handleResponse = (
+        atg as unknown as {
+          handleStartTransactionResponse: (
+            connectorId: number,
+            response: StartTransactionResponse
+          ) => void
+        }
+      ).handleStartTransactionResponse.bind(atg)
+
+      handleResponse(1, {
+        idTagInfo: { status: AuthorizationStatus.ACCEPTED },
+        transactionId: 1,
+      } as StartTransactionResponse)
+
+      expect(connectorStatus.startTransactionRequests).toBe(1)
+      expect(connectorStatus.acceptedStartTransactionRequests).toBe(1)
+      expect(connectorStatus.rejectedStartTransactionRequests).toBe(0)
+    })
+
+    await it('should increment rejected counters on rejected start response', () => {
+      const station = createStationForATG()
+      const atg = getDefinedATG(station)
+
+      const connectorStatus = atg.connectorsStatus.get(1)
+      expect(connectorStatus).toBeDefined()
+      if (connectorStatus == null) {
+        throw new BaseError('Connector status unexpectedly undefined')
+      }
+      const handleResponse = (
+        atg as unknown as {
+          handleStartTransactionResponse: (
+            connectorId: number,
+            response: StartTransactionResponse
+          ) => void
+        }
+      ).handleStartTransactionResponse.bind(atg)
+
+      handleResponse(1, {
+        idTagInfo: { status: AuthorizationStatus.INVALID },
+        transactionId: 1,
+      } as StartTransactionResponse)
+
+      expect(connectorStatus.startTransactionRequests).toBe(1)
+      expect(connectorStatus.acceptedStartTransactionRequests).toBe(0)
+      expect(connectorStatus.rejectedStartTransactionRequests).toBe(1)
+    })
+  })
+})