import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import fc from 'fast-check' // Hoist mocks before any module resolution const loggerMock = vi.hoisted(() => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), })) const insertOneMock = vi.hoisted(() => vi.fn()) const deleteOldestBeyondCapMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)) const findOneAndUpdateMock = vi.hoisted(() => vi.fn().mockResolvedValue({ seq: 1 })) vi.mock('@adonisjs/core/services/logger', () => ({ default: loggerMock, })) vi.mock('#start/env', () => ({ default: { get: (key: string, defaultValue?: any) => defaultValue, }, })) vi.mock('#models/exchange', () => ({ default: { getCollection: () => ({ insertOne: insertOneMock, }), deleteOldestBeyondCap: deleteOldestBeyondCapMock, }, })) vi.mock('#providers/mongodb_provider', () => ({ getMongoDb: () => ({ collection: () => ({ findOneAndUpdate: findOneAndUpdateMock, }), }), })) vi.mock('#config/exchange_store', () => ({ default: { maxMessagesPerDevice: 5000, retry: { maxAttempts: 3, baseDelayMs: 1000, }, featureFlags: { persistence: true, sseEnabled: true, apiSource: 'mongodb', }, }, })) import { ExchangeStoreService, CRITICAL_METHODS, NON_CRITICAL_METHODS, } from '../../../app/services/exchange_store_service.js' import type { CwmpExchange } from '../../../app/models/exchange.js' const PBT_NUM_RUNS = 100 /** * Property 1: CWMP Method Extraction * Validates: Requirements 1.2, 1.3, 1.6 * * For any SOAP XML body, the `extractCwmpMethod` function SHALL return the first child element * name under the `cwmp:` namespace prefix if one exists, "Empty" if the XML is empty/whitespace-only, * or "Unknown" if no `cwmp:` namespace element is found. */ describe('Property 1: CWMP Method Extraction', () => { let service: ExchangeStoreService beforeEach(() => { vi.clearAllMocks() ;(ExchangeStoreService as any).instance = undefined service = ExchangeStoreService.getInstance() }) // ─── Arbitraries ───────────────────────────────────────────────────────────── /** * Arbitrary that generates valid CWMP method names (alphanumeric word characters). */ const cwmpMethodName = fc .array( fc.constantFrom( ...'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'.split('') ), { minLength: 1, maxLength: 40 } ) .map((chars) => chars.join('')) /** * Arbitrary that generates valid SOAP XML with a cwmp: namespace element. */ const validCwmpXml = fc .record({ method: cwmpMethodName, hasXmlDecl: fc.boolean(), hasBody: fc.boolean(), selfClosing: fc.boolean(), }) .map(({ method, hasXmlDecl, hasBody, selfClosing }) => { let xml = '' if (hasXmlDecl) { xml += '\n' } xml += '\n' if (hasBody) { xml += ' \n' } if (selfClosing) { xml += ` \n` } else { xml += ` \n test\n \n` } if (hasBody) { xml += ' \n' } xml += '' return { xml, method } }) /** * Arbitrary that generates XML without any cwmp: namespace element. */ const noCwmpXml = fc .record({ tagName: fc .array(fc.constantFrom(...'abcdefghijklmnopqrstuvwxyz'.split('')), { minLength: 1, maxLength: 20, }) .map((chars) => chars.join('')), content: fc.string({ minLength: 0, maxLength: 50 }).map((s) => s.replace(/[<>&"']/g, 'x')), }) .map(({ tagName, content }) => { return `<${tagName}>${content}` }) /** * Arbitrary that generates empty or whitespace-only strings. */ const emptyOrWhitespace = fc.constantFrom('', ' ', ' ', '\t', '\n', ' \n\t ', '\r\n') // ─── Property Tests ────────────────────────────────────────────────────────── it('returns the cwmp method name for valid SOAP XML with cwmp namespace', () => { fc.assert( fc.property(validCwmpXml, ({ xml, method }) => { const result = service.extractCwmpMethod(xml) expect(result).toBe(method) }), { numRuns: PBT_NUM_RUNS } ) }) it('returns "Empty" for empty or whitespace-only XML', () => { fc.assert( fc.property(emptyOrWhitespace, (xml) => { const result = service.extractCwmpMethod(xml) expect(result).toBe('Empty') }), { numRuns: PBT_NUM_RUNS } ) }) it('returns "Unknown" for XML without cwmp: namespace', () => { fc.assert( fc.property(noCwmpXml, (xml) => { const result = service.extractCwmpMethod(xml) expect(result).toBe('Unknown') }), { numRuns: PBT_NUM_RUNS } ) }) it('returns "Unknown" for malformed XML without cwmp namespace', () => { const malformedXml = fc .string({ minLength: 1, maxLength: 200 }) .filter((s) => s.trim() !== '' && !s.includes(' { const result = service.extractCwmpMethod(xml) expect(result).toBe('Unknown') }), { numRuns: PBT_NUM_RUNS } ) }) it('extracts the FIRST cwmp method when multiple cwmp elements exist', () => { const multipleCwmpXml = fc .record({ method1: cwmpMethodName, method2: cwmpMethodName, }) .map(({ method1, method2 }) => ({ xml: ` `, expectedMethod: method1, })) fc.assert( fc.property(multipleCwmpXml, ({ xml, expectedMethod }) => { const result = service.extractCwmpMethod(xml) expect(result).toBe(expectedMethod) }), { numRuns: PBT_NUM_RUNS } ) }) it('result is always one of: valid method name, "Empty", or "Unknown"', () => { fc.assert( fc.property(fc.string({ minLength: 0, maxLength: 500 }), (xml) => { const result = service.extractCwmpMethod(xml) const isValidMethod = /^\w+$/.test(result) const isSpecialValue = result === 'Empty' || result === 'Unknown' expect(isValidMethod || isSpecialValue).toBe(true) }), { numRuns: PBT_NUM_RUNS } ) }) it('handles cwmp elements with attributes correctly', () => { const cwmpWithAttributes = fc .record({ method: cwmpMethodName, attrName: fc .array(fc.constantFrom(...'abcdefghijklmnopqrstuvwxyz'.split('')), { minLength: 1, maxLength: 10, }) .map((chars) => chars.join('')), attrValue: fc .array(fc.constantFrom(...'abcdefghijklmnopqrstuvwxyz0123456789'.split('')), { minLength: 1, maxLength: 10, }) .map((chars) => chars.join('')), }) .map(({ method, attrName, attrValue }) => ({ xml: ``, method, })) fc.assert( fc.property(cwmpWithAttributes, ({ xml, method }) => { const result = service.extractCwmpMethod(xml) expect(result).toBe(method) }), { numRuns: PBT_NUM_RUNS } ) }) }) /** * Property 4: Retry Classification Correctness * Validates: Requirements 1.7 * * For any CWMP message where a MongoDB write failure occurs: * - If the cwmpMethod is in the critical set → exactly 3 retry attempts with exponential backoff SHALL be made * (1 initial attempt + 3 retries = 4 total insertOne calls) * - If the cwmpMethod is non-critical (Empty, Unknown) → the message SHALL be discarded immediately with zero retries * (1 initial attempt only = 1 total insertOne call) */ describe('Property 4: Retry Classification Correctness', () => { let service: ExchangeStoreService beforeEach(() => { vi.clearAllMocks() vi.useFakeTimers() // Reset singleton for testing ;(ExchangeStoreService as any).instance = undefined service = ExchangeStoreService.getInstance() }) afterEach(() => { vi.useRealTimers() }) // ─── Arbitraries ───────────────────────────────────────────────────────────── /** * Generate a random critical CWMP method from the defined set */ const arbCriticalMethod = fc.constantFrom(...Array.from(CRITICAL_METHODS)) /** * Generate a random non-critical CWMP method from the defined set */ const arbNonCriticalMethod = fc.constantFrom(...Array.from(NON_CRITICAL_METHODS)) /** * Generate a random unknown method name (not in critical or non-critical sets) * These should behave as non-critical (isCriticalMethod returns false) */ const arbUnknownMethod = fc .string({ minLength: 1, maxLength: 30 }) .filter( (s) => !CRITICAL_METHODS.has(s) && !NON_CRITICAL_METHODS.has(s) && s.trim().length > 0 ) /** * Build a CwmpExchange document with a given method */ function makeDoc(method: string): CwmpExchange { return { deviceId: 'device-test', direction: 'CPE→ACS', timestamp: new Date().toISOString(), sessionId: '10.0.0.1:54321->192.168.1.1:8080', cwmpMethod: method, rawXml: ``, sequenceNumber: 1, createdAt: new Date(), } } // ─── Property Tests ────────────────────────────────────────────────────────── it('critical methods → isCriticalMethod returns true and persistWithRetry makes 4 total calls (1 initial + 3 retries)', () => { return fc.assert( fc.asyncProperty(arbCriticalMethod, async (method) => { vi.clearAllMocks() // Mock insertOne to always fail insertOneMock.mockRejectedValue(new Error('MongoDB write failed')) const doc = makeDoc(method) const isCritical = service.isCriticalMethod(method) // Verify classification expect(isCritical).toBe(true) const promise = service.persistWithRetry(doc, isCritical) // Advance through all retry delays: // Initial attempt fails immediately // Retry 1: after 1000ms (baseDelayMs * 2^0) // Retry 2: after 2000ms (baseDelayMs * 2^1) // Retry 3: after 4000ms (baseDelayMs * 2^2) await vi.advanceTimersByTimeAsync(10000) await promise // Critical: 1 initial attempt + 3 retries = 4 total calls expect(insertOneMock).toHaveBeenCalledTimes(4) }), { numRuns: PBT_NUM_RUNS } ) }) it('non-critical methods → isCriticalMethod returns false and persistWithRetry makes exactly 1 call (no retries)', () => { return fc.assert( fc.asyncProperty(arbNonCriticalMethod, async (method) => { vi.clearAllMocks() // Mock insertOne to always fail insertOneMock.mockRejectedValue(new Error('MongoDB write failed')) const doc = makeDoc(method) const isCritical = service.isCriticalMethod(method) // Verify classification expect(isCritical).toBe(false) await service.persistWithRetry(doc, isCritical) // Non-critical: 1 initial attempt only, discarded immediately expect(insertOneMock).toHaveBeenCalledTimes(1) }), { numRuns: PBT_NUM_RUNS } ) }) it('unknown methods (not in critical or non-critical sets) → classified as non-critical with 0 retries', () => { return fc.assert( fc.asyncProperty(arbUnknownMethod, async (method) => { vi.clearAllMocks() // Mock insertOne to always fail insertOneMock.mockRejectedValue(new Error('MongoDB write failed')) const doc = makeDoc(method) const isCritical = service.isCriticalMethod(method) // Unknown methods are NOT in the critical set expect(isCritical).toBe(false) await service.persistWithRetry(doc, isCritical) // Non-critical: 1 initial attempt only, discarded immediately expect(insertOneMock).toHaveBeenCalledTimes(1) }), { numRuns: PBT_NUM_RUNS } ) }) it('persistWithRetry never throws regardless of method classification (CWMP session never interrupted)', () => { const arbAnyMethod = fc.oneof(arbCriticalMethod, arbNonCriticalMethod, arbUnknownMethod) return fc.assert( fc.asyncProperty(arbAnyMethod, async (method) => { vi.clearAllMocks() // Mock insertOne to always fail with any error insertOneMock.mockRejectedValue(new Error('catastrophic failure')) const doc = makeDoc(method) const isCritical = service.isCriticalMethod(method) const promise = service.persistWithRetry(doc, isCritical) // Advance timers to allow retries to complete await vi.advanceTimersByTimeAsync(10000) // Should NEVER throw await expect(promise).resolves.toBeUndefined() }), { numRuns: PBT_NUM_RUNS } ) }) })