import shallowEqual from '@tinkoff/utils/is/shallowEqual' ; import omit from '@tinkoff/utils/object/omit' ; import type { IncomingMessage , ServerResponse } from 'http' ; import { createServer } from 'http' ; import querystring from 'querystring' ; import type { Request , IRouterMatcher , Express , RequestHandler } from 'express' ; import express , { Router } from 'express' ; import bodyParser from 'body-parser' ; import { createProxyMiddleware } from 'http-proxy-middleware' ; import type { Mock , MockConfig , MockerOptions , Logger , ApiConfig } from './mocker.h' ; import type { MockRepository } from './repositories/repository.h' ; export class Mocker { private appRoutePrefix : string ; private apiRoutePrefix : string ; private passUnhandledRequests : boolean ; private repositories : MockRepository [ ] ; private app : Express ; private api : Express ; private apis : Record < string , ApiConfig > ; private apisRouter : Router ; private mocksRouters : Record < string , Router > ; private mocks : Record < string , Record < string , Mock > > ; private logger : Logger ; constructor ( { appRoutePrefix , apiRoutePrefix , apis , passUnhandledRequests , repositories , logger , } ) { this . logger = logger ; this . repositories = repositories ; this . apis = apis ; this . appRoutePrefix = appRoutePrefix ?? '' ; this . apiRoutePrefix = apiRoutePrefix ?? '' ; this . passUnhandledRequests = passUnhandledRequests ?? false ; } async init ( ) { this . app = createApp ( ) ; this . api = createApp ( ) ; await this . update ( ) ; this . applyAppRouter ( ) ; this . applyApiRouter ( ) ; this . app . use ( ( err , req , res , next ) => { console . error ( err . stack ) ; res . status ( 500 ) . send ( 'Something broke!' ) ; } ) ; } async update ( ) { this . mocks = await this . resolveMocks ( ) ; this . updateRouters ( ) ; } setApis ( apis ) { this . apis = apis ; } addMocks ( api , mocks ) { this . mocks [ api ] = { ... this . mocks [ api ] , ... mocks , } ; this . updateRouters ( ) ; } removeMocks ( api , mocks ) { this . mocks [ api ] = omit ( mocks , this . mocks [ api ] ) ; this . updateRouters ( ) ; } use ( req , res ) { return this . app ( req , res ) ; } useApi ( req , res ) { return this . api ( req , res ) ; } async listen ( port ) { return new Promise < void > ( ( resolve , reject ) => { const server = createServer ( this . app ) ; server . on ( 'error' , reject ) ; server . listen ( port , resolve ) ; } ) ; } private updateRouters ( ) { this . apisRouter = createRouter ( ) ; this . mocksRouters = this . createMocksRouters ( ) ; this . applyMocksHandlers ( ) ; this . applyApisHandlers ( ) ; } private createMocksRouters ( ) { const routers = { } ; Object . keys ( this . apis ) . forEach ( ( api ) => { routers [ api ] = createRouter ( ) ; } ) ; return routers ; } private async resolveMocks ( ) { const mocks = { } ; const repoMocks = await Promise . all ( this . repositories . map ( ( repo ) => repo . getAll ( ) ) ) ; for ( const mock of repoMocks ) { for ( const api in mock ) { if ( api in this . apis ) { mocks [ api ] = { ... mocks [ api ] , ... mock [ api ] , } ; } } } return mocks ; } private applyMocksHandlers ( ) { Object . keys ( this . apis ) . forEach ( ( api ) => { const router = this . mocksRouters [ api ] ; const mocks = this . mocks [ api ] ?? { } ; const { target } = this . apis [ api ] ; const proxy = createProxy ( { target } ) ; Object . keys ( mocks ) . forEach ( ( key ) => { const mock = mocks [ key ] ; const [ method , url ] = key . split ( ' ' ) ; const [ path , query ] = url . split ( '?' ) ; const adoptedMethod = method . toLowerCase ( ) ; const addRequestHandler = router [ adoptedMethod ] ; if ( isRouterMethod ( addRequestHandler ) ) { router [ adoptedMethod ] ( path , ( req , res , next ) => { if ( typeof mock === 'function' ) { mock ( req , res ) ; } else { let parsedQuery ; if ( query ) { parsedQuery = parseQuery ( query ) ; } const mockHandler = this . convertMockConfigToHandler ( mock , proxy , parsedQuery ) ; mockHandler ( req , res , next ) ; } } ) ; } } ) ; if ( this . passUnhandledRequests ) { router . use ( '/' , ( req , res , next ) => { this . logger . debug ( `proxy ${ req . url }` ) ; proxy ( req , res , next ) ; } ) ; } else { router . use ( '/' , ( req , res ) => { res . status ( 500 ) . json ( `handle request to "${ req . url }" - this mock url is not registered` ) ; } ) ; } } ) ; } private applyApisHandlers ( ) { Object . keys ( this . apis ) . forEach ( ( api ) => { const router = this . mocksRouters [ api ] ; this . apisRouter . use ( `${ this . appRoutePrefix }/${ api }` , router ) ; } ) ; this . apisRouter . use ( '/' , ( req , res ) => { res . status ( 500 ) . json ( `handle request to "${ req . url }" - this api url is not registered` ) ; } ) ; } private applyAppRouter ( ) { this . app . use ( ( ... args ) => this . apisRouter ( ... args ) ) ; } private applyApiRouter ( ) { this . api . post ( `${ this . apiRoutePrefix }/mocks` , ( req , res ) => { const { api , mocks } = req . body ; this . addMocks ( api , mocks ) ; res . end ( ) ; } ) ; this . api . delete ( `${ this . apiRoutePrefix }/mocks` , ( req , res ) => { const { api , mocks } = req . body ; this . removeMocks ( api , mocks ) ; res . end ( ) ; } ) ; } private convertMockConfigToHandler ( mockConfig , proxy , query ? ) { const { payload , status , headers = { } , pass } = mockConfig ; return ( req , res , next ) => { if ( pass ) { proxy ( req , res , next ) ; return ; } if ( query && Object . keys ( query ) . length > 0 ) { if ( ! isQueriesEqual ( query , req ) ) { if ( this . passUnhandledRequests ) { proxy ( req , res , next ) ; } else { res . status ( 500 ) . json ( `request to "${ req . url }" have mock, but queries do not match, mock query is ${ JSON . stringify ( query ) }, and request query is ${ JSON . stringify ( req . query ) }` ) ; } return ; } } if ( status ) { res . status ( status ) ; } Object . keys ( headers ) . forEach ( ( key ) => { res . header [ key ] = headers [ key ] ; } ) ; res . json ( payload ) ; } ; } } function isQueriesEqual ( query , req ) { return shallowEqual ( query , req . query ) ; } function parseQuery ( query ) { return querystring . parse ( query ) ; } function isRouterMethod ( routerMethod ) { return typeof routerMethod === 'function' ; } function createRouter ( ) { return Router ( { mergeParams : true } ) ; } function createApp ( ) { const app = express ( ) ; app . use ( bodyParser . json ( ) ) ; return app ; } function createProxy ( { target } ) { const targetUrl = target . slice ( - 1 ) === '/' ? target . slice ( 0 , - 1 ) : target ; const pathRewrite = ( path , req ) => { return path . replace ( req . baseUrl , '' ) ; } ; return createProxyMiddleware ( { target : targetUrl , changeOrigin : true , pathRewrite , } ) ; }