standardCleanup()
})
- await it('should return Accepted with filename for DiagnosticsLog request', () => {
- const request: OCPP20GetLogRequest = {
- log: {
- remoteLocation: 'ftp://logs.example.com/uploads/',
- },
- logType: LogEnumType.DiagnosticsLog,
- requestId: 1,
- }
-
- const response = testableService.handleRequestGetLog(station, request)
-
- assert.notStrictEqual(response, undefined)
- assert.strictEqual(typeof response, 'object')
- assert.strictEqual(response.status, LogStatusEnumType.Accepted)
- assert.strictEqual(response.filename, 'simulator-log.txt')
- })
-
- await it('should return Accepted with filename for SecurityLog request', () => {
- const request: OCPP20GetLogRequest = {
- log: {
- remoteLocation: 'https://logs.example.com/security/',
- },
- logType: LogEnumType.SecurityLog,
- requestId: 2,
- }
-
- const response = testableService.handleRequestGetLog(station, request)
-
- assert.strictEqual(response.status, LogStatusEnumType.Accepted)
- assert.strictEqual(response.filename, 'simulator-log.txt')
- })
-
- await it('should register GET_LOG event listener in constructor', () => {
- const service = new OCPP20IncomingRequestService()
- assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.GET_LOG), 1)
- })
-
- await it('should call simulateLogUploadLifecycle when GET_LOG event emitted with Accepted response', () => {
- const service = new OCPP20IncomingRequestService()
- const simulateMock = mock.method(
- service as unknown as {
- simulateLogUploadLifecycle: (
- chargingStation: ChargingStation,
- requestId: number
- ) => Promise<void>
- },
- 'simulateLogUploadLifecycle',
- () => Promise.resolve()
- )
-
- const request: OCPP20GetLogRequest = {
- log: {
- remoteLocation: 'https://csms.example.com/logs',
- },
- logType: LogEnumType.DiagnosticsLog,
- requestId: 10,
- }
- const response: OCPP20GetLogResponse = {
- filename: 'simulator-log.txt',
- status: LogStatusEnumType.Accepted,
- }
-
- service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
-
- assert.strictEqual(simulateMock.mock.callCount(), 1)
- assert.strictEqual(simulateMock.mock.calls[0].arguments[1], 10)
- })
-
- await it('should NOT call simulateLogUploadLifecycle when GET_LOG event emitted with Rejected response', () => {
- const service = new OCPP20IncomingRequestService()
- const simulateMock = mock.method(
- service as unknown as {
- simulateLogUploadLifecycle: (
- chargingStation: ChargingStation,
- requestId: number
- ) => Promise<void>
- },
- 'simulateLogUploadLifecycle',
- () => Promise.resolve()
- )
+ await describe('Handler validation', async () => {
+ await it('should return Accepted with filename for DiagnosticsLog request', () => {
+ const request: OCPP20GetLogRequest = {
+ log: {
+ remoteLocation: 'ftp://logs.example.com/uploads/',
+ },
+ logType: LogEnumType.DiagnosticsLog,
+ requestId: 1,
+ }
+
+ const response = testableService.handleRequestGetLog(station, request)
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ assert.strictEqual(response.status, LogStatusEnumType.Accepted)
+ assert.strictEqual(response.filename, 'simulator-log.txt')
+ })
- const request: OCPP20GetLogRequest = {
- log: {
- remoteLocation: 'https://csms.example.com/logs',
- },
- logType: LogEnumType.DiagnosticsLog,
- requestId: 11,
- }
- const response: OCPP20GetLogResponse = {
- status: LogStatusEnumType.Rejected,
- }
+ await it('should return Accepted with filename for SecurityLog request', () => {
+ const request: OCPP20GetLogRequest = {
+ log: {
+ remoteLocation: 'https://logs.example.com/security/',
+ },
+ logType: LogEnumType.SecurityLog,
+ requestId: 2,
+ }
- service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
+ const response = testableService.handleRequestGetLog(station, request)
- assert.strictEqual(simulateMock.mock.callCount(), 0)
+ assert.strictEqual(response.status, LogStatusEnumType.Accepted)
+ assert.strictEqual(response.filename, 'simulator-log.txt')
+ })
})
- await it('should handle simulateLogUploadLifecycle rejection gracefully', async () => {
- const service = new OCPP20IncomingRequestService()
- mock.method(
- service as unknown as {
- simulateLogUploadLifecycle: (
- chargingStation: ChargingStation,
- requestId: number
- ) => Promise<void>
- },
- 'simulateLogUploadLifecycle',
- () => Promise.reject(new Error('log upload error'))
- )
-
- const request: OCPP20GetLogRequest = {
- log: {
- remoteLocation: 'https://csms.example.com/logs',
- },
- logType: LogEnumType.DiagnosticsLog,
- requestId: 99,
- }
- const response: OCPP20GetLogResponse = {
- filename: 'simulator-log.txt',
- status: LogStatusEnumType.Accepted,
- }
+ await describe('GET_LOG event listener', async () => {
+ await it('should register GET_LOG event listener in constructor', () => {
+ const service = new OCPP20IncomingRequestService()
+ assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.GET_LOG), 1)
+ })
- service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
+ await it('should call simulateLogUploadLifecycle when GET_LOG event emitted with Accepted response', () => {
+ const service = new OCPP20IncomingRequestService()
+ const simulateMock = mock.method(
+ service as unknown as {
+ simulateLogUploadLifecycle: (
+ chargingStation: ChargingStation,
+ requestId: number
+ ) => Promise<void>
+ },
+ 'simulateLogUploadLifecycle',
+ () => Promise.resolve()
+ )
+
+ const request: OCPP20GetLogRequest = {
+ log: {
+ remoteLocation: 'https://csms.example.com/logs',
+ },
+ logType: LogEnumType.DiagnosticsLog,
+ requestId: 10,
+ }
+ const response: OCPP20GetLogResponse = {
+ filename: 'simulator-log.txt',
+ status: LogStatusEnumType.Accepted,
+ }
+
+ service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
+
+ assert.strictEqual(simulateMock.mock.callCount(), 1)
+ assert.strictEqual(simulateMock.mock.calls[0].arguments[1], 10)
+ })
- await Promise.resolve()
- })
+ await it('should NOT call simulateLogUploadLifecycle when GET_LOG event emitted with Rejected response', () => {
+ const service = new OCPP20IncomingRequestService()
+ const simulateMock = mock.method(
+ service as unknown as {
+ simulateLogUploadLifecycle: (
+ chargingStation: ChargingStation,
+ requestId: number
+ ) => Promise<void>
+ },
+ 'simulateLogUploadLifecycle',
+ () => Promise.resolve()
+ )
+
+ const request: OCPP20GetLogRequest = {
+ log: {
+ remoteLocation: 'https://csms.example.com/logs',
+ },
+ logType: LogEnumType.DiagnosticsLog,
+ requestId: 11,
+ }
+ const response: OCPP20GetLogResponse = {
+ status: LogStatusEnumType.Rejected,
+ }
+
+ service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
+
+ assert.strictEqual(simulateMock.mock.callCount(), 0)
+ })
- await describe('N01 - LogStatusNotification lifecycle', async () => {
- afterEach(() => {
- standardCleanup()
+ await it('should handle simulateLogUploadLifecycle rejection gracefully', async () => {
+ const service = new OCPP20IncomingRequestService()
+ mock.method(
+ service as unknown as {
+ simulateLogUploadLifecycle: (
+ chargingStation: ChargingStation,
+ requestId: number
+ ) => Promise<void>
+ },
+ 'simulateLogUploadLifecycle',
+ () => Promise.reject(new Error('log upload error'))
+ )
+
+ const request: OCPP20GetLogRequest = {
+ log: {
+ remoteLocation: 'https://csms.example.com/logs',
+ },
+ logType: LogEnumType.DiagnosticsLog,
+ requestId: 99,
+ }
+ const response: OCPP20GetLogResponse = {
+ filename: 'simulator-log.txt',
+ status: LogStatusEnumType.Accepted,
+ }
+
+ service.emit(OCPP20IncomingRequestCommand.GET_LOG, station, request, response)
+
+ await Promise.resolve()
})
- await it('should send Uploading notification with correct requestId', async t => {
- await withMockTimers(t, ['setTimeout'], async () => {
- // Arrange
- const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
- const service = new OCPP20IncomingRequestService()
- const request: OCPP20GetLogRequest = {
- log: { remoteLocation: 'ftp://logs.example.com/' },
- logType: LogEnumType.DiagnosticsLog,
- requestId: 42,
- }
- const response: OCPP20GetLogResponse = {
- filename: 'simulator-log.txt',
- status: LogStatusEnumType.Accepted,
- }
-
- // Act
- service.emit(OCPP20IncomingRequestCommand.GET_LOG, trackingStation, request, response)
- await flushMicrotasks()
-
- // Assert
- assert.ok(sentRequests.length >= 1, 'Expected at least one notification')
- assert.deepStrictEqual(sentRequests[0].payload, {
- requestId: 42,
- status: UploadLogStatusEnumType.Uploading,
+ await describe('N01 - LogStatusNotification lifecycle', async () => {
+ await it('should send Uploading notification with correct requestId', async t => {
+ await withMockTimers(t, ['setTimeout'], async () => {
+ // Arrange
+ const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+ const service = new OCPP20IncomingRequestService()
+ const request: OCPP20GetLogRequest = {
+ log: { remoteLocation: 'ftp://logs.example.com/' },
+ logType: LogEnumType.DiagnosticsLog,
+ requestId: 42,
+ }
+ const response: OCPP20GetLogResponse = {
+ filename: 'simulator-log.txt',
+ status: LogStatusEnumType.Accepted,
+ }
+
+ // Act
+ service.emit(OCPP20IncomingRequestCommand.GET_LOG, trackingStation, request, response)
+ await flushMicrotasks()
+
+ // Assert
+ assert.ok(sentRequests.length >= 1, 'Expected at least one notification')
+ assert.deepStrictEqual(sentRequests[0].payload, {
+ requestId: 42,
+ status: UploadLogStatusEnumType.Uploading,
+ })
})
})
- })
- await it('should send Uploaded notification with correct requestId after delay', async t => {
- await withMockTimers(t, ['setTimeout'], async () => {
- // Arrange
- const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
- const service = new OCPP20IncomingRequestService()
- const request: OCPP20GetLogRequest = {
- log: { remoteLocation: 'ftp://logs.example.com/' },
- logType: LogEnumType.DiagnosticsLog,
- requestId: 42,
- }
- const response: OCPP20GetLogResponse = {
- filename: 'simulator-log.txt',
- status: LogStatusEnumType.Accepted,
- }
-
- // Act
- service.emit(OCPP20IncomingRequestCommand.GET_LOG, trackingStation, request, response)
- await flushMicrotasks()
-
- // Only Uploading should be sent before the timer fires
- assert.strictEqual(sentRequests.length, 1)
-
- // Advance past the simulated upload delay
- t.mock.timers.tick(1000)
- await flushMicrotasks()
-
- // Assert
- assert.strictEqual(sentRequests.length, 2)
- assert.deepStrictEqual(sentRequests[1].payload, {
- requestId: 42,
- status: UploadLogStatusEnumType.Uploaded,
+ await it('should send Uploaded notification with correct requestId after delay', async t => {
+ await withMockTimers(t, ['setTimeout'], async () => {
+ // Arrange
+ const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+ const service = new OCPP20IncomingRequestService()
+ const request: OCPP20GetLogRequest = {
+ log: { remoteLocation: 'ftp://logs.example.com/' },
+ logType: LogEnumType.DiagnosticsLog,
+ requestId: 42,
+ }
+ const response: OCPP20GetLogResponse = {
+ filename: 'simulator-log.txt',
+ status: LogStatusEnumType.Accepted,
+ }
+
+ // Act
+ service.emit(OCPP20IncomingRequestCommand.GET_LOG, trackingStation, request, response)
+ await flushMicrotasks()
+
+ // Only Uploading should be sent before the timer fires
+ assert.strictEqual(sentRequests.length, 1)
+
+ // Advance past the simulated upload delay
+ t.mock.timers.tick(1000)
+ await flushMicrotasks()
+
+ // Assert
+ assert.strictEqual(sentRequests.length, 2)
+ assert.deepStrictEqual(sentRequests[1].payload, {
+ requestId: 42,
+ status: UploadLogStatusEnumType.Uploaded,
+ })
})
})
- })
- await it('should send Uploading before Uploaded in correct sequence', async t => {
- await withMockTimers(t, ['setTimeout'], async () => {
- // Arrange
- const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
- const service = new OCPP20IncomingRequestService()
- const request: OCPP20GetLogRequest = {
- log: { remoteLocation: 'ftp://logs.example.com/' },
- logType: LogEnumType.DiagnosticsLog,
- requestId: 7,
- }
- const response: OCPP20GetLogResponse = {
- filename: 'simulator-log.txt',
- status: LogStatusEnumType.Accepted,
- }
-
- // Act - complete the full lifecycle
- service.emit(OCPP20IncomingRequestCommand.GET_LOG, trackingStation, request, response)
- await flushMicrotasks()
- t.mock.timers.tick(1000)
- await flushMicrotasks()
-
- // Assert - verify sequence and requestId propagation
- assert.strictEqual(sentRequests.length, 2)
- assert.strictEqual(
- sentRequests[0].payload.status,
- UploadLogStatusEnumType.Uploading,
- 'First notification should be Uploading'
- )
- assert.strictEqual(
- sentRequests[1].payload.status,
- UploadLogStatusEnumType.Uploaded,
- 'Second notification should be Uploaded'
- )
- assert.strictEqual(sentRequests[0].payload.requestId, 7)
- assert.strictEqual(sentRequests[1].payload.requestId, 7)
+ await it('should send Uploading before Uploaded in correct sequence', async t => {
+ await withMockTimers(t, ['setTimeout'], async () => {
+ // Arrange
+ const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+ const service = new OCPP20IncomingRequestService()
+ const request: OCPP20GetLogRequest = {
+ log: { remoteLocation: 'ftp://logs.example.com/' },
+ logType: LogEnumType.DiagnosticsLog,
+ requestId: 7,
+ }
+ const response: OCPP20GetLogResponse = {
+ filename: 'simulator-log.txt',
+ status: LogStatusEnumType.Accepted,
+ }
+
+ // Act - complete the full lifecycle
+ service.emit(OCPP20IncomingRequestCommand.GET_LOG, trackingStation, request, response)
+ await flushMicrotasks()
+ t.mock.timers.tick(1000)
+ await flushMicrotasks()
+
+ // Assert - verify sequence and requestId propagation
+ assert.strictEqual(sentRequests.length, 2)
+ assert.strictEqual(
+ sentRequests[0].payload.status,
+ UploadLogStatusEnumType.Uploading,
+ 'First notification should be Uploading'
+ )
+ assert.strictEqual(
+ sentRequests[1].payload.status,
+ UploadLogStatusEnumType.Uploaded,
+ 'Second notification should be Uploaded'
+ )
+ assert.strictEqual(sentRequests[0].payload.requestId, 7)
+ assert.strictEqual(sentRequests[1].payload.requestId, 7)
+ })
})
})
})
standardCleanup()
})
- await it('should return Accepted for valid firmware update request', () => {
- const request: OCPP20UpdateFirmwareRequest = {
- firmware: {
- location: 'https://firmware.example.com/update-v2.0.bin',
- retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
- },
- requestId: 1,
- }
-
- const response = testableService.handleRequestUpdateFirmware(station, request)
-
- assert.notStrictEqual(response, undefined)
- assert.strictEqual(typeof response, 'object')
- assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
- })
-
- await it('should register UPDATE_FIRMWARE event listener in constructor', () => {
- const service = new OCPP20IncomingRequestService()
- assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE), 1)
- })
-
- await it('should call simulateFirmwareUpdateLifecycle when UPDATE_FIRMWARE event emitted with Accepted response', () => {
- const service = new OCPP20IncomingRequestService()
- const simulateMock = mock.method(
- service as unknown as {
- simulateFirmwareUpdateLifecycle: (
- chargingStation: ChargingStation,
- requestId: number,
- firmware: FirmwareType
- ) => Promise<void>
- },
- 'simulateFirmwareUpdateLifecycle',
- () => Promise.resolve()
- )
-
- const request: OCPP20UpdateFirmwareRequest = {
- firmware: {
- location: 'https://firmware.example.com/update.bin',
- retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
- signature: 'dGVzdA==',
- },
- requestId: 42,
- }
- const response: OCPP20UpdateFirmwareResponse = {
- status: UpdateFirmwareStatusEnumType.Accepted,
- }
-
- service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
-
- assert.strictEqual(simulateMock.mock.callCount(), 1)
- assert.strictEqual(simulateMock.mock.calls[0].arguments[1], 42)
- assert.deepStrictEqual(simulateMock.mock.calls[0].arguments[2], request.firmware)
- })
-
- await it('should NOT call simulateFirmwareUpdateLifecycle when UPDATE_FIRMWARE event emitted with Rejected response', () => {
- const service = new OCPP20IncomingRequestService()
- const simulateMock = mock.method(
- service as unknown as {
- simulateFirmwareUpdateLifecycle: (
- chargingStation: ChargingStation,
- requestId: number,
- firmware: FirmwareType
- ) => Promise<void>
- },
- 'simulateFirmwareUpdateLifecycle',
- () => Promise.resolve()
- )
-
- const request: OCPP20UpdateFirmwareRequest = {
- firmware: {
- location: 'https://firmware.example.com/update.bin',
- retrieveDateTime: new Date(),
- },
- requestId: 43,
- }
- const response: OCPP20UpdateFirmwareResponse = {
- status: UpdateFirmwareStatusEnumType.Rejected,
- }
-
- service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
-
- assert.strictEqual(simulateMock.mock.callCount(), 0)
- })
-
- await it('should handle simulateFirmwareUpdateLifecycle rejection gracefully', async () => {
- const service = new OCPP20IncomingRequestService()
- mock.method(
- service as unknown as {
- simulateFirmwareUpdateLifecycle: (
- chargingStation: ChargingStation,
- requestId: number,
- firmware: FirmwareType
- ) => Promise<void>
- },
- 'simulateFirmwareUpdateLifecycle',
- () => Promise.reject(new Error('firmware lifecycle error'))
- )
-
- const request: OCPP20UpdateFirmwareRequest = {
- firmware: {
- location: 'https://firmware.example.com/update.bin',
- retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
- },
- requestId: 99,
- }
- const response: OCPP20UpdateFirmwareResponse = {
- status: UpdateFirmwareStatusEnumType.Accepted,
- }
+ await describe('Handler validation', async () => {
+ await it('should return Accepted for valid firmware update request', () => {
+ const request: OCPP20UpdateFirmwareRequest = {
+ firmware: {
+ location: 'https://firmware.example.com/update-v2.0.bin',
+ retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
+ },
+ requestId: 1,
+ }
- service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
+ const response = testableService.handleRequestUpdateFirmware(station, request)
- await Promise.resolve()
- })
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
+ })
- await describe('Security Features', async () => {
await it('should return InvalidCertificate for invalid signing certificate PEM', () => {
// Arrange
const certManager = createMockCertificateManager()
// Assert
assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
})
+ })
- await it('should cancel previous firmware update when new one arrives', async t => {
- const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+ await describe('UPDATE_FIRMWARE event listener', async () => {
+ await it('should register UPDATE_FIRMWARE event listener in constructor', () => {
const service = new OCPP20IncomingRequestService()
- const testable = createTestableIncomingRequestService(service)
-
- const firstRequest: OCPP20UpdateFirmwareRequest = {
- firmware: {
- location: 'https://firmware.example.com/v1.bin',
- retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
- },
- requestId: 100,
- }
- const firstResponse: OCPP20UpdateFirmwareResponse = {
- status: UpdateFirmwareStatusEnumType.Accepted,
- }
-
- await withMockTimers(t, ['setTimeout'], async () => {
- service.emit(
- OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
- trackingStation,
- firstRequest,
- firstResponse
- )
-
- await flushMicrotasks()
- assert.strictEqual(sentRequests.length, 1)
- assert.strictEqual(sentRequests[0].payload.status, OCPP20FirmwareStatusEnumType.Downloading)
-
- const secondRequest: OCPP20UpdateFirmwareRequest = {
- firmware: {
- location: 'https://firmware.example.com/v2.bin',
- retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
- },
- requestId: 101,
- }
-
- const secondResponse = testable.handleRequestUpdateFirmware(trackingStation, secondRequest)
- assert.strictEqual(secondResponse.status, UpdateFirmwareStatusEnumType.AcceptedCanceled)
-
- await flushMicrotasks()
- })
+ assert.strictEqual(service.listenerCount(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE), 1)
})
- })
- await describe('Firmware Update Lifecycle', async () => {
- await it('should send full lifecycle Downloading→Downloaded→Installing→Installed + SecurityEvent', async t => {
- const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+ await it('should call simulateFirmwareUpdateLifecycle when UPDATE_FIRMWARE event emitted with Accepted response', () => {
const service = new OCPP20IncomingRequestService()
+ const simulateMock = mock.method(
+ service as unknown as {
+ simulateFirmwareUpdateLifecycle: (
+ chargingStation: ChargingStation,
+ requestId: number,
+ firmware: FirmwareType
+ ) => Promise<void>
+ },
+ 'simulateFirmwareUpdateLifecycle',
+ () => Promise.resolve()
+ )
const request: OCPP20UpdateFirmwareRequest = {
firmware: {
location: 'https://firmware.example.com/update.bin',
- retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+ retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
+ signature: 'dGVzdA==',
},
- requestId: 1,
+ requestId: 42,
}
const response: OCPP20UpdateFirmwareResponse = {
status: UpdateFirmwareStatusEnumType.Accepted,
}
- await withMockTimers(t, ['setTimeout'], async () => {
- service.emit(
- OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
- trackingStation,
- request,
- response
- )
+ service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
- await flushMicrotasks()
- assert.strictEqual(sentRequests.length, 1)
- assert.strictEqual(
- sentRequests[0].command,
- OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION
- )
- assert.strictEqual(sentRequests[0].payload.status, OCPP20FirmwareStatusEnumType.Downloading)
- assert.strictEqual(sentRequests[0].payload.requestId, 1)
-
- t.mock.timers.tick(2000)
- await flushMicrotasks()
- assert.strictEqual(sentRequests.length, 3)
- assert.strictEqual(sentRequests[1].payload.status, OCPP20FirmwareStatusEnumType.Downloaded)
- assert.strictEqual(sentRequests[2].payload.status, OCPP20FirmwareStatusEnumType.Installing)
-
- t.mock.timers.tick(1000)
- await flushMicrotasks()
- assert.strictEqual(sentRequests[3].payload.status, OCPP20FirmwareStatusEnumType.Installed)
-
- // H11: SecurityEventNotification for FirmwareUpdated
- assert.strictEqual(sentRequests.length, 5)
- assert.strictEqual(
- sentRequests[4].command,
- OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION
- )
- assert.strictEqual(sentRequests[4].payload.type, 'FirmwareUpdated')
- })
+ assert.strictEqual(simulateMock.mock.callCount(), 1)
+ assert.strictEqual(simulateMock.mock.calls[0].arguments[1], 42)
+ assert.deepStrictEqual(simulateMock.mock.calls[0].arguments[2], request.firmware)
})
- await it('should send DownloadFailed for empty firmware location', async t => {
- const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+ await it('should NOT call simulateFirmwareUpdateLifecycle when UPDATE_FIRMWARE event emitted with Rejected response', () => {
const service = new OCPP20IncomingRequestService()
+ const simulateMock = mock.method(
+ service as unknown as {
+ simulateFirmwareUpdateLifecycle: (
+ chargingStation: ChargingStation,
+ requestId: number,
+ firmware: FirmwareType
+ ) => Promise<void>
+ },
+ 'simulateFirmwareUpdateLifecycle',
+ () => Promise.resolve()
+ )
const request: OCPP20UpdateFirmwareRequest = {
firmware: {
- location: '',
- retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+ location: 'https://firmware.example.com/update.bin',
+ retrieveDateTime: new Date(),
},
- requestId: 7,
+ requestId: 43,
}
const response: OCPP20UpdateFirmwareResponse = {
- status: UpdateFirmwareStatusEnumType.Accepted,
+ status: UpdateFirmwareStatusEnumType.Rejected,
}
- await withMockTimers(t, ['setTimeout'], async () => {
- service.emit(
- OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
- trackingStation,
- request,
- response
- )
-
- await flushMicrotasks()
- assert.strictEqual(sentRequests.length, 1)
- assert.strictEqual(sentRequests[0].payload.status, OCPP20FirmwareStatusEnumType.Downloading)
+ service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
- t.mock.timers.tick(2000)
- await flushMicrotasks()
- assert.strictEqual(sentRequests.length, 2)
- assert.strictEqual(
- sentRequests[1].payload.status,
- OCPP20FirmwareStatusEnumType.DownloadFailed
- )
- assert.strictEqual(sentRequests[1].payload.requestId, 7)
- })
+ assert.strictEqual(simulateMock.mock.callCount(), 0)
})
- await it('should send DownloadFailed for malformed firmware location', async t => {
- const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+ await it('should handle simulateFirmwareUpdateLifecycle rejection gracefully', async () => {
const service = new OCPP20IncomingRequestService()
+ mock.method(
+ service as unknown as {
+ simulateFirmwareUpdateLifecycle: (
+ chargingStation: ChargingStation,
+ requestId: number,
+ firmware: FirmwareType
+ ) => Promise<void>
+ },
+ 'simulateFirmwareUpdateLifecycle',
+ () => Promise.reject(new Error('firmware lifecycle error'))
+ )
const request: OCPP20UpdateFirmwareRequest = {
firmware: {
- location: 'not-a-valid-url',
- retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+ location: 'https://firmware.example.com/update.bin',
+ retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
},
- requestId: 8,
+ requestId: 99,
}
const response: OCPP20UpdateFirmwareResponse = {
status: UpdateFirmwareStatusEnumType.Accepted,
}
- await withMockTimers(t, ['setTimeout'], async () => {
- service.emit(
- OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
- trackingStation,
- request,
- response
- )
-
- await flushMicrotasks()
- t.mock.timers.tick(2000)
- await flushMicrotasks()
+ service.emit(OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, station, request, response)
- assert.strictEqual(sentRequests.length, 2)
- assert.strictEqual(
- sentRequests[1].payload.status,
- OCPP20FirmwareStatusEnumType.DownloadFailed
- )
- assert.strictEqual(sentRequests[1].payload.requestId, 8)
- })
+ await Promise.resolve()
})
- await it('should include requestId in all firmware status notifications', async t => {
+ await it('should cancel previous firmware update when new one arrives', async t => {
const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
const service = new OCPP20IncomingRequestService()
- const expectedRequestId = 42
+ const testable = createTestableIncomingRequestService(service)
- const request: OCPP20UpdateFirmwareRequest = {
+ const firstRequest: OCPP20UpdateFirmwareRequest = {
firmware: {
- location: 'https://firmware.example.com/update.bin',
+ location: 'https://firmware.example.com/v1.bin',
retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
},
- requestId: expectedRequestId,
+ requestId: 100,
}
- const response: OCPP20UpdateFirmwareResponse = {
+ const firstResponse: OCPP20UpdateFirmwareResponse = {
status: UpdateFirmwareStatusEnumType.Accepted,
}
service.emit(
OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
trackingStation,
- request,
- response
+ firstRequest,
+ firstResponse
)
await flushMicrotasks()
- t.mock.timers.tick(2000)
- await flushMicrotasks()
- t.mock.timers.tick(1000)
+ assert.strictEqual(sentRequests.length, 1)
+ assert.strictEqual(sentRequests[0].payload.status, OCPP20FirmwareStatusEnumType.Downloading)
+
+ const secondRequest: OCPP20UpdateFirmwareRequest = {
+ firmware: {
+ location: 'https://firmware.example.com/v2.bin',
+ retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+ },
+ requestId: 101,
+ }
+
+ const secondResponse = testable.handleRequestUpdateFirmware(trackingStation, secondRequest)
+ assert.strictEqual(secondResponse.status, UpdateFirmwareStatusEnumType.AcceptedCanceled)
+
await flushMicrotasks()
+ })
+ })
- const firmwareNotifications = sentRequests.filter(
- r =>
- (r.command as OCPP20RequestCommand) ===
+ await describe('Firmware Update Lifecycle', async () => {
+ await it('should send full lifecycle Downloading→Downloaded→Installing→Installed + SecurityEvent', async t => {
+ const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+ const service = new OCPP20IncomingRequestService()
+
+ const request: OCPP20UpdateFirmwareRequest = {
+ firmware: {
+ location: 'https://firmware.example.com/update.bin',
+ retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+ },
+ requestId: 1,
+ }
+ const response: OCPP20UpdateFirmwareResponse = {
+ status: UpdateFirmwareStatusEnumType.Accepted,
+ }
+
+ await withMockTimers(t, ['setTimeout'], async () => {
+ service.emit(
+ OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
+ trackingStation,
+ request,
+ response
+ )
+
+ await flushMicrotasks()
+ assert.strictEqual(sentRequests.length, 1)
+ assert.strictEqual(
+ sentRequests[0].command,
OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION
- )
- assert.strictEqual(firmwareNotifications.length, 4)
- for (const req of firmwareNotifications) {
- assert.strictEqual(req.payload.requestId, expectedRequestId)
+ )
+ assert.strictEqual(
+ sentRequests[0].payload.status,
+ OCPP20FirmwareStatusEnumType.Downloading
+ )
+ assert.strictEqual(sentRequests[0].payload.requestId, 1)
+
+ t.mock.timers.tick(2000)
+ await flushMicrotasks()
+ assert.strictEqual(sentRequests.length, 3)
+ assert.strictEqual(
+ sentRequests[1].payload.status,
+ OCPP20FirmwareStatusEnumType.Downloaded
+ )
+ assert.strictEqual(
+ sentRequests[2].payload.status,
+ OCPP20FirmwareStatusEnumType.Installing
+ )
+
+ t.mock.timers.tick(1000)
+ await flushMicrotasks()
+ assert.strictEqual(sentRequests[3].payload.status, OCPP20FirmwareStatusEnumType.Installed)
+
+ // H11: SecurityEventNotification for FirmwareUpdated
+ assert.strictEqual(sentRequests.length, 5)
+ assert.strictEqual(
+ sentRequests[4].command,
+ OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION
+ )
+ assert.strictEqual(sentRequests[4].payload.type, 'FirmwareUpdated')
+ })
+ })
+
+ await it('should send DownloadFailed for empty firmware location', async t => {
+ const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+ const service = new OCPP20IncomingRequestService()
+
+ const request: OCPP20UpdateFirmwareRequest = {
+ firmware: {
+ location: '',
+ retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+ },
+ requestId: 7,
+ }
+ const response: OCPP20UpdateFirmwareResponse = {
+ status: UpdateFirmwareStatusEnumType.Accepted,
}
+
+ await withMockTimers(t, ['setTimeout'], async () => {
+ service.emit(
+ OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
+ trackingStation,
+ request,
+ response
+ )
+
+ await flushMicrotasks()
+ assert.strictEqual(sentRequests.length, 1)
+ assert.strictEqual(
+ sentRequests[0].payload.status,
+ OCPP20FirmwareStatusEnumType.Downloading
+ )
+
+ t.mock.timers.tick(2000)
+ await flushMicrotasks()
+ assert.strictEqual(sentRequests.length, 2)
+ assert.strictEqual(
+ sentRequests[1].payload.status,
+ OCPP20FirmwareStatusEnumType.DownloadFailed
+ )
+ assert.strictEqual(sentRequests[1].payload.requestId, 7)
+ })
})
- })
- await it('should include SignatureVerified when firmware has signature', async t => {
- const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
- const service = new OCPP20IncomingRequestService()
+ await it('should send DownloadFailed for malformed firmware location', async t => {
+ const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+ const service = new OCPP20IncomingRequestService()
- const request: OCPP20UpdateFirmwareRequest = {
- firmware: {
- location: 'https://firmware.example.com/update.bin',
- retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
- signature: 'dGVzdA==',
- },
- requestId: 5,
- }
- const response: OCPP20UpdateFirmwareResponse = {
- status: UpdateFirmwareStatusEnumType.Accepted,
- }
+ const request: OCPP20UpdateFirmwareRequest = {
+ firmware: {
+ location: 'not-a-valid-url',
+ retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+ },
+ requestId: 8,
+ }
+ const response: OCPP20UpdateFirmwareResponse = {
+ status: UpdateFirmwareStatusEnumType.Accepted,
+ }
- await withMockTimers(t, ['setTimeout'], async () => {
- service.emit(
- OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
- trackingStation,
- request,
- response
- )
+ await withMockTimers(t, ['setTimeout'], async () => {
+ service.emit(
+ OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
+ trackingStation,
+ request,
+ response
+ )
+
+ await flushMicrotasks()
+ t.mock.timers.tick(2000)
+ await flushMicrotasks()
+
+ assert.strictEqual(sentRequests.length, 2)
+ assert.strictEqual(
+ sentRequests[1].payload.status,
+ OCPP20FirmwareStatusEnumType.DownloadFailed
+ )
+ assert.strictEqual(sentRequests[1].payload.requestId, 8)
+ })
+ })
- await flushMicrotasks()
- assert.strictEqual(sentRequests.length, 1)
+ await it('should include requestId in all firmware status notifications', async t => {
+ const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+ const service = new OCPP20IncomingRequestService()
+ const expectedRequestId = 42
- t.mock.timers.tick(2000)
- await flushMicrotasks()
- assert.strictEqual(sentRequests.length, 2)
- assert.strictEqual(sentRequests[1].payload.status, OCPP20FirmwareStatusEnumType.Downloaded)
+ const request: OCPP20UpdateFirmwareRequest = {
+ firmware: {
+ location: 'https://firmware.example.com/update.bin',
+ retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+ },
+ requestId: expectedRequestId,
+ }
+ const response: OCPP20UpdateFirmwareResponse = {
+ status: UpdateFirmwareStatusEnumType.Accepted,
+ }
- t.mock.timers.tick(500)
- await flushMicrotasks()
- assert.strictEqual(
- sentRequests[2].payload.status,
- OCPP20FirmwareStatusEnumType.SignatureVerified
- )
- assert.strictEqual(sentRequests[3].payload.status, OCPP20FirmwareStatusEnumType.Installing)
+ await withMockTimers(t, ['setTimeout'], async () => {
+ service.emit(
+ OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
+ trackingStation,
+ request,
+ response
+ )
+
+ await flushMicrotasks()
+ t.mock.timers.tick(2000)
+ await flushMicrotasks()
+ t.mock.timers.tick(1000)
+ await flushMicrotasks()
+
+ const firmwareNotifications = sentRequests.filter(
+ r =>
+ (r.command as OCPP20RequestCommand) ===
+ OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION
+ )
+ assert.strictEqual(firmwareNotifications.length, 4)
+ for (const req of firmwareNotifications) {
+ assert.strictEqual(req.payload.requestId, expectedRequestId)
+ }
+ })
+ })
- t.mock.timers.tick(1000)
- await flushMicrotasks()
- assert.strictEqual(sentRequests[4].payload.status, OCPP20FirmwareStatusEnumType.Installed)
+ await it('should include SignatureVerified when firmware has signature', async t => {
+ const { sentRequests, station: trackingStation } = createMockStationWithRequestTracking()
+ const service = new OCPP20IncomingRequestService()
- // H11: SecurityEventNotification after Installed
- assert.strictEqual(sentRequests.length, 6)
- assert.strictEqual(
- sentRequests[5].command,
- OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION
- )
- assert.strictEqual(sentRequests[5].payload.type, 'FirmwareUpdated')
+ const request: OCPP20UpdateFirmwareRequest = {
+ firmware: {
+ location: 'https://firmware.example.com/update.bin',
+ retrieveDateTime: new Date('2020-01-01T00:00:00.000Z'),
+ signature: 'dGVzdA==',
+ },
+ requestId: 5,
+ }
+ const response: OCPP20UpdateFirmwareResponse = {
+ status: UpdateFirmwareStatusEnumType.Accepted,
+ }
+
+ await withMockTimers(t, ['setTimeout'], async () => {
+ service.emit(
+ OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
+ trackingStation,
+ request,
+ response
+ )
+
+ await flushMicrotasks()
+ assert.strictEqual(sentRequests.length, 1)
+
+ t.mock.timers.tick(2000)
+ await flushMicrotasks()
+ assert.strictEqual(sentRequests.length, 2)
+ assert.strictEqual(
+ sentRequests[1].payload.status,
+ OCPP20FirmwareStatusEnumType.Downloaded
+ )
+
+ t.mock.timers.tick(500)
+ await flushMicrotasks()
+ assert.strictEqual(
+ sentRequests[2].payload.status,
+ OCPP20FirmwareStatusEnumType.SignatureVerified
+ )
+ assert.strictEqual(
+ sentRequests[3].payload.status,
+ OCPP20FirmwareStatusEnumType.Installing
+ )
+
+ t.mock.timers.tick(1000)
+ await flushMicrotasks()
+ assert.strictEqual(sentRequests[4].payload.status, OCPP20FirmwareStatusEnumType.Installed)
+
+ // H11: SecurityEventNotification after Installed
+ assert.strictEqual(sentRequests.length, 6)
+ assert.strictEqual(
+ sentRequests[5].command,
+ OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION
+ )
+ assert.strictEqual(sentRequests[5].payload.type, 'FirmwareUpdated')
+ })
})
})
})