import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { DyFM_HttpCallType } from '@futdevpro/fsm-dynamo'; import { DyNTS_global_settings } from '../../_collections/global-settings.const'; import { DyNTS_FileLog_Service } from './file-log.service'; import { DyNTS_FileLogs_Controller } from './file-logs.controller'; /** Minimal Request mock builder. */ const mockReq = (args: { params?: Record; query?: Record } = {}): any => ({ params: args.params ?? {}, query: args.query ?? {}, headers: {}, }); /** Minimal Response mock that records status + send payload. */ interface MockRes { statusCode: number; payload: any; status: (code: number) => MockRes; send: (data: any) => MockRes; } const mockRes = (): MockRes => { const res: MockRes = { statusCode: 200, payload: undefined, status(code: number): MockRes { res.statusCode = code; return res; }, send(data: any): MockRes { res.payload = data; return res; }, }; return res; }; /** Extract a task function by endpoint name. */ const taskByName = (controller: DyNTS_FileLogs_Controller, name: string): ((req: any, res: any) => Promise) => { const ep: any = controller.endpoints.find((e: any) => e.name === name); if (!ep) { throw new Error(`endpoint ${name} not found`); } return ep.tasks[0]; }; describe('| DyNTS_FileLogs_Controller', (): void => { let tempDir: string; let originalFileLogConfig: typeof DyNTS_global_settings.log_settings.file_log; beforeEach((): void => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nts-filelogs-ctrl-')); originalFileLogConfig = DyNTS_global_settings.log_settings.file_log; }); afterEach((): void => { DyNTS_FileLog_Service.getInstance()._teardownForTesting(); DyNTS_FileLogs_Controller.configure({}); DyNTS_global_settings.log_settings.file_log = originalFileLogConfig; try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch { /* swallow */ } }); describe('| singleton + endpoint shape', (): void => { it('| getInstance() singleton', (): void => { const a = DyNTS_FileLogs_Controller.getInstance(); const b = DyNTS_FileLogs_Controller.getInstance(); expect(a).toBe(b); }); it('| setupEndpoints letrehozza /list + /file/:filename endpointokat', (): void => { const ctrl = DyNTS_FileLogs_Controller.getInstance(); ctrl.setupEndpoints(); const list = ctrl.endpoints.find((e: any) => e.name === 'listFileLogs'); const read = ctrl.endpoints.find((e: any) => e.name === 'readFileLog'); expect(list).toBeDefined(); expect(list?.type).toBe(DyFM_HttpCallType.get); expect(read).toBeDefined(); expect(read?.type).toBe(DyFM_HttpCallType.get); }); it('| authPreProcess hozzaadodik preProcesses-be ha config-olva', (): void => { const myAuth = async (): Promise => { /* noop */ }; DyNTS_FileLogs_Controller.configure({ authPreProcess: myAuth }); const ctrl = DyNTS_FileLogs_Controller.getInstance(); ctrl.setupEndpoints(); const list: any = ctrl.endpoints.find((e: any) => e.name === 'listFileLogs'); expect(list).toBeDefined(); // A preProcesses property private, de cast-tal ellenorizheto const pre: any = (list as any).preProcesses; expect(pre).toBeDefined(); expect(pre.length).toBe(1); expect(pre[0]).toBe(myAuth); }); }); describe('| GET /list', (): void => { it('| 503 ha a service nincs installalva', async (): Promise => { const ctrl = DyNTS_FileLogs_Controller.getInstance(); ctrl.setupEndpoints(); const task = taskByName(ctrl, 'listFileLogs'); const res = mockRes(); await task(mockReq(), res); expect(res.statusCode).toBe(503); expect(res.payload.error).toContain('not installed'); }); it('| 200 + files lista ha installalva van', async (): Promise => { DyNTS_global_settings.log_settings.file_log = { enabled: true, logDir: tempDir, filenamePrefix: 'spec-' }; DyNTS_FileLog_Service.getInstance().install(); // Egy extra spec- fajlt kezzel fs.writeFileSync(path.join(tempDir, 'spec-extra-2026.log'), 'extra-content'); const ctrl = DyNTS_FileLogs_Controller.getInstance(); ctrl.setupEndpoints(); const task = taskByName(ctrl, 'listFileLogs'); const res = mockRes(); await task(mockReq(), res); expect(res.statusCode).toBe(200); expect(res.payload.count).toBeGreaterThanOrEqual(2); // session + extra expect(res.payload.totalSizeBytes).toBeGreaterThan(0); expect(Array.isArray(res.payload.files)).toBe(true); }); }); describe('| GET /file/:filename', (): void => { let testFile: string; beforeEach((): void => { DyNTS_global_settings.log_settings.file_log = { enabled: true, logDir: tempDir, filenamePrefix: 'spec-' }; DyNTS_FileLog_Service.getInstance().install(); testFile = `spec-read-${Date.now()}.log`; const lines: string[] = []; for (let i: number = 1; i <= 30; i++) { lines.push(`row-${i}`); } fs.writeFileSync(path.join(tempDir, testFile), lines.join('\n') + '\n'); }); it('| 503 ha a service nincs installalva', async (): Promise => { DyNTS_FileLog_Service.getInstance()._teardownForTesting(); const ctrl = DyNTS_FileLogs_Controller.getInstance(); ctrl.setupEndpoints(); const task = taskByName(ctrl, 'readFileLog'); const res = mockRes(); await task(mockReq({ params: { filename: testFile } }), res); expect(res.statusCode).toBe(503); }); it('| 200 + tail mod default', async (): Promise => { const ctrl = DyNTS_FileLogs_Controller.getInstance(); ctrl.setupEndpoints(); const task = taskByName(ctrl, 'readFileLog'); const res = mockRes(); await task(mockReq({ params: { filename: testFile }, query: { tail: '5' } }), res); expect(res.statusCode).toBe(200); expect(res.payload.mode).toBe('tail'); expect(res.payload.lines).toEqual(['row-26', 'row-27', 'row-28', 'row-29', 'row-30']); expect(res.payload.totalLines).toBe(30); }); it('| 200 + head mod', async (): Promise => { const ctrl = DyNTS_FileLogs_Controller.getInstance(); ctrl.setupEndpoints(); const task = taskByName(ctrl, 'readFileLog'); const res = mockRes(); await task(mockReq({ params: { filename: testFile }, query: { head: '3' } }), res); expect(res.statusCode).toBe(200); expect(res.payload.mode).toBe('head'); expect(res.payload.lines).toEqual(['row-1', 'row-2', 'row-3']); }); it('| 200 + range mod', async (): Promise => { const ctrl = DyNTS_FileLogs_Controller.getInstance(); ctrl.setupEndpoints(); const task = taskByName(ctrl, 'readFileLog'); const res = mockRes(); await task(mockReq({ params: { filename: testFile }, query: { rangeStart: '5', rangeEnd: '8' } }), res); expect(res.statusCode).toBe(200); expect(res.payload.mode).toBe('range'); expect(res.payload.lines).toEqual(['row-5', 'row-6', 'row-7', 'row-8']); }); it('| 400 invalid tail (negativ)', async (): Promise => { const ctrl = DyNTS_FileLogs_Controller.getInstance(); ctrl.setupEndpoints(); const task = taskByName(ctrl, 'readFileLog'); const res = mockRes(); await task(mockReq({ params: { filename: testFile }, query: { tail: '-5' } }), res); expect(res.statusCode).toBe(400); }); it('| 400 invalid range (end < start)', async (): Promise => { const ctrl = DyNTS_FileLogs_Controller.getInstance(); ctrl.setupEndpoints(); const task = taskByName(ctrl, 'readFileLog'); const res = mockRes(); await task(mockReq({ params: { filename: testFile }, query: { rangeStart: '10', rangeEnd: '5' } }), res); expect(res.statusCode).toBe(400); }); it('| 404 invalid filename (prefix nem matchel)', async (): Promise => { const ctrl = DyNTS_FileLogs_Controller.getInstance(); ctrl.setupEndpoints(); const task = taskByName(ctrl, 'readFileLog'); const res = mockRes(); await task(mockReq({ params: { filename: 'evil-2026.log' } }), res); expect(res.statusCode).toBe(404); expect(res.payload.error).toBe('invalid filename'); }); it('| 404 path traversal (`../`)', async (): Promise => { const ctrl = DyNTS_FileLogs_Controller.getInstance(); ctrl.setupEndpoints(); const task = taskByName(ctrl, 'readFileLog'); const res = mockRes(); await task(mockReq({ params: { filename: '../etc/passwd' } }), res); expect(res.statusCode).toBe(404); expect(res.payload.error).toBe('invalid filename'); }); it('| 404 file nem letezik (de valid prefix)', async (): Promise => { const ctrl = DyNTS_FileLogs_Controller.getInstance(); ctrl.setupEndpoints(); const task = taskByName(ctrl, 'readFileLog'); const res = mockRes(); await task(mockReq({ params: { filename: 'spec-nonexistent.log' } }), res); expect(res.statusCode).toBe(404); expect(res.payload.error).toBe('file not found'); }); }); });