import { DyNTS_Errors_Controller } from './errors.controller'; import { DyFM_Error, DyFM_Errors, DyFM_HttpCallType, DyFM_RelativeDate } from '@futdevpro/fsm-dynamo'; import { DyNTS_Errors_ControlService } from './errors.control-service'; import { DyNTS_Endpoint_Params } from '../../../_models/control-models/endpoint-params.control-model'; class TestError extends DyFM_Error { testProperty?: string; } class TestErrors extends DyFM_Errors { testProperty?: string; } class TestErrorsControlService extends DyNTS_Errors_ControlService { async recordError(data: TestErrors, issuer: string, alwaysRecord?: boolean): Promise {} async handleInternalError(error: any, issuer: string, alwaysRecord?: boolean): Promise {} checkErrorIsStringifyableOrResolvable(error: TestError, issuer: string): TestError | 'UNRESOLVABLE' { return error; } getPriorityMultiplierByLevel(level: any, issuer: string): number { return 0; } async getErrorsFromDate(date: Date, issuer: string): Promise { return { results: [] }; } async deleteError(errorId: string, issuer: string, alwaysDelete?: boolean): Promise {} async deleteAllErrors(issuer: string, alwaysDelete?: boolean): Promise {} async recordFixAttempt(errorId: string, version: string, hypothesis: string, issuer: string): Promise { return { at: new Date(), by: issuer, version, hypothesis }; } async getErrorsByCategoryPaged(category: string, range: any, pageSize: number, pageIndex: number, issuer: string): Promise { return { items: [], total: 0, pageIndex, pageSize }; } async resolveError(errorId: string, issuer: string, notes?: string): Promise { return; } async getErrorsByStatusPaged(status: string, range: any, pageSize: number, pageIndex: number, issuer: string): Promise { return { items: [], total: 0, pageIndex, pageSize }; } async getErrorsInRange(range: DyFM_RelativeDate, issuer: string): Promise { return { results: [] }; } async getErrorsPaged(range: DyFM_RelativeDate, pageSize: number, pageIndex: number, issuer: string): Promise { return { results: [] }; } async getLastErrors(range: DyFM_RelativeDate, pageSize: number, pageIndex: number, issuer: string): Promise { return { results: [] }; } async searchErrors(searchQuery: any, issuer: string): Promise { return { results: [] }; } } class TestErrorsController extends DyNTS_Errors_Controller { protected getError_ControlService(set: { data?: TestError, issuer: string }): TestErrorsControlService { return new TestErrorsControlService(); } protected override readonly additionalEndpoints: DyNTS_Endpoint_Params[] = []; } describe('| DyNTS_Errors_Controller', () => { let controller: TestErrorsController; beforeEach(() => { controller = new (TestErrorsController as any)(); }); describe('| setupEndpoints', () => { it('| should setup all error endpoints', () => { controller.setupEndpoints(); expect(controller.endpoints).toBeDefined(); expect(controller.endpoints.length).toBeGreaterThanOrEqual(6); }); it('| should setup logError endpoint', () => { controller.setupEndpoints(); const logErrorEndpoint = controller.endpoints.find(ep => ep.name === 'logError'); expect(logErrorEndpoint).toBeDefined(); expect(logErrorEndpoint?.type).toBe(DyFM_HttpCallType.post); expect(logErrorEndpoint?.endpoint).toBe('/error/log'); expect((logErrorEndpoint as any)?.tasks).toBeDefined(); expect((logErrorEndpoint as any)?.tasks?.length).toBe(1); }); it('| should setup newError endpoint', () => { controller.setupEndpoints(); const newErrorEndpoint = controller.endpoints.find(ep => ep.name === 'newError'); expect(newErrorEndpoint).toBeDefined(); expect(newErrorEndpoint?.type).toBe(DyFM_HttpCallType.post); expect(newErrorEndpoint?.endpoint).toBe('/error/new'); expect((newErrorEndpoint as any)?.tasks).toBeDefined(); expect((newErrorEndpoint as any)?.tasks?.length).toBe(1); }); it('| should setup markErrorDone endpoint', () => { controller.setupEndpoints(); const markErrorDoneEndpoint = controller.endpoints.find(ep => ep.name === 'markErrorDone'); expect(markErrorDoneEndpoint).toBeDefined(); expect(markErrorDoneEndpoint?.type).toBe(DyFM_HttpCallType.get); expect(markErrorDoneEndpoint?.endpoint).toBe('/error/mark-done/:errorId'); expect((markErrorDoneEndpoint as any)?.tasks).toBeDefined(); expect((markErrorDoneEndpoint as any)?.tasks?.length).toBe(1); }); it('| should setup markAllErrorDone endpoint', () => { controller.setupEndpoints(); const markAllErrorDoneEndpoint = controller.endpoints.find(ep => ep.name === 'markAllErrorDone'); expect(markAllErrorDoneEndpoint).toBeDefined(); expect(markAllErrorDoneEndpoint?.type).toBe(DyFM_HttpCallType.get); expect(markAllErrorDoneEndpoint?.endpoint).toBe('/error/mark-all-done'); expect((markAllErrorDoneEndpoint as any)?.tasks).toBeDefined(); expect((markAllErrorDoneEndpoint as any)?.tasks?.length).toBe(1); }); it('| should setup getErrors endpoint', () => { controller.setupEndpoints(); const getErrorsEndpoint = controller.endpoints.find(ep => ep.name === 'getErrors'); expect(getErrorsEndpoint).toBeDefined(); expect(getErrorsEndpoint?.type).toBe(DyFM_HttpCallType.get); expect(getErrorsEndpoint?.endpoint).toBe('/error/get-range/:range'); expect((getErrorsEndpoint as any)?.tasks).toBeDefined(); expect((getErrorsEndpoint as any)?.tasks?.length).toBe(1); }); it('| should setup getErrorsPaged endpoint', () => { controller.setupEndpoints(); const getErrorsPagedEndpoint = controller.endpoints.find(ep => ep.name === 'getErrorsPaged'); expect(getErrorsPagedEndpoint).toBeDefined(); expect(getErrorsPagedEndpoint?.type).toBe(DyFM_HttpCallType.get); expect(getErrorsPagedEndpoint?.endpoint).toBe('/error/get-paged/:range/:pageSize/:pageIndex'); expect((getErrorsPagedEndpoint as any)?.tasks).toBeDefined(); expect((getErrorsPagedEndpoint as any)?.tasks?.length).toBe(1); }); it('| should setup getLastErrors endpoint', () => { controller.setupEndpoints(); const getLastErrorsEndpoint = controller.endpoints.find(ep => ep.name === 'getLastErrors'); expect(getLastErrorsEndpoint).toBeDefined(); expect(getLastErrorsEndpoint?.type).toBe(DyFM_HttpCallType.get); expect(getLastErrorsEndpoint?.endpoint).toBe('/error/get-last-paged/:range/:pageSize/:pageIndex'); expect((getLastErrorsEndpoint as any)?.tasks).toBeDefined(); expect((getLastErrorsEndpoint as any)?.tasks?.length).toBe(1); }); it('| should setup searchErrors endpoint', () => { controller.setupEndpoints(); const searchErrorsEndpoint = controller.endpoints.find(ep => ep.name === 'searchErrors'); expect(searchErrorsEndpoint).toBeDefined(); expect(searchErrorsEndpoint?.type).toBe(DyFM_HttpCallType.get); expect(searchErrorsEndpoint?.endpoint).toBe('/error/search'); expect((searchErrorsEndpoint as any)?.tasks).toBeDefined(); expect((searchErrorsEndpoint as any)?.tasks?.length).toBe(1); }); it('| should include additionalEndpoints', () => { const additionalEndpoint = new DyNTS_Endpoint_Params({ name: 'customEndpoint', type: DyFM_HttpCallType.get, endpoint: '/error/custom', tasks: [], }); (controller as any).additionalEndpoints.push(additionalEndpoint); controller.setupEndpoints(); const customEndpoint = controller.endpoints.find(ep => ep.name === 'customEndpoint'); expect(customEndpoint).toBeDefined(); }); it('| should throw error when additionalEndpoints is not defined', () => { const controllerWithoutEndpoints = new (class extends TestErrorsController { protected override readonly additionalEndpoints: DyNTS_Endpoint_Params[] = undefined as any; } as any)(); expect(() => { controllerWithoutEndpoints.setupEndpoints(); }).toThrow(); }); }); describe('| opt-in admin auth retrofit (BL-20260420-004)', (): void => { afterEach((): void => { DyNTS_Errors_Controller._resetAuthConfigForTesting(); }); // FR-263 — a guard mostantol REQUEST-IDOBEN fut (lazy wrapper); igy MINDEN endpoint-on pontosan // 1 preProcess (a wrapper) van, a tenyleges guard-logikat a wrapper MEGHIVASAVAL teszteljuk. const lazyWrapperOf = (c: any, name: string): ((req: any, res: any) => Promise) | undefined => { const ep: any = c.endpoints.find((e: any) => e.name === name); return ep?.preProcesses?.[0]; }; const mockReqRes = (): { req: any; res: any } => ({ req: {} as any, res: { headersSent: false } as any }); it('| default (no config): a lazy wrapper jelen van, de egy keres NEM hiv guardot (nyitva marad)', async (): Promise => { controller.setupEndpoints(); for (const name of ['logError', 'getErrors', 'searchErrors']) { const w = lazyWrapperOf(controller, name); expect(w).withContext(`${name} wrapper`).toBeDefined(); const { req, res } = mockReqRes(); await w!(req, res); // nincs config → no-op, nem dob, nem blokkol } }); it('| configure({ authPreProcess }) → egy keres MINDEN built-in endpoint-on meghivja a guardot', async (): Promise => { let calls: number = 0; DyNTS_Errors_Controller.configure({ authPreProcess: async (): Promise => { calls++; } }); const c = new (TestErrorsController as any)(); c.setupEndpoints(); const builtIn: string[] = [ 'logError', 'newError', 'markErrorDone', 'markAllErrorDone', 'getErrors', 'getErrorsPaged', 'getLastErrors', 'searchErrors', ]; for (const name of builtIn) { const w = lazyWrapperOf(c, name); expect(w).withContext(`endpoint ${name} missing`).toBeDefined(); const { req, res } = mockReqRes(); await w!(req, res); } expect(calls).toBe(builtIn.length); }); it('| configure({ protectedEndpoints: subset }) → a guard CSAK a listazott endpoint-okon fut', async (): Promise => { let calls: number = 0; DyNTS_Errors_Controller.configure({ authPreProcess: async (): Promise => { calls++; }, protectedEndpoints: ['markAllErrorDone', 'logError'], }); const c = new (TestErrorsController as any)(); c.setupEndpoints(); for (const name of ['markAllErrorDone', 'logError']) { const { req, res } = mockReqRes(); await lazyWrapperOf(c, name)!(req, res); } expect(calls).withContext('protected → guard runs').toBe(2); calls = 0; const { req, res } = mockReqRes(); await lazyWrapperOf(c, 'getErrors')!(req, res); expect(calls).withContext('non-protected getErrors → guard does NOT run').toBe(0); }); it('| FR-263 ORDER-INDEPENDENCE: setupEndpoints() ELOTT epitett endpoint is enforce-olja a guardot (a fix lenyege)', async (): Promise => { // A framework korai singleton-konstrukcioja: az endpoints config NELKUL epul fel... const c = new (TestErrorsController as any)(); c.setupEndpoints(); // ...es a configure() KESOBB fut (ez volt a fals-200 info-disclosure bug). A lazy wrapper // request-idoben olvassa a configot → a guard MEGIS lefut. Ez az FR-263 regresszio-guard. let calls: number = 0; DyNTS_Errors_Controller.configure({ authPreProcess: async (): Promise => { calls++; } }); const { req, res } = mockReqRes(); await lazyWrapperOf(c, 'getErrors')!(req, res); expect(calls).withContext('guard must run despite configure() AFTER setupEndpoints').toBe(1); }); it('| getAuthConfig() returns the active config', (): void => { const fakeAuth = async (): Promise => { /* noop */ }; DyNTS_Errors_Controller.configure({ authPreProcess: fakeAuth, protectedEndpoints: ['logError'] }); const cfg = DyNTS_Errors_Controller.getAuthConfig(); expect(cfg.authPreProcess).toBe(fakeAuth); expect(cfg.protectedEndpoints).toEqual(['logError']); }); it('| _resetAuthConfigForTesting() clears the config', (): void => { DyNTS_Errors_Controller.configure({ authPreProcess: async (): Promise => { /* noop */ } }); DyNTS_Errors_Controller._resetAuthConfigForTesting(); const cfg = DyNTS_Errors_Controller.getAuthConfig(); expect(cfg.authPreProcess).toBeUndefined(); }); }); });