Files
adam-acs/tests/unit/services/tr069_trace_service.spec.ts
T
Kevin L'HereecandCopilot 847da1cdf5 feat: per-device TR-069 SOAP trace logging
- Tr069TraceService: in-memory Map O(1) lookup, Promise-queue per device
  for sequential file writes, 10MB rotation (5 files kept)
- config/tr069-trace.json: persisted list of traced devices (atomic write)
- CwmpLoggerMiddleware: buffers request+response, writes to device log file
  after next() once ctx.cwmpDeviceId is set by controller
- CwmpController: sets ctx.cwmpDeviceId on all code paths (Inform + others)
- StateMachineManager: getHashIdForRemote() for non-Inform messages
- REST API: GET/POST/DELETE /api/v1/tr069-trace
- UI: Tr069Trace Vue page with device search, add/remove tracing
- 8 new unit tests (192 total, all passing)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 12:07:07 +02:00

149 lines
4.4 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const fsMocks = vi.hoisted(() => ({
appendFileSync: vi.fn(),
mkdirSync: vi.fn(),
readFileSync: vi.fn(),
renameSync: vi.fn(),
statSync: vi.fn(),
}))
const fsPromisesMocks = vi.hoisted(() => ({
writeFile: vi.fn(),
}))
vi.mock('node:fs', () => ({
appendFileSync: fsMocks.appendFileSync,
mkdirSync: fsMocks.mkdirSync,
readFileSync: fsMocks.readFileSync,
renameSync: fsMocks.renameSync,
statSync: fsMocks.statSync,
}))
vi.mock('node:fs/promises', () => ({
writeFile: fsPromisesMocks.writeFile,
}))
import { Tr069TraceService } from '../../../app/services/tr069_trace_service.js'
describe('Tr069TraceService', () => {
const baseEntry = {
deviceId: 'device-123',
manufacturer: 'Orange',
oui: 'A1B2C3',
productClass: 'Livebox',
serialNumber: 'SN123456',
}
beforeEach(() => {
vi.clearAllMocks()
fsMocks.readFileSync.mockImplementation(() => {
throw new Error('missing config')
})
fsMocks.statSync.mockImplementation(() => {
throw new Error('missing log')
})
;(Tr069TraceService as any).instance = undefined
})
it('returns false for untracked devices', () => {
const service = Tr069TraceService.getInstance()
expect(service.isTraced('unknown-device')).toBe(false)
})
it('returns true after adding a device', async () => {
const service = Tr069TraceService.getInstance()
await service.addDevice(baseEntry)
expect(service.isTraced(baseEntry.deviceId)).toBe(true)
})
it('lists all traced devices', async () => {
const service = Tr069TraceService.getInstance()
await service.addDevice(baseEntry)
await service.addDevice({
...baseEntry,
deviceId: 'device-456',
serialNumber: 'SN654321',
})
expect(service.list()).toHaveLength(2)
expect(service.list().map((entry) => entry.deviceId)).toEqual(['device-123', 'device-456'])
})
it('adds a device with an auto-generated startedAt value', async () => {
const service = Tr069TraceService.getInstance()
await service.addDevice(baseEntry)
const [entry] = service.list()
expect(entry.startedAt).toBeTypeOf('string')
expect(Number.isNaN(Date.parse(entry.startedAt))).toBe(false)
})
it('removes a traced device and returns true', async () => {
const service = Tr069TraceService.getInstance()
await service.addDevice(baseEntry)
await expect(service.removeDevice(baseEntry.deviceId)).resolves.toBe(true)
expect(service.isTraced(baseEntry.deviceId)).toBe(false)
})
it('returns false when removing an unknown device', async () => {
const service = Tr069TraceService.getInstance()
await expect(service.removeDevice('unknown-device')).resolves.toBe(false)
})
it('persists config after add and remove operations', async () => {
const service = Tr069TraceService.getInstance()
await service.addDevice(baseEntry)
await service.removeDevice(baseEntry.deviceId)
expect(fsPromisesMocks.writeFile).toHaveBeenCalledTimes(2)
expect(fsPromisesMocks.writeFile).toHaveBeenNthCalledWith(
1,
expect.stringContaining('config/tr069-trace.json.tmp'),
expect.stringContaining(baseEntry.deviceId),
'utf-8'
)
expect(fsPromisesMocks.writeFile).toHaveBeenNthCalledWith(
2,
expect.stringContaining('config/tr069-trace.json.tmp'),
JSON.stringify({ entries: [] }, null, 2),
'utf-8'
)
expect(fsMocks.renameSync).toHaveBeenCalledWith(
expect.stringContaining('config/tr069-trace.json.tmp'),
expect.stringContaining('config/tr069-trace.json')
)
})
it('rotates log files when size exceeds the limit', async () => {
const service = Tr069TraceService.getInstance()
fsMocks.statSync.mockReturnValue({ size: 10 * 1024 * 1024 })
await (service as any).doWrite(baseEntry.deviceId, 'CPE→ACS', '<Inform />')
expect(fsMocks.renameSync).toHaveBeenNthCalledWith(
1,
expect.stringContaining(`${baseEntry.deviceId}.log.4`),
expect.stringContaining(`${baseEntry.deviceId}.log.5`)
)
expect(fsMocks.renameSync).toHaveBeenLastCalledWith(
expect.stringContaining(`${baseEntry.deviceId}.log`),
expect.stringContaining(`${baseEntry.deviceId}.log.1`)
)
expect(fsMocks.appendFileSync).toHaveBeenCalledWith(
expect.stringContaining(`${baseEntry.deviceId}.log`),
expect.stringContaining('CPE→ACS'),
'utf-8'
)
})
})