]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(tests): harmonize UI server tests with codebase style
authorJérôme Benoit <jerome.benoit@sap.com>
Thu, 12 Feb 2026 19:24:51 +0000 (20:24 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Thu, 12 Feb 2026 19:24:51 +0000 (20:24 +0100)
- Add helper functions to reduce duplication (createHttpServerConfig, createLargePayload)
- Add waitForStreamFlush() utility for async stream tests
- Add GZIP_STREAM_FLUSH_DELAY_MS constant with documentation
- Remove low-value constant assertion tests
- Add edge case tests for compression boundary and context cleanup
- Harmonize test names to 'Verify X' style
- Use Reflect.get() instead of unsafe double cast
- Replace eslint-disable with proper guard pattern

tests/charging-station/ui-server/UIHttpServer.test.ts
tests/charging-station/ui-server/UIServerSecurity.test.ts
tests/charging-station/ui-server/UIServerTestConstants.ts
tests/charging-station/ui-server/UIServerTestUtils.ts

index 5df5f6d1d2a174b08817feeb28478593a74b05ca..3c7fabb3c2fcbf15088f04ffa6fa9527a93bea2d 100644 (file)
@@ -9,27 +9,42 @@ import type { UUIDv4 } from '../../../src/types/index.js'
 import { UIHttpServer } from '../../../src/charging-station/ui-server/UIHttpServer.js'
 import { DEFAULT_COMPRESSION_THRESHOLD } from '../../../src/charging-station/ui-server/UIServerSecurity.js'
 import { ApplicationProtocol, ResponseStatus } from '../../../src/types/index.js'
-import { TEST_UUID } from './UIServerTestConstants.js'
-import { createMockUIServerConfiguration, MockServerResponse } from './UIServerTestUtils.js'
+import { GZIP_STREAM_FLUSH_DELAY_MS, TEST_UUID } from './UIServerTestConstants.js'
+import {
+  createMockUIServerConfiguration,
+  MockServerResponse,
+  waitForStreamFlush,
+} from './UIServerTestUtils.js'
 
 class TestableUIHttpServer extends UIHttpServer {
   public addResponseHandler (uuid: UUIDv4, res: MockServerResponse): void {
     this.responseHandlers.set(uuid, res as never)
   }
 
+  public getAcceptsGzip (): Map<UUIDv4, boolean> {
+    return Reflect.get(this, 'acceptsGzip') as Map<UUIDv4, boolean>
+  }
+
   public getResponseHandlersSize (): number {
     return this.responseHandlers.size
   }
 
   public setAcceptsGzip (uuid: UUIDv4, value: boolean): void {
-    ;(this as unknown as { acceptsGzip: Map<UUIDv4, boolean> }).acceptsGzip.set(uuid, value)
+    this.getAcceptsGzip().set(uuid, value)
   }
 }
 
+const createHttpServerConfig = () =>
+  createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
+
+const createLargePayload = (status: ResponseStatus = ResponseStatus.SUCCESS) => ({
+  data: 'x'.repeat(DEFAULT_COMPRESSION_THRESHOLD + 100),
+  status,
+})
+
 await describe('UIHttpServer test suite', async () => {
   await it('Verify sendResponse() deletes handler after sending', () => {
-    const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-    const server = new TestableUIHttpServer(config)
+    const server = new TestableUIHttpServer(createHttpServerConfig())
     const res = new MockServerResponse()
 
     server.addResponseHandler(TEST_UUID, res)
@@ -43,8 +58,7 @@ await describe('UIHttpServer test suite', async () => {
   })
 
   await it('Verify sendResponse() logs error when handler not found', () => {
-    const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-    const server = new TestableUIHttpServer(config)
+    const server = new TestableUIHttpServer(createHttpServerConfig())
 
     server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
 
@@ -52,8 +66,7 @@ await describe('UIHttpServer test suite', async () => {
   })
 
   await it('Verify sendResponse() sets correct status code for failure', () => {
-    const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-    const server = new TestableUIHttpServer(config)
+    const server = new TestableUIHttpServer(createHttpServerConfig())
     const res = new MockServerResponse()
 
     server.addResponseHandler(TEST_UUID, res)
@@ -65,8 +78,7 @@ await describe('UIHttpServer test suite', async () => {
   })
 
   await it('Verify sendResponse() handles send errors gracefully', () => {
-    const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-    const server = new TestableUIHttpServer(config)
+    const server = new TestableUIHttpServer(createHttpServerConfig())
     const res = new MockServerResponse()
     res.end = (): never => {
       throw new Error('HTTP response end error')
@@ -79,8 +91,7 @@ await describe('UIHttpServer test suite', async () => {
   })
 
   await it('Verify sendResponse() sets correct Content-Type header', () => {
-    const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-    const server = new TestableUIHttpServer(config)
+    const server = new TestableUIHttpServer(createHttpServerConfig())
     const res = new MockServerResponse()
 
     server.addResponseHandler(TEST_UUID, res)
@@ -90,8 +101,7 @@ await describe('UIHttpServer test suite', async () => {
   })
 
   await it('Verify response handlers cleanup', () => {
-    const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-    const server = new TestableUIHttpServer(config)
+    const server = new TestableUIHttpServer(createHttpServerConfig())
     const res1 = new MockServerResponse()
     const res2 = new MockServerResponse()
 
@@ -107,8 +117,7 @@ await describe('UIHttpServer test suite', async () => {
   })
 
   await it('Verify handlers cleared on server stop', () => {
-    const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-    const server = new TestableUIHttpServer(config)
+    const server = new TestableUIHttpServer(createHttpServerConfig())
     const res = new MockServerResponse()
 
     server.addResponseHandler(TEST_UUID, res)
@@ -120,8 +129,7 @@ await describe('UIHttpServer test suite', async () => {
   })
 
   await it('Verify response payload serialization', () => {
-    const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-    const server = new TestableUIHttpServer(config)
+    const server = new TestableUIHttpServer(createHttpServerConfig())
     const res = new MockServerResponse()
     const payload = {
       hashIdsSucceeded: ['station-1', 'station-2'],
@@ -132,15 +140,13 @@ await describe('UIHttpServer test suite', async () => {
     server.sendResponse([TEST_UUID, payload])
 
     expect(res.body).toBeDefined()
-    // HTTP server sends only the payload, not [uuid, payload]
     const parsedBody = JSON.parse(res.body ?? '{}') as Record<string, unknown>
     expect(parsedBody.status).toBe('success')
     expect(parsedBody.hashIdsSucceeded).toEqual(['station-1', 'station-2'])
   })
 
   await it('Verify response with error details', () => {
-    const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-    const server = new TestableUIHttpServer(config)
+    const server = new TestableUIHttpServer(createHttpServerConfig())
     const res = new MockServerResponse()
     const payload = {
       errorMessage: 'Test error',
@@ -152,7 +158,6 @@ await describe('UIHttpServer test suite', async () => {
     server.sendResponse([TEST_UUID, payload])
 
     expect(res.body).toBeDefined()
-    // HTTP server sends only the payload, not [uuid, payload]
     const parsedBody = JSON.parse(res.body ?? '{}') as Record<string, unknown>
     expect(parsedBody.status).toBe('failure')
     expect(parsedBody.errorMessage).toBe('Test error')
@@ -160,8 +165,7 @@ await describe('UIHttpServer test suite', async () => {
   })
 
   await it('Verify valid HTTP configuration', () => {
-    const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-    const server = new UIHttpServer(config)
+    const server = new UIHttpServer(createHttpServerConfig())
 
     expect(server).toBeDefined()
   })
@@ -180,87 +184,108 @@ await describe('UIHttpServer test suite', async () => {
   })
 
   await describe('Gzip compression', async () => {
-    await it('Verify sendResponse() does not compress when acceptsGzip is false', () => {
-      const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-      const server = new TestableUIHttpServer(config)
+    await it('Verify no compression when acceptsGzip is false', () => {
+      const server = new TestableUIHttpServer(createHttpServerConfig())
       const res = new MockServerResponse()
-      const largeData = 'x'.repeat(DEFAULT_COMPRESSION_THRESHOLD + 100)
-      const payload = {
-        data: largeData,
-        status: ResponseStatus.SUCCESS,
-      }
 
       server.addResponseHandler(TEST_UUID, res)
       server.setAcceptsGzip(TEST_UUID, false)
-      server.sendResponse([TEST_UUID, payload])
+      server.sendResponse([TEST_UUID, createLargePayload()])
 
       expect(res.headers['Content-Encoding']).toBeUndefined()
       expect(res.headers['Content-Type']).toBe('application/json')
     })
 
-    await it('Verify sendResponse() does not compress small responses', () => {
-      const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-      const server = new TestableUIHttpServer(config)
+    await it('Verify no compression for small responses', () => {
+      const server = new TestableUIHttpServer(createHttpServerConfig())
       const res = new MockServerResponse()
-      const payload = {
-        status: ResponseStatus.SUCCESS,
-      }
 
       server.addResponseHandler(TEST_UUID, res)
       server.setAcceptsGzip(TEST_UUID, true)
-      server.sendResponse([TEST_UUID, payload])
+      server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
 
       expect(res.headers['Content-Encoding']).toBeUndefined()
       expect(res.headers['Content-Type']).toBe('application/json')
     })
 
-    await it('Verify sendResponse() compresses large responses when client accepts gzip', async () => {
-      const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-      const server = new TestableUIHttpServer(config)
+    await it('Verify no compression at exact threshold boundary', () => {
+      const server = new TestableUIHttpServer(createHttpServerConfig())
       const res = new MockServerResponse()
-      const largeData = 'x'.repeat(DEFAULT_COMPRESSION_THRESHOLD + 100)
-      const payload = {
-        data: largeData,
+      const basePayload = { data: '', status: ResponseStatus.SUCCESS }
+      const baseSize = Buffer.byteLength(JSON.stringify(basePayload))
+      const paddingNeeded = DEFAULT_COMPRESSION_THRESHOLD - baseSize
+      const boundaryPayload = {
+        data: 'x'.repeat(Math.max(0, paddingNeeded)),
         status: ResponseStatus.SUCCESS,
       }
 
       server.addResponseHandler(TEST_UUID, res)
       server.setAcceptsGzip(TEST_UUID, true)
-      server.sendResponse([TEST_UUID, payload])
+      server.sendResponse([TEST_UUID, boundaryPayload])
+
+      expect(res.headers['Content-Encoding']).toBeUndefined()
+    })
+
+    await it('Verify compression headers for large responses', async () => {
+      const server = new TestableUIHttpServer(createHttpServerConfig())
+      const res = new MockServerResponse()
+
+      server.addResponseHandler(TEST_UUID, res)
+      server.setAcceptsGzip(TEST_UUID, true)
+      server.sendResponse([TEST_UUID, createLargePayload()])
 
-      await new Promise(resolve => {
-        setTimeout(resolve, 50)
-      })
+      await waitForStreamFlush(GZIP_STREAM_FLUSH_DELAY_MS)
 
       expect(res.headers['Content-Encoding']).toBe('gzip')
       expect(res.headers['Content-Type']).toBe('application/json')
       expect(res.headers.Vary).toBe('Accept-Encoding')
     })
 
-    await it('Verify compressed response can be decompressed to original payload', async () => {
-      const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
-      const server = new TestableUIHttpServer(config)
+    await it('Verify compressed response decompresses to original payload', async () => {
+      const server = new TestableUIHttpServer(createHttpServerConfig())
       const res = new MockServerResponse()
-      const largeData = 'x'.repeat(DEFAULT_COMPRESSION_THRESHOLD + 100)
-      const payload = {
-        data: largeData,
-        status: ResponseStatus.SUCCESS,
-      }
+      const payload = createLargePayload()
 
       server.addResponseHandler(TEST_UUID, res)
       server.setAcceptsGzip(TEST_UUID, true)
       server.sendResponse([TEST_UUID, payload])
 
-      await new Promise(resolve => {
-        setTimeout(resolve, 50)
-      })
+      await waitForStreamFlush(GZIP_STREAM_FLUSH_DELAY_MS)
 
       expect(res.bodyBuffer).toBeDefined()
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      const decompressed = gunzipSync(res.bodyBuffer!).toString('utf8')
+      if (res.bodyBuffer == null) {
+        throw new Error('Expected bodyBuffer to be defined')
+      }
+      const decompressed = gunzipSync(res.bodyBuffer).toString('utf8')
       const parsedBody = JSON.parse(decompressed) as Record<string, unknown>
       expect(parsedBody.status).toBe('success')
-      expect(parsedBody.data).toBe(largeData)
+      expect(parsedBody.data).toBe(payload.data)
+    })
+
+    await it('Verify no compression when acceptsGzip context is missing', () => {
+      const server = new TestableUIHttpServer(createHttpServerConfig())
+      const res = new MockServerResponse()
+
+      server.addResponseHandler(TEST_UUID, res)
+      server.sendResponse([TEST_UUID, createLargePayload()])
+
+      expect(res.headers['Content-Encoding']).toBeUndefined()
+      expect(res.headers['Content-Type']).toBe('application/json')
+    })
+
+    await it('Verify acceptsGzip context cleanup after response', async () => {
+      const server = new TestableUIHttpServer(createHttpServerConfig())
+      const res = new MockServerResponse()
+
+      server.addResponseHandler(TEST_UUID, res)
+      server.setAcceptsGzip(TEST_UUID, true)
+      expect(server.getAcceptsGzip().has(TEST_UUID)).toBe(true)
+
+      server.sendResponse([TEST_UUID, createLargePayload()])
+
+      await waitForStreamFlush(GZIP_STREAM_FLUSH_DELAY_MS)
+
+      expect(server.getAcceptsGzip().has(TEST_UUID)).toBe(false)
     })
   })
 })
index 5c95d29297762e3edf0a4e05fd0fd86ab364c7bf..42d8d5742d1dfc4f735d45d12b33befe43a0f4fa 100644 (file)
@@ -6,165 +6,132 @@ import { describe, it } from 'node:test'
 import {
   createBodySizeLimiter,
   createRateLimiter,
-  DEFAULT_COMPRESSION_THRESHOLD,
-  DEFAULT_MAX_PAYLOAD_SIZE,
   DEFAULT_MAX_STATIONS,
-  DEFAULT_MAX_TRACKED_IPS,
-  DEFAULT_RATE_LIMIT,
-  DEFAULT_RATE_WINDOW,
   isValidCredential,
   isValidNumberOfStations,
 } from '../../../src/charging-station/ui-server/UIServerSecurity.js'
+import { waitForStreamFlush } from './UIServerTestUtils.js'
+
+const RATE_WINDOW_EXPIRY_DELAY_MS = 110
 
 await describe('UIServerSecurity test suite', async () => {
   await describe('isValidCredential()', async () => {
-    await it('should return true for matching credentials', () => {
-      const result = isValidCredential('myPassword123', 'myPassword123')
-      expect(result).toBe(true)
+    await it('Verify matching credentials return true', () => {
+      expect(isValidCredential('myPassword123', 'myPassword123')).toBe(true)
     })
 
-    await it('should return false for non-matching credentials', () => {
-      const result = isValidCredential('password1', 'password2')
-      expect(result).toBe(false)
+    await it('Verify non-matching credentials return false', () => {
+      expect(isValidCredential('password1', 'password2')).toBe(false)
     })
 
-    await it('should handle empty string credentials', () => {
-      const result = isValidCredential('', '')
-      expect(result).toBe(true)
+    await it('Verify empty string credentials match', () => {
+      expect(isValidCredential('', '')).toBe(true)
     })
 
-    await it('should handle different length credentials', () => {
+    await it('Verify different length credentials return false', () => {
       // cspell:disable-next-line
-      const result = isValidCredential('short', 'verylongpassword')
-      expect(result).toBe(false)
+      expect(isValidCredential('short', 'verylongpassword')).toBe(false)
     })
   })
 
   await describe('createBodySizeLimiter()', async () => {
-    await it('should return true when accumulated bytes are under limit', () => {
+    await it('Verify bytes under limit return true', () => {
       const limiter = createBodySizeLimiter(1000)
-      const result = limiter(500)
-      expect(result).toBe(true)
+
+      expect(limiter(500)).toBe(true)
     })
 
-    await it('should return false when accumulated bytes exceed limit', () => {
+    await it('Verify accumulated bytes exceeding limit return false', () => {
       const limiter = createBodySizeLimiter(1000)
       limiter(600)
-      const result = limiter(500)
-      expect(result).toBe(false)
+
+      expect(limiter(500)).toBe(false)
     })
 
-    await it('should return true at exact limit boundary', () => {
+    await it('Verify exact limit boundary returns true', () => {
       const limiter = createBodySizeLimiter(1000)
-      const result = limiter(1000)
-      expect(result).toBe(true)
+
+      expect(limiter(1000)).toBe(true)
     })
   })
 
   await describe('createRateLimiter()', async () => {
-    await it('should allow requests under the limit', () => {
+    await it('Verify requests under limit are allowed', () => {
       const limiter = createRateLimiter(5, 1000)
+
       for (let i = 0; i < 5; i++) {
         expect(limiter('192.168.1.1')).toBe(true)
       }
     })
 
-    await it('should block requests that exceed the limit', () => {
+    await it('Verify requests exceeding limit are blocked', () => {
       const limiter = createRateLimiter(3, 1000)
       limiter('192.168.1.1')
       limiter('192.168.1.1')
       limiter('192.168.1.1')
-      const result = limiter('192.168.1.1')
-      expect(result).toBe(false)
+
+      expect(limiter('192.168.1.1')).toBe(false)
     })
 
-    await it('should reset window after time expires', async () => {
+    await it('Verify window resets after time expires', async () => {
       const limiter = createRateLimiter(2, 100)
       limiter('10.0.0.1')
       limiter('10.0.0.1')
       expect(limiter('10.0.0.1')).toBe(false)
 
-      await new Promise(resolve => {
-        setTimeout(resolve, 110)
-      })
+      await waitForStreamFlush(RATE_WINDOW_EXPIRY_DELAY_MS)
 
       expect(limiter('10.0.0.1')).toBe(true)
     })
 
-    await it('should reject new IPs when at max tracked IPs capacity', () => {
+    await it('Verify new IPs rejected when at max tracked capacity', () => {
       const limiter = createRateLimiter(10, 60000, 3)
+
       expect(limiter('192.168.1.1')).toBe(true)
       expect(limiter('192.168.1.2')).toBe(true)
       expect(limiter('192.168.1.3')).toBe(true)
       expect(limiter('192.168.1.4')).toBe(false)
     })
 
-    await it('should still allow existing IPs when at capacity', () => {
+    await it('Verify existing IPs still allowed when at capacity', () => {
       const limiter = createRateLimiter(10, 60000, 2)
+
       expect(limiter('192.168.1.1')).toBe(true)
       expect(limiter('192.168.1.2')).toBe(true)
       expect(limiter('192.168.1.1')).toBe(true)
       expect(limiter('192.168.1.2')).toBe(true)
     })
 
-    await it('should cleanup expired entries when at capacity', async () => {
+    await it('Verify expired entries cleanup when at capacity', async () => {
       const limiter = createRateLimiter(10, 50, 2)
       expect(limiter('192.168.1.1')).toBe(true)
       expect(limiter('192.168.1.2')).toBe(true)
 
-      await new Promise(resolve => {
-        setTimeout(resolve, 60)
-      })
+      await waitForStreamFlush(60)
 
       expect(limiter('192.168.1.3')).toBe(true)
     })
   })
 
   await describe('isValidNumberOfStations()', async () => {
-    await it('should return true for valid number of stations', () => {
-      const result = isValidNumberOfStations(50, DEFAULT_MAX_STATIONS)
-      expect(result).toBe(true)
-    })
-
-    await it('should return false when exceeding max stations', () => {
-      const result = isValidNumberOfStations(150, DEFAULT_MAX_STATIONS)
-      expect(result).toBe(false)
-    })
-
-    await it('should return false for zero stations', () => {
-      const result = isValidNumberOfStations(0, DEFAULT_MAX_STATIONS)
-      expect(result).toBe(false)
-    })
-
-    await it('should return false for negative stations', () => {
-      const result = isValidNumberOfStations(-5, DEFAULT_MAX_STATIONS)
-      expect(result).toBe(false)
-    })
-  })
-
-  await describe('Security constants', async () => {
-    await it('should have correct DEFAULT_MAX_PAYLOAD_SIZE value', () => {
-      expect(DEFAULT_MAX_PAYLOAD_SIZE).toBe(1048576) // 1MB
-    })
-
-    await it('should have correct DEFAULT_RATE_LIMIT value', () => {
-      expect(DEFAULT_RATE_LIMIT).toBe(100)
+    await it('Verify valid number of stations returns true', () => {
+      expect(isValidNumberOfStations(50, DEFAULT_MAX_STATIONS)).toBe(true)
     })
 
-    await it('should have correct DEFAULT_RATE_WINDOW value', () => {
-      expect(DEFAULT_RATE_WINDOW).toBe(60000) // 60 seconds
+    await it('Verify exceeding max stations returns false', () => {
+      expect(isValidNumberOfStations(150, DEFAULT_MAX_STATIONS)).toBe(false)
     })
 
-    await it('should have correct DEFAULT_MAX_STATIONS value', () => {
-      expect(DEFAULT_MAX_STATIONS).toBe(100)
+    await it('Verify zero stations returns false', () => {
+      expect(isValidNumberOfStations(0, DEFAULT_MAX_STATIONS)).toBe(false)
     })
 
-    await it('should have correct DEFAULT_MAX_TRACKED_IPS value', () => {
-      expect(DEFAULT_MAX_TRACKED_IPS).toBe(10000)
+    await it('Verify negative stations returns false', () => {
+      expect(isValidNumberOfStations(-5, DEFAULT_MAX_STATIONS)).toBe(false)
     })
 
-    await it('should have correct DEFAULT_COMPRESSION_THRESHOLD value', () => {
-      expect(DEFAULT_COMPRESSION_THRESHOLD).toBe(1024) // 1KB
+    await it('Verify exact max stations boundary returns true', () => {
+      expect(isValidNumberOfStations(DEFAULT_MAX_STATIONS, DEFAULT_MAX_STATIONS)).toBe(true)
     })
   })
 })
index 8a2b995871744d9f8776c08199fbf12992639dbe..e95ba4d26b7b8f22b66f3d75effde54609294a1b 100644 (file)
@@ -15,3 +15,10 @@ export const TEST_PROCEDURES = {
 
 export const TEST_HASH_ID = 'test-station-001' as const
 export const TEST_HASH_ID_2 = 'test-station-002' as const
+
+/**
+ * Delay for gzip stream completion in tests.
+ * Gzip compression is asynchronous (stream-based), requiring a brief wait
+ * for the pipe to flush before asserting on the compressed output.
+ */
+export const GZIP_STREAM_FLUSH_DELAY_MS = 50
index 6a604123654ae169ac5093398f9010f492e7bcd0..7f5fa81104d25778333c7bce587ab99a0a23585e 100644 (file)
@@ -222,3 +222,9 @@ export class MockUIServiceNonBroadcast {
     return Promise.resolve([request[0], { status: ResponseStatus.SUCCESS }])
   }
 }
+
+export const waitForStreamFlush = async (delayMs: number): Promise<void> => {
+  await new Promise(resolve => {
+    setTimeout(resolve, delayMs)
+  })
+}