import { Request, Response } from 'express'; import { DyFM_HttpCallType, DyFM_Log, DyFM_Error, DyFM_EnvironmentFlag } from '@futdevpro/fsm-dynamo'; import { DyNTS_Endpoint_Params } from './endpoint-params.control-model'; import { DyNTS_RouteSecurity } from '../../_enums/route-security.enum'; import { DyNTS_global_settings } from '../../_collections/global-settings.const'; import { DyNTS_GlobalService } from '../../_services/core/global.service'; // Skipped: DyFM_Error stack trace causes Jasmine 'split' errors describe('| DyNTS_Endpoint_Params', () => { // Ensure env_settings and log_settings exist before tests beforeAll(() => { if (!DyNTS_global_settings.env_settings) { (DyNTS_global_settings as any).env_settings = { environment: DyFM_EnvironmentFlag.local, discord: {}, mongoUri: undefined, openAi: {}, }; } else if (!DyNTS_global_settings.env_settings.environment) { DyNTS_global_settings.env_settings.environment = DyFM_EnvironmentFlag.local; } // Initialize log_settings for error handling tests if (!DyNTS_global_settings.log_settings) { (DyNTS_global_settings as any).log_settings = { highDetailedLogs: false, api_errors: true, requestStackLocation: false, }; } // Initialize compactErrorResponse if (DyNTS_global_settings.compactErrorResponse === undefined) { (DyNTS_global_settings as any).compactErrorResponse = false; } }); describe('| constructor', () => { it('| should create an instance with required parameters', () => { const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: tasks, }); expect(params.name).toBe('testEndpoint'); expect(params.type).toBe(DyFM_HttpCallType.get); expect(params.endpoint).toBe('/test'); expect((params as any).tasks).toBe(tasks); }); it('| should set default security to open', () => { const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: tasks, }); expect(params.security).toBe(DyNTS_RouteSecurity.open); }); it('| should use provided security', () => { const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', security: DyNTS_RouteSecurity.secure, tasks: tasks, }); expect(params.security).toBe(DyNTS_RouteSecurity.secure); }); it('| should extract path parameters from endpoint', () => { const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test/:userId/:itemId', tasks: tasks, }); expect(params['pathParams']).toEqual(['userId', 'itemId']); }); it('| should handle endpoint without path parameters', () => { const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: tasks, }); expect(params['pathParams']).toEqual([]); }); it('| should set default logRequest to false', () => { const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: tasks, }); expect(params['logRequest']).toBe(DyNTS_global_settings.log_settings.request); }); it('| should use provided logRequest value', () => { const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: tasks, logRequest: true, }); expect(params['logRequest']).toBe(true); }); it('| should set default autoResolveCirculation to false', () => { const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: tasks, }); expect(params['autoResolveCirculation']).toBe(DyNTS_global_settings.autoResolveEndpointCirculationErrors); }); it('| should set stackLocation', () => { const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: tasks, }); expect(params.stackLocation).toBeDefined(); expect(typeof params.stackLocation).toBe('string'); }); it('| should handle preProcesses', () => { const preProcess = async (req: Request, res: Response): Promise => { // Pre-process logic }; const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', preProcesses: [preProcess], tasks: tasks, }); expect(params['preProcesses']).toEqual([preProcess]); }); it('| should throw error when endpoint is not provided', () => { const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; expect(() => { new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '', tasks: tasks, }); }).toThrow(); }); it('| should set default log settings from global settings', () => { const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: tasks, }); expect(params['logRequestsParams']).toBe(DyNTS_global_settings.log_settings.requestsParams); expect(params['logRequestsContent']).toBe(DyNTS_global_settings.log_settings.requestsContent); expect(params['logResponseContent']).toBe(DyNTS_global_settings.log_settings.responseContent); }); it('| should use provided log settings', () => { const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: tasks, logRequestsParams: true, logRequestsContent: true, logResponseContent: true, }); expect(params['logRequestsParams']).toBe(true); expect(params['logRequestsContent']).toBe(true); expect(params['logResponseContent']).toBe(true); }); it('| should use endpoint as name when name is not provided', () => { const tasks = [ async (req: Request, res: Response, issuer: string): Promise => { res.send({ success: true }); }, ]; const params = new DyNTS_Endpoint_Params({ name: '/test-endpoint', type: DyFM_HttpCallType.get, endpoint: '/test-endpoint', tasks: tasks, }); expect(params.name).toBe('/test-endpoint'); }); }); describe('| getFullExecution', () => { let mockRequest: Partial; let mockResponse: Partial; let sendSpy: jasmine.Spy; let statusSpy: jasmine.Spy; beforeEach(() => { mockRequest = { params: {}, body: {}, originalUrl: '/test', }; sendSpy = jasmine.createSpy('send'); statusSpy = jasmine.createSpy('status').and.returnValue({ send: sendSpy }); mockResponse = { send: sendSpy, status: statusSpy, }; spyOn(DyNTS_GlobalService, 'getAuthService').and.returnValue({ getIssuerFromRequest: () => 'test-issuer', } as any); }); it('| should execute tasks successfully', async () => { const taskSpy = jasmine.createSpy('task').and.callFake(async (req: Request, res: Response, issuer: string) => { res.send({ success: true }); }); const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: [taskSpy], logRequest: false, }); const execution = params.getFullExecution(); await execution(mockRequest as Request, mockResponse as Response); expect(taskSpy).toHaveBeenCalledWith(mockRequest, mockResponse, 'test-issuer'); expect(sendSpy).toHaveBeenCalled(); }); it('| should execute preProcesses before tasks', async () => { const preProcessSpy = jasmine.createSpy('preProcess').and.returnValue(Promise.resolve()); const taskSpy = jasmine.createSpy('task').and.returnValue(Promise.resolve()); const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', preProcesses: [preProcessSpy], tasks: [taskSpy], logRequest: false, }); const execution = params.getFullExecution(); await execution(mockRequest as Request, mockResponse as Response); expect(preProcessSpy).toHaveBeenCalledBefore(taskSpy as any); expect(preProcessSpy).toHaveBeenCalledWith(mockRequest, mockResponse); }); it('| should handle errors and call error handler', async () => { const error = new Error('Test error'); const taskSpy = jasmine.createSpy('task').and.returnValue(Promise.reject(error)); const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: [taskSpy], logRequest: false, }); const globalErrorHandlerSpy = jasmine.createSpy('globalErrorHandler').and.returnValue(Promise.resolve()); (DyNTS_GlobalService as any).globalErrorHandler = globalErrorHandlerSpy; const execution = params.getFullExecution(); await execution(mockRequest as Request, mockResponse as Response); // Wait for async error handling to complete (error handler is not awaited in source) await new Promise(resolve => setTimeout(resolve, 50)); expect(globalErrorHandlerSpy).toHaveBeenCalled(); expect(statusSpy).toHaveBeenCalled(); expect(statusSpy.calls.mostRecent().args[0]).toBe(500); expect(sendSpy).toHaveBeenCalled(); }); it('| should log request when logRequest is true', async () => { const logInfoSpy = spyOn(DyFM_Log, 'info'); const taskSpy = jasmine.createSpy('task').and.returnValue(Promise.resolve()); const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: [taskSpy], logRequest: true, logRequestsParams: false, logRequestsContent: false, }); const execution = params.getFullExecution(); await execution(mockRequest as Request, mockResponse as Response); expect(logInfoSpy).toHaveBeenCalled(); }); it('| should log path params when logRequestsParams is true', async () => { const logInfoSpy = spyOn(DyFM_Log, 'info'); mockRequest.params = { userId: '123', itemId: '456' }; const taskSpy = jasmine.createSpy('task').and.returnValue(Promise.resolve()); const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test/:userId/:itemId', tasks: [taskSpy], logRequest: true, logRequestsParams: true, logRequestsContent: false, }); const execution = params.getFullExecution(); await execution(mockRequest as Request, mockResponse as Response); expect(logInfoSpy).toHaveBeenCalled(); }); it('| should log request body when logRequestsContent is true', async () => { const logInfoSpy = spyOn(DyFM_Log, 'info'); mockRequest.body = { key: 'value' }; const taskSpy = jasmine.createSpy('task').and.returnValue(Promise.resolve()); const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: [taskSpy], logRequest: true, logRequestsParams: false, logRequestsContent: true, }); const execution = params.getFullExecution(); await execution(mockRequest as Request, mockResponse as Response); expect(logInfoSpy).toHaveBeenCalled(); }); it('| should handle issuer retrieval error', async () => { (DyNTS_GlobalService.getAuthService as jasmine.Spy).and.returnValue({ getIssuerFromRequest: () => { throw new Error('Auth error'); }, } as any); const logWarnSpy = spyOn(DyFM_Log, 'warn'); const taskSpy = jasmine.createSpy('task').and.callFake(async (req: Request, res: Response, issuer: string) => { res.send({ success: true }); }); const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: [taskSpy], logRequest: false, }); const execution = params.getFullExecution(); await execution(mockRequest as Request, mockResponse as Response); expect(logWarnSpy).toHaveBeenCalled(); expect(taskSpy).toHaveBeenCalledWith(mockRequest, mockResponse, 'unknown-issuer'); }); it('| should handle DyFM_Error with status code', async () => { const error = new DyFM_Error({ error: new Error('Test error'), }); (error as any).___status = 404; const taskSpy = jasmine.createSpy('task').and.returnValue(Promise.reject(error)); const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: [taskSpy], logRequest: false, }); const globalErrorHandlerSpy = jasmine.createSpy('globalErrorHandler').and.returnValue(Promise.resolve()); (DyNTS_GlobalService as any).globalErrorHandler = globalErrorHandlerSpy; const execution = params.getFullExecution(); await execution(mockRequest as Request, mockResponse as Response); // Wait for async error handling to complete await new Promise(resolve => setTimeout(resolve, 50)); expect(statusSpy).toHaveBeenCalled(); expect(statusSpy.calls.mostRecent().args[0]).toBe(404); expect(sendSpy).toHaveBeenCalled(); }); it('| should use default status 500 when error has no status', async () => { const error = new Error('Test error'); const taskSpy = jasmine.createSpy('task').and.returnValue(Promise.reject(error)); const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: [taskSpy], logRequest: false, }); const globalErrorHandlerSpy = jasmine.createSpy('globalErrorHandler').and.returnValue(Promise.resolve()); (DyNTS_GlobalService as any).globalErrorHandler = globalErrorHandlerSpy; const execution = params.getFullExecution(); await execution(mockRequest as Request, mockResponse as Response); // Wait for async error handling to complete await new Promise(resolve => setTimeout(resolve, 50)); expect(statusSpy).toHaveBeenCalledWith(500); expect(sendSpy).toHaveBeenCalled(); }); // FR-067 (2026-06-06): non-DyFM_Error wrap — minden plain Error a kliens // fele DyFM_Error-shape-ben mehessen, errorCode-szal, userMessage-zsel. it('| FR-067: should wrap plain Error into DyFM_Error with FDP errorCode + userMessage', async () => { const originalShortCode = DyNTS_global_settings.systemShortCodeName; DyNTS_global_settings.systemShortCodeName = 'TST'; const error = new Error('Raw error message'); const taskSpy = jasmine.createSpy('task').and.returnValue(Promise.reject(error)); const params = new DyNTS_Endpoint_Params({ name: 'myEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: [taskSpy], logRequest: false, }); (DyNTS_GlobalService as any).globalErrorHandler = jasmine.createSpy('globalErrorHandler').and.returnValue(Promise.resolve()); const execution = params.getFullExecution(); await execution(mockRequest as Request, mockResponse as Response); await new Promise(resolve => setTimeout(resolve, 50)); expect(sendSpy).toHaveBeenCalled(); const sent = sendSpy.calls.mostRecent().args[0] as DyFM_Error; expect(sent._errorCode).toBe('TST|DyNTS-EPP-myEndpoint'); expect(sent.__userMessage).toBeDefined(); expect(statusSpy).toHaveBeenCalledWith(500); DyNTS_global_settings.systemShortCodeName = originalShortCode; }); // FR-067: axios-style HTTP errors (with `status` field) — wrap-status onorzes. it('| FR-067: should preserve downstream HTTP status when wrapping axios-style error', async () => { const error = Object.assign(new Error('Upstream 502'), { status: 502 }); const taskSpy = jasmine.createSpy('task').and.returnValue(Promise.reject(error)); const params = new DyNTS_Endpoint_Params({ name: 'proxyEndpoint', type: DyFM_HttpCallType.get, endpoint: '/proxy', tasks: [taskSpy], logRequest: false, }); (DyNTS_GlobalService as any).globalErrorHandler = jasmine.createSpy('globalErrorHandler').and.returnValue(Promise.resolve()); const execution = params.getFullExecution(); await execution(mockRequest as Request, mockResponse as Response); await new Promise(resolve => setTimeout(resolve, 50)); expect(statusSpy).toHaveBeenCalledWith(502); }); it('| should handle production environment error response', async () => { const originalEnv = DyNTS_global_settings.env_settings.environment; DyNTS_global_settings.env_settings.environment = DyFM_EnvironmentFlag.prod; const error = new DyFM_Error({ error: new Error('Test error'), }); (error as any).___status = 500; error.confidentialContent = { secret: 'value' }; error.additionalContent = { debug: 'info' }; const taskSpy = jasmine.createSpy('task').and.returnValue(Promise.reject(error)); const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: [taskSpy], logRequest: false, }); const globalErrorHandlerSpy = jasmine.createSpy('globalErrorHandler').and.returnValue(Promise.resolve()); (DyNTS_GlobalService as any).globalErrorHandler = globalErrorHandlerSpy; const execution = params.getFullExecution(); await execution(mockRequest as Request, mockResponse as Response); // Wait for async error handling to complete await new Promise(resolve => setTimeout(resolve, 50)); expect(statusSpy).toHaveBeenCalled(); const statusCall = statusSpy.calls.mostRecent(); expect(statusCall.args[0]).toBe(500); expect(sendSpy).toHaveBeenCalled(); const sentError = sendSpy.calls.mostRecent().args[0] as DyFM_Error; expect(sentError.confidentialContent).toBeUndefined(); DyNTS_global_settings.env_settings.environment = originalEnv; }); it('| should handle autoResolveCirculation for non-serializable errors', async () => { const circularError: any = { message: 'Circular error' }; circularError.self = circularError; const taskSpy = jasmine.createSpy('task').and.returnValue(Promise.reject(circularError)); const params = new DyNTS_Endpoint_Params({ name: 'testEndpoint', type: DyFM_HttpCallType.get, endpoint: '/test', tasks: [taskSpy], logRequest: false, autoResolveCirculation: true, }); const globalErrorHandlerSpy = jasmine.createSpy('globalErrorHandler').and.returnValue(Promise.resolve()); (DyNTS_GlobalService as any).globalErrorHandler = globalErrorHandlerSpy; const execution = params.getFullExecution(); await execution(mockRequest as Request, mockResponse as Response); // Wait for async error handling to complete await new Promise(resolve => setTimeout(resolve, 50)); expect(statusSpy).toHaveBeenCalled(); expect(statusSpy.calls.mostRecent().args[0]).toBe(500); expect(sendSpy).toHaveBeenCalled(); }); it('| FR-263 security: skips tasks when a preProcess already sent a response (res.headersSent)', async () => { const taskSpy = jasmine.createSpy('task').and.returnValue(Promise.resolve()); const respondingPreProcess = jasmine.createSpy('preProcess').and.callFake( async (_req: Request, res: Response): Promise => { (res as any).headersSent = true; // a guard MÁR válaszolt (pl. 401) } ); const params = new DyNTS_Endpoint_Params({ name: 'guardedEndpoint', type: DyFM_HttpCallType.get, endpoint: '/guarded', preProcesses: [respondingPreProcess], tasks: [taskSpy], logRequest: false, }); (mockResponse as any).headersSent = false; await params.getFullExecution()(mockRequest as Request, mockResponse as Response); expect(respondingPreProcess).toHaveBeenCalled(); // FR-263 defense-in-depth: a preProcess-válasz (headersSent) UTÁN a task-ok NEM futhatnak expect(taskSpy).not.toHaveBeenCalled(); }); it('| FR-263 security: runs tasks normally when no preProcess responded (headersSent false)', async () => { const taskSpy = jasmine.createSpy('task').and.returnValue(Promise.resolve()); const params = new DyNTS_Endpoint_Params({ name: 'openEndpoint', type: DyFM_HttpCallType.get, endpoint: '/open', preProcesses: [], tasks: [taskSpy], logRequest: false, }); (mockResponse as any).headersSent = false; await params.getFullExecution()(mockRequest as Request, mockResponse as Response); // normál endpoint (nem-válaszoló preProcess) — a task-ok LEFUTNAK (nincs regresszió) expect(taskSpy).toHaveBeenCalled(); }); }); });