import { ArgumentsHost } from '@nestjs/common' ; import { ApiErrorMessage , ApiErrorParams , ApiHttpResponseCode } from '@fc/app' ; import { ConfigService } from '@fc/config' ; import { Loggable , Trackable } from '@fc/exceptions' ; import { LoggerService } from '@fc/logger' ; import { TrackingService } from '@fc/tracking' ; import { TrackableEvent } from '../events' ; import { FcException } from '../exceptions' ; import { FcExceptionFilter } from './fc.exception-filter' ; jest . mock ( '@fc/exceptions/decorator/trackable.decorator' ) ; describe ( 'FcExceptionFilter' , ( ) => { let exceptionFilter ; const loggerServiceMock = { trace : jest . fn ( ) , debug : jest . fn ( ) , warn : jest . fn ( ) , setContext : jest . fn ( ) , } as unknown as LoggerService ; const trackingServiceMock = { track : jest . fn ( ) , } as unknown as TrackingService ; const resMock = { } ; resMock . render = jest . fn ( ) . mockReturnValue ( resMock ) ; resMock . json = jest . fn ( ) . mockReturnValue ( resMock ) ; resMock . status = jest . fn ( ) . mockReturnValue ( resMock ) ; const reqMock = { } ; const errorValueMock = { code : 'codeValueMock' , id : 'idValueMock' , message : 'messageValueMock' , } ; const argumentHostMock = { switchToHttp : ( ) => ( { getResponse : ( ) => resMock , getRequest : ( ) => reqMock , } ) , } as ArgumentsHost ; const configServiceMock = { get : jest . fn ( ) , } ; beforeEach ( ( ) => { jest . resetAllMocks ( ) ; jest . restoreAllMocks ( ) ; exceptionFilter = new FcExceptionFilter ( configServiceMock as unknown as ConfigService , loggerServiceMock , trackingServiceMock ) ; configServiceMock . get . mockReturnValue ( { apiOutputContentType : 'html' , } ) ; } ) ; describe ( 'getStackTraceArray()' , ( ) => { it ( 'should return stack trace as an array' , ( ) => { const stackMock = [ 'line1' , 'line2' , 'line3' ] ; const errorMock = { stack : stackMock . join ( '\n' ) , } ; const result = exceptionFilter [ 'getStackTraceArray' ] ( errorMock ) ; expect ( result ) . toEqual ( stackMock ) ; } ) ; it ( 'should return empty array if not stack is present in error object' , ( ) => { const errorMock = { } ; const result = exceptionFilter [ 'getStackTraceArray' ] ( errorMock ) ; expect ( result ) . toEqual ( [ ] ) ; } ) ; it ( 'should concat with original error stack if provided' , ( ) => { const stackMok = [ 'A1' , 'A2' ] ; const originalErrorStackMock = [ 'B1' , 'B2' ] ; const errorMock = { stack : stackMok . join ( '\n' ) , originalError : { stack : originalErrorStackMock . join ( '\n' ) , } , } ; const result = exceptionFilter [ 'getStackTraceArray' ] ( errorMock ) ; expect ( result ) . toEqual ( [ ... stackMok , ... originalErrorStackMock ] ) ; } ) ; it ( 'should work seamlessly if original original error is provided but does not have a stack' , ( ) => { const stackMok = [ 'A1' , 'A2' ] ; const errorMock = { stack : stackMok . join ( '\n' ) , originalError : { } , } ; const result = exceptionFilter [ 'getStackTraceArray' ] ( errorMock ) ; expect ( result ) . toEqual ( stackMok ) ; } ) ; } ) ; describe ( 'catch()' , ( ) => { const STUB_ERROR_SCOPE = 2 ; const STUB_ERROR_CODE = 3 ; it ( 'should log a warning by default' , ( ) => { const exception = new FcException ( 'message text' ) ; exception . scope = STUB_ERROR_SCOPE ; exception . code = STUB_ERROR_CODE ; exceptionFilter . catch ( exception , argumentHostMock ) ; expect ( loggerServiceMock . warn ) . toHaveBeenCalledWith ( expect . objectContaining ( { type : 'FcException' , code : 'Y020003' , message : 'message text' , } ) ) ; } ) ; it ( 'should concat stack trace from original error' , ( ) => { const exception = new FcException ( ) ; exception . scope = STUB_ERROR_SCOPE ; exception . code = STUB_ERROR_CODE ; exception . originalError = new Error ( 'foo bar' ) ; exceptionFilter . catch ( exception , argumentHostMock ) ; expect ( loggerServiceMock . warn ) . toHaveBeenCalledWith ( expect . objectContaining ( { type : 'FcException' , code : 'Y020003' , stackTrace : expect . any ( Array ) , } ) ) ; } ) ; it ( 'should render error template' , ( ) => { const exception = new FcException ( 'message text' ) ; exception . scope = STUB_ERROR_SCOPE ; exception . code = STUB_ERROR_CODE ; exceptionFilter . catch ( exception , argumentHostMock ) ; expect ( resMock . render ) . toHaveBeenCalledWith ( 'error' , expect . objectContaining ( { code : 'Y020003' , message : 'message text' , } ) ) ; } ) ; it ( 'should not log error' , ( ) => { @ Loggable ( false ) class ClassMock extends FcException { } const exception = new ClassMock ( 'message text' ) ; exception . scope = STUB_ERROR_SCOPE ; exception . code = STUB_ERROR_CODE ; exceptionFilter [ 'logException' ] = jest . fn ( ) ; exceptionFilter . catch ( exception , argumentHostMock ) ; expect ( exceptionFilter [ 'logException' ] ) . toHaveBeenCalledTimes ( 0 ) ; expect ( resMock . render ) . toHaveBeenCalled ( ) ; } ) ; it ( 'should log error' , ( ) => { const exception = new FcException ( 'message text' ) ; exception . scope = STUB_ERROR_SCOPE ; exception . code = STUB_ERROR_CODE ; exceptionFilter [ 'logException' ] = jest . fn ( ) ; exceptionFilter . catch ( exception , argumentHostMock ) ; expect ( exceptionFilter [ 'logException' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( resMock . render ) . toHaveBeenCalled ( ) ; } ) ; it ( 'should not render for redirections' , ( ) => { const exception = new FcException ( 'message text' ) ; exception . scope = STUB_ERROR_SCOPE ; exception . code = STUB_ERROR_CODE ; exception . redirect = true ; exceptionFilter . catch ( exception , argumentHostMock ) ; expect ( resMock . render ) . not . toHaveBeenCalled ( ) ; } ) ; it ( 'should not call tracking service if isTrackable is not defined' , ( ) => { const exception = new FcException ( 'message text' ) ; exception . scope = STUB_ERROR_SCOPE ; exception . code = STUB_ERROR_CODE ; exceptionFilter . catch ( exception , argumentHostMock ) ; expect ( trackingServiceMock . track ) . toHaveBeenCalledTimes ( 0 ) ; expect ( resMock . render ) . toHaveBeenCalled ( ) ; } ) ; it ( 'should not call tracking service if isTrackable is defined to false' , ( ) => { const exception = new FcException ( 'message text' ) ; exception . scope = STUB_ERROR_SCOPE ; exception . code = STUB_ERROR_CODE ; const spy = jest . spyOn ( Trackable , 'isTrackable' ) ; spy . mockImplementationOnce ( ( ) => false ) ; exceptionFilter . catch ( exception , argumentHostMock ) ; expect ( trackingServiceMock . track ) . toHaveBeenCalledTimes ( 0 ) ; expect ( resMock . render ) . toHaveBeenCalled ( ) ; spy . mockRestore ( ) ; } ) ; it ( 'should call tracking service if isTrackable is defined to true' , ( ) => { const exception = new FcException ( 'message text' ) ; exception . scope = STUB_ERROR_SCOPE ; exception . code = STUB_ERROR_CODE ; const spy = jest . spyOn ( Trackable , 'isTrackable' ) ; spy . mockImplementationOnce ( ( ) => true ) ; exceptionFilter . catch ( exception , argumentHostMock ) ; expect ( trackingServiceMock . track ) . toHaveBeenCalledTimes ( 1 ) ; expect ( trackingServiceMock . track ) . toHaveBeenCalledWith ( TrackableEvent , { req : expect . any ( Object ) , exception : expect . any ( FcException ) , } ) ; expect ( resMock . render ) . toHaveBeenCalled ( ) ; spy . mockRestore ( ) ; } ) ; } ) ; describe ( 'ArgumentHostAdapter()' , ( ) => { it ( 'should provide a fake argument adapter with switchToHttp method' , ( ) => { const ctx = { res : { } , req : { } } ; const result = FcExceptionFilter . ArgumentHostAdapter ( ctx ) ; expect ( result ) . toBeDefined ( ) ; expect ( typeof result . switchToHttp ) . toBe ( 'function' ) ; } ) ; it ( 'should provide a fake argument adapter with a getResponse method in response to switchToHttp' , ( ) => { const ctx = { res : { } } ; const adapter = FcExceptionFilter . ArgumentHostAdapter ( ctx ) ; const httpAdapter = adapter . switchToHttp ( ) ; const result = httpAdapter . getResponse ( ) ; expect ( result ) . toBeDefined ( ) ; expect ( result ) . toBe ( ctx . res ) ; } ) ; it ( 'should provide a fake argument adapter with a getRequest method in response to switchToHttp' , ( ) => { const ctx = { req : { } } ; const adapter = FcExceptionFilter . ArgumentHostAdapter ( ctx ) ; const httpAdapter = adapter . switchToHttp ( ) ; const result = httpAdapter . getRequest ( ) ; expect ( result ) . toBeDefined ( ) ; expect ( result ) . toBe ( ctx . req ) ; } ) ; } ) ; describe ( 'errorOutput()' , ( ) => { it ( 'should return an error in JSON if the `apiOutputContentType` value is set to `json`' , ( ) => { configServiceMock . get . mockReturnValue ( { apiOutputContentType : 'json' , } ) ; const httpErrorCodeValueMock = 500 ; const exceptionParam = { res : resMock , error : errorValueMock , httpResponseCode : httpErrorCodeValueMock , } ; exceptionFilter [ 'errorOutput' ] ( exceptionParam ) ; expect ( resMock . json ) . toHaveBeenCalledTimes ( 1 ) ; expect ( resMock . json ) . toHaveBeenCalledWith ( { code : 'codeValueMock' , id : 'idValueMock' , message : 'messageValueMock' , } ) ; } ) ; it ( 'should return an error through `res.render` if the `apiOutputContentType` value is set to `html`' , ( ) => { const httpErrorCodeValueMock = ApiHttpResponseCode . ERROR_CODE_NONE ; const exceptionParam = { res : resMock , error : errorValueMock , httpResponseCode : httpErrorCodeValueMock , } ; const errorValueReturnedMock = { code : 'codeValueMock' , id : 'idValueMock' , message : 'messageValueMock' , } ; exceptionFilter [ 'errorOutput' ] ( exceptionParam ) ; expect ( resMock . render ) . toHaveBeenCalledTimes ( 1 ) ; expect ( resMock . render ) . toHaveBeenCalledWith ( 'error' , expect . objectContaining ( errorValueReturnedMock ) ) ; } ) ; it ( 'should return a 500 error code if the status is set to `true`' , ( ) => { const httpErrorCodeValueMock = 500 ; const exceptionParam = { res : resMock , error : errorValueMock , httpResponseCode : httpErrorCodeValueMock , } ; exceptionFilter [ 'errorOutput' ] ( exceptionParam ) ; expect ( resMock . status ) . toHaveBeenCalledTimes ( 1 ) ; expect ( resMock . status ) . toHaveBeenCalledWith ( 500 ) ; } ) ; } ) ; } ) ;