- MongoDB storage: cwmp_exchanges collection with indexes (deviceId+timestamp, deviceId+sequenceNumber unique, TTL 24h) - ExchangeStoreService: CWMP method extraction, per-device queue for ordering, retry logic, message cap (5000/device) - Exchange API: GET/DELETE/SSE stream endpoints with Last-Event-ID reconnection - CwmpLoggerMiddleware: dual-write (MongoDB + legacy files) with fire-and-forget - Frontend: SequenceViewer (CPE↔ACS lifelines, arrows, auto-scroll, virtual scroll) + InspectorPanel (formatted XML, copy, syntax highlighting) - Tr069Trace page: integrated new viewer with col-6/col-6 layout - DeviceDetail page: collapsible exchange section when device is traced - 14 property-based tests, 40+ unit tests, 5 integration tests - Health check includes index verification - Config: feature flags, SSE timeouts, retry policy
146 lines
4.9 KiB
TypeScript
146 lines
4.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
|
|
/**
|
|
* Unit tests for useExchangeStream memory management (max 1000 cached messages).
|
|
* Validates: Requirement 7.1 - client-side message cap enforcement
|
|
*
|
|
* We test the appendMessage logic directly by extracting it into a testable pattern
|
|
* since the composable relies on Vue reactivity and EventSource which require mocking.
|
|
*/
|
|
|
|
const MAX_CACHED_MESSAGES = 1000
|
|
|
|
interface CwmpExchange {
|
|
deviceId: string
|
|
direction: 'CPE→ACS' | 'ACS→CPE'
|
|
timestamp: string
|
|
sessionId: string
|
|
cwmpMethod: string
|
|
rawXml: string
|
|
sequenceNumber: number
|
|
createdAt?: string
|
|
}
|
|
|
|
/**
|
|
* Simulates the appendMessage logic from useExchangeStream.ts
|
|
* This is the exact same logic used in the composable.
|
|
*/
|
|
function appendMessage(messages: CwmpExchange[], exchange: CwmpExchange): CwmpExchange[] {
|
|
let result = [...messages, exchange]
|
|
if (result.length > MAX_CACHED_MESSAGES) {
|
|
result = result.slice(-MAX_CACHED_MESSAGES)
|
|
}
|
|
return result
|
|
}
|
|
|
|
function createExchange(sequenceNumber: number): CwmpExchange {
|
|
return {
|
|
deviceId: 'test-device',
|
|
direction: 'CPE→ACS',
|
|
timestamp: new Date(Date.now() + sequenceNumber * 1000).toISOString(),
|
|
sessionId: 'session-1',
|
|
cwmpMethod: 'Inform',
|
|
rawXml: `<soap:Envelope><soap:Body><cwmp:Inform/></soap:Body></soap:Envelope>`,
|
|
sequenceNumber,
|
|
}
|
|
}
|
|
|
|
describe('useExchangeStream memory management', () => {
|
|
describe('appendMessage cap enforcement', () => {
|
|
it('should allow messages up to MAX_CACHED_MESSAGES without discarding', () => {
|
|
let messages: CwmpExchange[] = []
|
|
|
|
for (let i = 1; i <= MAX_CACHED_MESSAGES; i++) {
|
|
messages = appendMessage(messages, createExchange(i))
|
|
}
|
|
|
|
expect(messages.length).toBe(MAX_CACHED_MESSAGES)
|
|
expect(messages[0].sequenceNumber).toBe(1)
|
|
expect(messages[messages.length - 1].sequenceNumber).toBe(MAX_CACHED_MESSAGES)
|
|
})
|
|
|
|
it('should discard oldest message when exceeding the cap', () => {
|
|
let messages: CwmpExchange[] = []
|
|
|
|
// Fill to cap
|
|
for (let i = 1; i <= MAX_CACHED_MESSAGES; i++) {
|
|
messages = appendMessage(messages, createExchange(i))
|
|
}
|
|
|
|
// Add one more (1001)
|
|
messages = appendMessage(messages, createExchange(MAX_CACHED_MESSAGES + 1))
|
|
|
|
expect(messages.length).toBe(MAX_CACHED_MESSAGES)
|
|
// Oldest (seq 1) should be discarded, newest starts at seq 2
|
|
expect(messages[0].sequenceNumber).toBe(2)
|
|
expect(messages[messages.length - 1].sequenceNumber).toBe(MAX_CACHED_MESSAGES + 1)
|
|
})
|
|
|
|
it('should always keep the most recent messages when adding many past the cap', () => {
|
|
let messages: CwmpExchange[] = []
|
|
|
|
// Add 1500 messages (500 over the cap)
|
|
const totalMessages = 1500
|
|
for (let i = 1; i <= totalMessages; i++) {
|
|
messages = appendMessage(messages, createExchange(i))
|
|
}
|
|
|
|
expect(messages.length).toBe(MAX_CACHED_MESSAGES)
|
|
// Should keep messages 501 through 1500
|
|
expect(messages[0].sequenceNumber).toBe(totalMessages - MAX_CACHED_MESSAGES + 1)
|
|
expect(messages[messages.length - 1].sequenceNumber).toBe(totalMessages)
|
|
})
|
|
|
|
it('should handle empty initial state', () => {
|
|
let messages: CwmpExchange[] = []
|
|
messages = appendMessage(messages, createExchange(1))
|
|
|
|
expect(messages.length).toBe(1)
|
|
expect(messages[0].sequenceNumber).toBe(1)
|
|
})
|
|
|
|
it('should never exceed MAX_CACHED_MESSAGES regardless of input pattern', () => {
|
|
let messages: CwmpExchange[] = []
|
|
|
|
// Rapid burst of 2000 messages
|
|
for (let i = 1; i <= 2000; i++) {
|
|
messages = appendMessage(messages, createExchange(i))
|
|
expect(messages.length).toBeLessThanOrEqual(MAX_CACHED_MESSAGES)
|
|
}
|
|
|
|
expect(messages.length).toBe(MAX_CACHED_MESSAGES)
|
|
})
|
|
|
|
it('should preserve chronological order after cap enforcement', () => {
|
|
let messages: CwmpExchange[] = []
|
|
|
|
for (let i = 1; i <= 1200; i++) {
|
|
messages = appendMessage(messages, createExchange(i))
|
|
}
|
|
|
|
// Verify all remaining messages are in ascending order
|
|
for (let i = 1; i < messages.length; i++) {
|
|
expect(messages[i].sequenceNumber).toBeGreaterThan(messages[i - 1].sequenceNumber)
|
|
expect(messages[i].timestamp > messages[i - 1].timestamp).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('should correctly handle cap at exactly MAX_CACHED_MESSAGES boundary', () => {
|
|
let messages: CwmpExchange[] = []
|
|
|
|
// Add exactly MAX_CACHED_MESSAGES messages
|
|
for (let i = 1; i <= MAX_CACHED_MESSAGES; i++) {
|
|
messages = appendMessage(messages, createExchange(i))
|
|
}
|
|
|
|
expect(messages.length).toBe(MAX_CACHED_MESSAGES)
|
|
expect(messages[0].sequenceNumber).toBe(1)
|
|
|
|
// Add one more to go over by 1
|
|
messages = appendMessage(messages, createExchange(MAX_CACHED_MESSAGES + 1))
|
|
expect(messages.length).toBe(MAX_CACHED_MESSAGES)
|
|
expect(messages[0].sequenceNumber).toBe(2)
|
|
})
|
|
})
|
|
})
|