it('should reject invalid identifier', () => {})
// ✅ Good — with spec traceability prefix (for FR-referenced tests)
-it('G03.FR.01.T5.01 - should evict non-valid entry before valid one', () => {})
-it('G04.INT.01: should wire auth cache into local strategy', async () => {})
+it('C10.FR.07.T01 - should evict non-valid entry before valid one', () => {})
+it('C10.INT.01: should wire auth cache into local strategy', async () => {})
// ❌ Bad
it('Should start successfully', () => {}) // Capital 'S'
IdentifierType,
} from '../../../../src/charging-station/ocpp/auth/types/AuthTypes.js'
import { OCPPVersion } from '../../../../src/types/index.js'
-import { sleep, standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
import {
- createMockAuthorizationResult,
createMockAuthRequest,
createMockIdentifier,
createMockLocalAuthListManager,
})
await describe('Cache Spec Compliance Integration', async () => {
- // G04.INT.01 - Cache wiring regression (T2)
- await it('G04.INT.01: OCPPAuthServiceImpl wires auth cache into local strategy', async () => {
+ // C10.INT.01 - Cache wiring regression
+ await it('C10.INT.01: OCPPAuthServiceImpl wires auth cache into local strategy', async () => {
const result16 = createMockChargingStation({
baseName: 'TEST_CACHE_WIRING',
connectorsCount: 1,
assert.notStrictEqual(authCache, undefined)
})
- // G04.INT.02 - All-status caching (T4)
- await it('G04.INT.02: cache stores and retrieves all authorization statuses', () => {
- const cache = new InMemoryAuthCache({ cleanupIntervalSeconds: 0 })
- try {
- const blockedResult = createMockAuthorizationResult({
- status: AuthorizationStatus.BLOCKED,
- })
-
- cache.set('BLOCKED-ID', blockedResult)
- const retrieved = cache.get('BLOCKED-ID')
-
- assert.notStrictEqual(retrieved, undefined)
- assert.strictEqual(retrieved?.status, AuthorizationStatus.BLOCKED)
- } finally {
- cache.dispose()
- }
- })
-
- // G04.INT.03 - Status-aware eviction (T5)
- await it('G04.INT.03: eviction prefers ACCEPTED entries over non-ACCEPTED', () => {
- const cache = new InMemoryAuthCache({ cleanupIntervalSeconds: 0, maxEntries: 3 })
- try {
- // Add 3 ACCEPTED entries
- for (let i = 0; i < 3; i++) {
- cache.set(
- `ACCEPTED-${String(i)}`,
- createMockAuthorizationResult({ status: AuthorizationStatus.ACCEPTED })
- )
- }
-
- // Insert a BLOCKED entry — triggers eviction of one ACCEPTED entry
- cache.set(
- 'BLOCKED-ENTRY',
- createMockAuthorizationResult({ status: AuthorizationStatus.BLOCKED })
- )
-
- const stats = cache.getStats()
- assert.strictEqual(stats.totalEntries, 3)
-
- // BLOCKED entry must still exist
- const blocked = cache.get('BLOCKED-ENTRY')
- assert.notStrictEqual(blocked, undefined)
- assert.strictEqual(blocked?.status, AuthorizationStatus.BLOCKED)
- } finally {
- cache.dispose()
- }
- })
-
- // G04.INT.04 - TTL sliding window (T6/R5/R16)
- await it('G04.INT.04: cache hit resets TTL sliding window', async () => {
- const cache = new InMemoryAuthCache({ cleanupIntervalSeconds: 0, defaultTtl: 1 })
- try {
- cache.set(
- 'SLIDING-ID',
- createMockAuthorizationResult({ status: AuthorizationStatus.ACCEPTED })
- )
-
- // Wait 500ms, then access to reset TTL
- await sleep(500)
- const midResult = cache.get('SLIDING-ID')
- assert.notStrictEqual(midResult, undefined)
- assert.strictEqual(midResult?.status, AuthorizationStatus.ACCEPTED)
-
- // Wait another 700ms (total 1200ms from initial set, but only 700ms from last access)
- await sleep(700)
- const lateResult = cache.get('SLIDING-ID')
-
- // Entry should still be valid because TTL was reset at the 500ms access
- assert.notStrictEqual(lateResult, undefined)
- assert.strictEqual(lateResult?.status, AuthorizationStatus.ACCEPTED)
- } finally {
- cache.dispose()
- }
- })
-
- // G04.INT.05 - Expired entry transition (T7/R10)
- await it('G04.INT.05: expired entries transition to EXPIRED status instead of being deleted', async () => {
- const cache = new InMemoryAuthCache({ cleanupIntervalSeconds: 0, defaultTtl: 1 })
- try {
- cache.set(
- 'EXPIRE-ID',
- createMockAuthorizationResult({ status: AuthorizationStatus.ACCEPTED })
- )
-
- // Wait for TTL to expire
- await sleep(1100)
- const result = cache.get('EXPIRE-ID')
-
- assert.notStrictEqual(result, undefined)
- assert.strictEqual(result?.status, AuthorizationStatus.EXPIRED)
- } finally {
- cache.dispose()
- }
- })
-
- // G04.INT.06 - Local Auth List exclusion (T8/R17)
- await it('G04.INT.06: identifiers from local auth list are not cached', async () => {
+ // C13.FR.01.INT.01 - Local Auth List exclusion (R17)
+ await it('C13.FR.01.INT.01: identifiers from local auth list are not cached', async () => {
const cache = new InMemoryAuthCache({ cleanupIntervalSeconds: 0 })
try {
const listManager = createMockLocalAuthListManager({
cache.dispose()
}
})
-
- // G04.INT.07 - Cache lifecycle with stats preservation (T11)
- await it('G04.INT.07: clear preserves stats, resetStats zeroes them', () => {
- const cache = new InMemoryAuthCache({ cleanupIntervalSeconds: 0 })
- try {
- // Perform some operations to generate stats
- cache.set(
- 'STATS-ID',
- createMockAuthorizationResult({ status: AuthorizationStatus.ACCEPTED })
- )
- cache.get('STATS-ID')
- cache.get('NONEXISTENT')
-
- const statsBefore = cache.getStats()
- assert.ok(statsBefore.hits > 0)
- assert.ok(statsBefore.misses > 0)
-
- // Clear entries — stats should be preserved
- cache.clear()
- const statsAfterClear = cache.getStats()
- assert.strictEqual(statsAfterClear.totalEntries, 0)
- assert.strictEqual(statsAfterClear.hits, statsBefore.hits)
- assert.strictEqual(statsAfterClear.misses, statsBefore.misses)
-
- // Reset stats — counters should be zeroed
- cache.resetStats()
- const statsAfterReset = cache.getStats()
- assert.strictEqual(statsAfterReset.hits, 0)
- assert.strictEqual(statsAfterReset.misses, 0)
- assert.strictEqual(statsAfterReset.evictions, 0)
- } finally {
- cache.dispose()
- }
- })
})
})
})
})
- await describe('OCPP20AuthAdapter - G03.FR.02 Offline Authorization', async () => {
+ await describe('OCPP20AuthAdapter - C15 Offline Authorization', async () => {
let offlineAdapter: OCPP20AuthAdapter
let offlineMockChargingStation: ChargingStation
mock.reset()
})
- await describe('G03.FR.02.001 - Offline detection', async () => {
+ await describe('C15 - Offline detection', async () => {
await it('should detect station is offline when not in accepted state', () => {
// Given: Station is offline (not in accepted state)
offlineMockChargingStation.inAcceptedState = () => false
})
})
- await describe('G03.FR.02.002 - Remote availability check', async () => {
+ await describe('C15 - Remote availability check', async () => {
await it('should return false when offline even with valid configuration', () => {
// Given: Station is offline
offlineMockChargingStation.inAcceptedState = () => false
})
})
- await describe('G03.FR.02.003 - Configuration validation', async () => {
+ await describe('Configuration validation', async () => {
await it('should initialize with default configuration for offline scenarios', () => {
// When: Adapter is created
// Then: Should have OCPP 2.0 version
/**
* @file Tests for InMemoryAuthCache
- * @description Unit tests for in-memory authorization cache conformance (G03.FR.01)
+ * @description Unit tests for in-memory authorization cache conformance — OCPP 2.0.1 Authorization Cache (C10/C11/C12)
*/
import assert from 'node:assert/strict'
import { afterEach, beforeEach, describe, it } from 'node:test'
import { createMockAuthorizationResult } from '../helpers/MockFactories.js'
/**
- * OCPP 2.0 Cache Conformance Tests (G03.FR.01)
+ * OCPP 2.0.1 Authorization Cache Conformance Tests (C10/C11/C12)
*
* Tests verify:
- * - Cache hit/miss behavior
- * - TTL-based expiration
- * - Cache invalidation
+ * - Cache hit/miss behavior (C10, C12)
+ * - TTL-based expiration (C10.FR.08)
+ * - Cache invalidation (C11)
* - Rate limiting (security)
- * - LRU eviction
+ * - LRU eviction (C10.FR.07)
* - Statistics accuracy
*/
-await describe('InMemoryAuthCache - G03.FR.01 Conformance', async () => {
+await describe('InMemoryAuthCache - OCPP 2.0.1 Authorization Cache Conformance', async () => {
let cache: InMemoryAuthCache
beforeEach(() => {
standardCleanup()
})
- await describe('G03.FR.01.001 - Cache Hit Behavior', async () => {
+ await describe('C10 - Cache Hit Behavior', async () => {
let mockResult: AuthorizationResult
beforeEach(() => {
})
})
- await describe('G03.FR.01.002 - Cache Miss Behavior', async () => {
+ await describe('C12 - Cache Miss Behavior', async () => {
let mockResult: AuthorizationResult
beforeEach(() => {
})
})
- await describe('G03.FR.01.003 - Cache Expiration (TTL)', async () => {
+ await describe('C10.FR.08 - Cache Expiration (TTL)', async () => {
let mockResult: AuthorizationResult
beforeEach(() => {
})
})
- await describe('G03.FR.01.004 - Cache Invalidation', async () => {
+ await describe('C11 - Cache Invalidation', async () => {
let mockResult: AuthorizationResult
beforeEach(() => {
})
})
- await describe('G03.FR.01.005 - Rate Limiting (Security)', async () => {
+ await describe('Rate Limiting (Security)', async () => {
let mockResult: AuthorizationResult
beforeEach(() => {
})
})
- await describe('G03.FR.01.006 - LRU Eviction', async () => {
+ await describe('C10.FR.07 - LRU Eviction', async () => {
let mockResult: AuthorizationResult
beforeEach(() => {
})
})
- await describe('G03.FR.01.007 - Statistics & Monitoring', async () => {
+ await describe('Statistics & Monitoring', async () => {
let mockResult: AuthorizationResult
beforeEach(() => {
})
})
- await describe('G03.FR.01.008 - Edge Cases', async () => {
+ await describe('Edge Cases', async () => {
let mockResult: AuthorizationResult
beforeEach(() => {
})
})
- await describe('G03.FR.01.009 - Integration with Auth System', async () => {
+ await describe('Integration with Auth System', async () => {
await it('should cache ACCEPTED authorization results', () => {
const mockResult = createMockAuthorizationResult({
method: AuthenticationMethod.REMOTE_AUTHORIZATION,
})
})
- await describe('G03.FR.01.T5 - Status-aware Eviction (R2)', async () => {
- await it('G03.FR.01.T5.01 - should evict non-valid entry before valid one', () => {
+ await describe('C10.FR.07 - Status-aware Eviction', async () => {
+ await it('C10.FR.07.T01 - should evict non-valid entry before valid one', () => {
const lruCache = new InMemoryAuthCache({
defaultTtl: 3600,
maxEntries: 2,
assert.notStrictEqual(newResult, undefined)
})
- await it('G03.FR.01.T5.02 - should fall back to LRU when all entries are ACCEPTED', () => {
+ await it('C10.FR.07.T02 - should fall back to LRU when all entries are ACCEPTED', () => {
const lruCache = new InMemoryAuthCache({
defaultTtl: 3600,
maxEntries: 2,
})
})
- await describe('G03.FR.01.T6 - TTL Reset on Access (R16, R5)', async () => {
- await it('G03.FR.01.T6.01 - should reset TTL on cache hit', async t => {
+ await describe('C10.FR.08 - TTL Reset on Access', async () => {
+ await it('C10.FR.08.T01 - should reset TTL on cache hit', async t => {
await withMockTimers(t, ['Date'], () => {
const shortCache = new InMemoryAuthCache({
defaultTtl: 0.15, // 150ms
})
})
- await it('G03.FR.01.T6.02 - should not reset TTL when max absolute lifetime exceeded', async t => {
+ await it('C10.FR.08.T02 - should not reset TTL when max absolute lifetime exceeded', async t => {
await withMockTimers(t, ['Date'], () => {
const shortCache = new InMemoryAuthCache({
defaultTtl: 0.15, // 150ms
})
})
- await describe('G03.FR.01.T7 - Expired Entry Lifecycle (R10)', async () => {
- await it('G03.FR.01.T7.01 - should return EXPIRED status instead of undefined', async t => {
+ await describe('C10.FR.08 - Expired Entry Lifecycle', async () => {
+ await it('C10.FR.08.T03 - should return EXPIRED status instead of undefined', async t => {
await withMockTimers(t, ['Date'], () => {
const shortCache = new InMemoryAuthCache({
defaultTtl: 0.001,
})
})
- await it('G03.FR.01.T7.02 - should keep expired entry in cache after first access', async t => {
+ await it('C10.FR.08.T04 - should keep expired entry in cache after first access', async t => {
await withMockTimers(t, ['Date'], () => {
const shortCache = new InMemoryAuthCache({
defaultTtl: 0.001,
})
})
- await describe('G03.FR.01.T10 - Periodic Cleanup and Rate Limit Bounds (R8, R9)', async () => {
- await it('G03.FR.01.T10.01 - dispose() stops the cleanup interval and is safe to call twice', () => {
+ await describe('Periodic Cleanup and Rate Limit Bounds', async () => {
+ await it('should stop the cleanup interval and be safe to call dispose() twice', () => {
const cleanupCache = new InMemoryAuthCache({
cleanupIntervalSeconds: 1,
defaultTtl: 3600,
})
})
- await it('G03.FR.01.T10.02 - cleanup interval is not started when cleanupIntervalSeconds is 0', () => {
+ await it('should not start cleanup interval when cleanupIntervalSeconds is 0', () => {
const noCleanupCache = new InMemoryAuthCache({
cleanupIntervalSeconds: 0,
defaultTtl: 3600,
noCleanupCache.dispose()
})
- await it('G03.FR.01.T10.03 - runCleanup removes expired entries (two-phase)', async t => {
+ await it('should remove expired entries via runCleanup (two-phase)', async t => {
await withMockTimers(t, ['Date'], () => {
const cleanupCache = new InMemoryAuthCache({
cleanupIntervalSeconds: 0,
})
})
- await it('G03.FR.01.T10.04 - rateLimits map is bounded to maxEntries * 2', () => {
+ await it('should bound rateLimits map to maxEntries * 2', () => {
const boundedCache = new InMemoryAuthCache({
cleanupIntervalSeconds: 0,
defaultTtl: 3600,
})
})
- await describe('G03.FR.01.T11 - Stats preservation and resetStats() (R14, R15)', async () => {
- await it('G03.FR.01.T11.01 - clear() preserves hits, misses, evictions', () => {
+ await describe('Stats preservation and resetStats()', async () => {
+ await it('should preserve hits, misses, evictions on clear()', () => {
const statsCache = new InMemoryAuthCache({
cleanupIntervalSeconds: 0,
defaultTtl: 3600,
statsCache.dispose()
})
- await it('G03.FR.01.T11.02 - resetStats() zeroes all stat fields', () => {
+ await it('should zero all stat fields on resetStats()', () => {
const statsCache = new InMemoryAuthCache({
cleanupIntervalSeconds: 0,
defaultTtl: 3600,
statsCache.dispose()
})
- await it('G03.FR.01.T11.03 - resetStats() after clear() zeroes stats correctly', () => {
+ await it('should zero stats correctly on resetStats() after clear()', () => {
const statsCache = new InMemoryAuthCache({
cleanupIntervalSeconds: 0,
defaultTtl: 3600,
})
})
- await describe('G03.FR.01.100 - Cache Wiring', async () => {
+ await describe('Cache Wiring', async () => {
let mockStation: ChargingStation
beforeEach(() => {
mockStation = createMockAuthServiceTestStation('cache-wiring')
})
- await it('G03.FR.01.101 - local strategy has a defined authCache after initialization', async () => {
+ await it('should have a defined authCache after initialization', async () => {
const authService = new OCPPAuthServiceImpl(mockStation)
await authService.initialize()
assert.strictEqual(cachedTtl, 300)
})
- await it('G03.FR.01.T4.01 - should cache BLOCKED authorization status', async () => {
+ await it('C10.FR.01.T01 - should cache BLOCKED authorization status', async () => {
let cachedKey: string | undefined
let cachedValue: AuthorizationResult | undefined
let cachedTtl: number | undefined
assert.strictEqual(cachedTtl, 300)
})
- await it('G03.FR.01.T4.02 - should cache EXPIRED authorization status', async () => {
+ await it('C10.FR.01.T02 - should cache EXPIRED authorization status', async () => {
let cachedKey: string | undefined
let cachedValue: AuthorizationResult | undefined
let cachedTtl: number | undefined
assert.strictEqual(cachedTtl, 300)
})
- await it('G03.FR.01.T4.03 - should cache INVALID authorization status', async () => {
+ await it('C10.FR.01.T03 - should cache INVALID authorization status', async () => {
let cachedKey: string | undefined
let cachedValue: AuthorizationResult | undefined
let cachedTtl: number | undefined
assert.strictEqual(cachedTtl, 300)
})
- await it('G03.FR.01.T4.04 - should still cache ACCEPTED authorization status (regression)', async () => {
+ await it('C10.FR.01.T04 - should still cache ACCEPTED authorization status (regression)', async () => {
let cachedKey: string | undefined
let cachedValue: AuthorizationResult | undefined
let cachedTtl: number | undefined
assert.strictEqual(result?.status, AuthorizationStatus.ACCEPTED)
})
- await it('G03.FR.01.T8.01 - should not cache identifier that is in local auth list', async () => {
+ await it('C13.FR.01.T01 - should not cache identifier that is in local auth list', async () => {
let cachedKey: string | undefined
mockAuthCache.set = (key: string) => {
cachedKey = key
assert.strictEqual(cachedKey, undefined)
})
- await it('G03.FR.01.T8.02 - should cache identifier that is not in local auth list', async () => {
+ await it('C10.FR.01.T05 - should cache identifier that is not in local auth list', async () => {
let cachedKey: string | undefined
mockAuthCache.set = (key: string) => {
cachedKey = key