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', '') 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' ) }) })