Files
adam-acs/tests/integration/ttl_expiration.spec.ts
T
Kevin L'Hereec 0c83a4ccb2 feat: TR-069 Exchange Viewer - structured MongoDB storage with SSE streaming and sequence diagram UI
- 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
2026-06-03 15:52:44 +02:00

125 lines
4.7 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
import { MongoClient, Db } from 'mongodb'
const MONGO_URI = 'mongodb://adam:adam@localhost:27017/adam-acs-test?authSource=admin'
const COLLECTION_NAME = 'cwmp_exchanges'
let client: MongoClient
let db: Db
describe('TTL expiration integration test', () => {
beforeAll(async () => {
client = new MongoClient(MONGO_URI)
await client.connect()
db = client.db('adam-acs-test')
// Create the TTL index (as the migration would)
const collection = db.collection(COLLECTION_NAME)
await collection.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 86400, name: 'idx_createdAt_ttl', background: true }
)
})
afterAll(async () => {
await db.dropDatabase()
await client.close()
})
beforeEach(async () => {
await db.collection(COLLECTION_NAME).deleteMany({})
})
/**
* Validates: Requirements 7.3
*
* Verify the TTL index is configured with expireAfterSeconds = 86400 (24 hours).
* In a real MongoDB deployment, the TTL monitor thread runs every 60s and removes
* documents whose createdAt field is older than 86400 seconds. For this test, we
* verify the index configuration is correct rather than waiting for the TTL monitor.
*/
it('should have a TTL index on createdAt with expireAfterSeconds = 86400', async () => {
const collection = db.collection(COLLECTION_NAME)
const indexes = await collection.indexes()
const ttlIndex = indexes.find((idx) => idx.name === 'idx_createdAt_ttl')
expect(ttlIndex).toBeDefined()
expect(ttlIndex!.key).toEqual({ createdAt: 1 })
expect(ttlIndex!.expireAfterSeconds).toBe(86400)
})
it('should store exchanges with createdAt set to 25 hours ago as expired candidates', async () => {
const collection = db.collection(COLLECTION_NAME)
const twentyFiveHoursAgo = new Date(Date.now() - 25 * 60 * 60 * 1000)
// Insert exchanges with createdAt set 25 hours in the past
const expiredExchanges = Array.from({ length: 5 }, (_, i) => ({
deviceId: 'device-ttl-test',
direction: 'CPE→ACS' as const,
timestamp: new Date(twentyFiveHoursAgo.getTime() + i * 1000).toISOString(),
sessionId: '10.0.0.1:54321->192.168.1.1:8080',
cwmpMethod: 'Inform',
rawXml: `<soap-env:Envelope><soap-env:Body><cwmp:Inform/></soap-env:Body></soap-env:Envelope>`,
sequenceNumber: i + 1,
createdAt: twentyFiveHoursAgo,
}))
await collection.insertMany(expiredExchanges)
// Verify the documents were inserted
const count = await collection.countDocuments({ deviceId: 'device-ttl-test' })
expect(count).toBe(5)
// Verify all documents have a createdAt older than 24 hours
const docs = await collection.find({ deviceId: 'device-ttl-test' }).toArray()
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
for (const doc of docs) {
expect(doc.createdAt).toBeInstanceOf(Date)
expect(doc.createdAt.getTime()).toBeLessThan(twentyFourHoursAgo.getTime())
}
})
it('should NOT mark recent exchanges as expired candidates', async () => {
const collection = db.collection(COLLECTION_NAME)
const now = new Date()
// Insert exchanges with current createdAt (should NOT be expired)
const recentExchanges = Array.from({ length: 3 }, (_, i) => ({
deviceId: 'device-ttl-recent',
direction: 'ACS→CPE' as const,
timestamp: new Date(now.getTime() + i * 1000).toISOString(),
sessionId: '10.0.0.1:54321->192.168.1.1:8080',
cwmpMethod: 'GetParameterValuesResponse',
rawXml: `<soap-env:Envelope><soap-env:Body><cwmp:GetParameterValuesResponse/></soap-env:Body></soap-env:Envelope>`,
sequenceNumber: i + 1,
createdAt: now,
}))
await collection.insertMany(recentExchanges)
// Verify documents exist and their createdAt is within 24 hours
const docs = await collection.find({ deviceId: 'device-ttl-recent' }).toArray()
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
expect(docs).toHaveLength(3)
for (const doc of docs) {
expect(doc.createdAt).toBeInstanceOf(Date)
expect(doc.createdAt.getTime()).toBeGreaterThan(twentyFourHoursAgo.getTime())
}
})
it('TTL index expireAfterSeconds should equal exactly 24 hours in seconds', async () => {
const collection = db.collection(COLLECTION_NAME)
const indexes = await collection.indexes()
const ttlIndex = indexes.find((idx) => idx.name === 'idx_createdAt_ttl')
expect(ttlIndex).toBeDefined()
// 24 hours = 24 * 60 * 60 = 86400 seconds
const twentyFourHoursInSeconds = 24 * 60 * 60
expect(ttlIndex!.expireAfterSeconds).toBe(twentyFourHoursInSeconds)
})
})