import * as OidcProvider from 'oidc-provider' ; import { HttpAdapterHost } from '@nestjs/core' ; import { Test , TestingModule } from '@nestjs/testing' ; import { LoggerLevelNames , LoggerService } from '@fc/logger' ; import { IOidcIdentity , OidcSession } from '@fc/oidc' ; import { REDIS_CONNECTION_TOKEN } from '@fc/redis' ; import { OidcProviderMiddlewarePattern , OidcProviderMiddlewareStep , } from './enums' ; import { OidcProviderBindingException , OidcProviderInitialisationException , OidcProviderInteractionNotFoundException , OidcProviderRuntimeException , } from './exceptions' ; import { OidcProviderService } from './oidc-provider.service' ; import { OidcProviderConfigService } from './services/oidc-provider-config.service' ; import { OidcProviderErrorService } from './services/oidc-provider-error.service' ; import { OidcProviderGrantService } from './services/oidc-provider-grant.service' ; describe ( 'OidcProviderService' , ( ) => { let service ; const httpAdapterHostMock = { httpAdapter : { use : jest . fn ( ) , } , } ; const ProviderProxyMock = class { callback ( ) { return true ; } } as any ; const BadProviderProxyMock = class { constructor ( ) { throw Error ( 'not Working' ) ; } } as any ; const configServiceMock = { get : jest . fn ( ) , } ; const oidcProviderRedisAdapterMock = class AdapterMock { } ; const configOidcProviderMock = { prefix : '/api' , issuer : 'http://foo.bar' , configuration : { adapter : oidcProviderRedisAdapterMock , jwks : { keys : [ ] } , features : { devInteractions : { enabled : false } , } , cookies : { keys : [ 'foo' ] , } , } , } ; const loggerServiceMock = { setContext : jest . fn ( ) , verbose : jest . fn ( ) , debug : jest . fn ( ) , trace : jest . fn ( ) , businessEvent : jest . fn ( ) , } as unknown as LoggerService ; const serviceProviderListMock = [ { name : 'my SP' } ] ; const serviceProviderServiceMock = { getList : jest . fn ( ) , getById : jest . fn ( ) , } ; const sessionServiceMock = { set : jest . fn ( ) , get : jest . fn ( ) , } ; const useSpy = jest . fn ( ) ; const providerMock = { middlewares : [ ] , use : ( middleware ) => { providerMock . middlewares . push ( middleware ) ; useSpy ( ) ; } , on : jest . fn ( ) , interactionDetails : jest . fn ( ) , interactionFinished : jest . fn ( ) , } ; const redisMock = { hgetall : jest . fn ( ) , get : jest . fn ( ) , multi : jest . fn ( ) , hset : jest . fn ( ) , ttl : jest . fn ( ) , lrange : jest . fn ( ) , del : jest . fn ( ) , } ; const oidcProviderErrorServiceMock = { catchErrorEvents : jest . fn ( ) , throwError : jest . fn ( ) , } ; const oidcProviderConfigServiceMock = { getConfig : jest . fn ( ) , findAccount : jest . fn ( ) , } ; const oidcProviderGrantServiceMock = { generateGrant : jest . fn ( ) , saveGrant : jest . fn ( ) , } ; beforeEach ( async ( ) => { const module = await Test . createTestingModule ( { providers : [ OidcProviderService , HttpAdapterHost , LoggerService , { provide : REDIS_CONNECTION_TOKEN , useValue : redisMock , } , OidcProviderErrorService , OidcProviderConfigService , OidcProviderGrantService , ] , } ) . overrideProvider ( HttpAdapterHost ) . useValue ( httpAdapterHostMock ) . overrideProvider ( LoggerService ) . useValue ( loggerServiceMock ) . overrideProvider ( OidcProviderErrorService ) . useValue ( oidcProviderErrorServiceMock ) . overrideProvider ( OidcProviderConfigService ) . useValue ( oidcProviderConfigServiceMock ) . overrideProvider ( OidcProviderGrantService ) . useValue ( oidcProviderGrantServiceMock ) . compile ( ) ; module . useLogger ( loggerServiceMock ) ; service = module . get < OidcProviderService > ( OidcProviderService ) ; jest . resetAllMocks ( ) ; configServiceMock . get . mockImplementation ( ( module ) => { switch ( module ) { case 'OidcProvider' : return configOidcProviderMock ; case 'Logger' : return { path : '/dev/null' , level : LoggerLevelNames . TRACE , isDevelopment : false , } ; } } ) ; service [ 'provider' ] = providerMock as any ; serviceProviderServiceMock . getById . mockResolvedValue ( serviceProviderListMock [ 0 ] ) ; serviceProviderServiceMock . getList . mockResolvedValue ( serviceProviderListMock ) ; } ) ; describe ( 'onModuleInit' , ( ) => { beforeEach ( ( ) => { oidcProviderConfigServiceMock . getConfig . mockReturnValueOnce ( configOidcProviderMock ) ; oidcProviderConfigServiceMock . getConfig . mockImplementation ( ( ) => ( { paramsMock : 'paramMocks' , } ) ) ; service [ 'getConfig' ] = jest . fn ( ) . mockResolvedValue ( { ... configOidcProviderMock , } ) ; service [ 'registerMiddlewares' ] = jest . fn ( ) ; service [ 'catchErrorEvents' ] = jest . fn ( ) ; } ) ; it ( 'Should create oidc-provider instance' , async ( ) => { await service . onModuleInit ( ) ; expect ( service ) . toBeDefined ( ) ; expect ( service [ 'provider' ] ) . toBeInstanceOf ( OidcProvider . Provider ) ; } ) ; it ( 'should mount oidc-provider in express' , async ( ) => { await service . onModuleInit ( ) ; expect ( httpAdapterHostMock . httpAdapter . use ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should throw if provider can not be instantied' , async ( ) => { service [ 'ProviderProxy' ] = BadProviderProxyMock ; await expect ( service . onModuleInit ( ) ) . rejects . toThrow ( OidcProviderInitialisationException ) ; } ) ; it ( 'should throw if provider can not be mounted to server' , async ( ) => { service [ 'ProviderProxy' ] = ProviderProxyMock ; httpAdapterHostMock . httpAdapter . use . mockImplementation ( ( ) => { throw Error ( 'not working' ) ; } ) ; await expect ( service . onModuleInit ( ) ) . rejects . toThrow ( OidcProviderBindingException ) ; } ) ; it ( 'should call several internal initializers' , async ( ) => { service [ 'ProviderProxy' ] = ProviderProxyMock ; await service . onModuleInit ( ) ; expect ( service [ 'errorService' ] [ 'catchErrorEvents' ] ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; } ) ; describe ( 'getProvider' , ( ) => { it ( 'Should return the oidc-provider instance' , ( ) => { const result = service . getProvider ( ) ; expect ( result ) . toBe ( service [ 'provider' ] ) ; } ) ; } ) ; describe ( 'finishInteraction' , ( ) => { const reqMock = { fc : { interactionId : 'interactiondMockValue' } , } ; const resMock = { } ; it ( 'should return the result of oidc-provider.interactionFinished()' , async ( ) => { const resolvedValue = Symbol ( 'resolved value' ) ; providerMock . interactionFinished . mockResolvedValueOnce ( resolvedValue ) ; const sessionDataMock = { spAcr : 'spAcrValue' , spIdentity : { sub : 'subValue' , } , } ; sessionServiceMock . get . mockResolvedValueOnce ( sessionDataMock ) ; const result = await service . finishInteraction ( reqMock , resMock , sessionDataMock ) ; expect ( result ) . toBe ( resolvedValue ) ; } ) ; it ( 'should finish interaction with grant' , async ( ) => { const spAcrMock = 'spAcrValue' ; const interactionIdMock = 'interactionIdValue' ; const amrValueMock = [ 'amrValue' ] ; const spIdentityMock = { sub : 'subValue' , } as IOidcIdentity ; const sessionDataMock = { spAcr : spAcrMock , amr : amrValueMock , interactionId : interactionIdMock , spIdentity : spIdentityMock , } ; sessionServiceMock . get . mockResolvedValueOnce ( sessionDataMock ) ; const grantMock = Symbol ( 'grant' ) ; const grantIdMock = Symbol ( 'grantIdMock' ) ; oidcProviderGrantServiceMock . generateGrant . mockResolvedValueOnce ( grantMock ) ; oidcProviderGrantServiceMock . saveGrant . mockResolvedValueOnce ( grantIdMock ) ; const resultMock = { consent : { grantId : grantIdMock , } , login : { accountId : spIdentityMock . sub , acr : spAcrMock , amr : amrValueMock , ts : expect . any ( Number ) , remember : false , } , } ; providerMock . interactionFinished . mockResolvedValueOnce ( 'ignoredValue' ) ; await service . finishInteraction ( reqMock , resMock , sessionDataMock ) ; expect ( oidcProviderGrantServiceMock . generateGrant ) . toHaveBeenCalledTimes ( 1 ) ; expect ( oidcProviderGrantServiceMock . generateGrant ) . toHaveBeenCalledWith ( service . getProvider ( ) , reqMock , resMock , spIdentityMock . sub ) ; expect ( oidcProviderGrantServiceMock . saveGrant ) . toHaveBeenCalledTimes ( 1 ) ; expect ( oidcProviderGrantServiceMock . saveGrant ) . toHaveBeenCalledWith ( grantMock ) ; expect ( providerMock . interactionFinished ) . toHaveBeenCalledTimes ( 1 ) ; expect ( providerMock . interactionFinished ) . toHaveBeenCalledWith ( reqMock , resMock , resultMock ) ; } ) ; it ( 'should throw OidcProviderRuntimeException' , async ( ) => { const nativeError = new Error ( 'invalid_request' ) ; providerMock . interactionFinished . mockRejectedValueOnce ( nativeError ) ; const sessionDataMock = { spAcr : 'spAcrValue' , spIdentity : { sub : 'subValue' , } , } ; sessionServiceMock . get . mockResolvedValueOnce ( sessionDataMock ) ; await expect ( service . finishInteraction ( reqMock , resMock , sessionDataMock ) ) . rejects . toThrow ( OidcProviderRuntimeException ) ; } ) ; } ) ; describe ( 'getInteractionIdFromCtx' , ( ) => { it ( 'should call getInteractionIdFromCtxSymbol' , ( ) => { const ctxMock = { req : { _parsedUrl : { pathname : '/token' } } , oidc : { entities : { Grant : { accountId : 'accountIdValue' , } , } , } , } ; const result = service [ 'getInteractionIdFromCtx' ] ( ctxMock ) ; expect ( result ) . toEqual ( 'accountIdValue' ) ; } ) ; it ( 'should get id from context with /userinfo' , ( ) => { const ctxMock = { req : { _parsedUrl : { pathname : '/userinfo' } } , oidc : { entities : { Account : { accountId : 'accountIdValue' , } , } , } , } ; const result = service [ 'getInteractionIdFromCtx' ] ( ctxMock ) ; expect ( result ) . toEqual ( 'accountIdValue' ) ; } ) ; it ( 'should throw' , ( ) => { const ctxMock = { req : { _parsedUrl : { pathname : '/somewhere' } } } ; expect ( ( ) => service [ 'getInteractionIdFromCtx' ] ( ctxMock ) ) . toThrow ( OidcProviderInteractionNotFoundException ) ; } ) ; } ) ; describe ( 'registerMiddleware' , ( ) => { it ( 'should register middleware but not call callback on middleware registration' , ( ) => { providerMock . middlewares = [ ] ; const callback = jest . fn ( ) ; service . registerMiddleware ( OidcProviderMiddlewareStep . BEFORE , OidcProviderMiddlewarePattern . USERINFO , callback ) ; expect ( useSpy ) . toBeCalledTimes ( 1 ) ; expect ( providerMock . middlewares ) . toHaveLength ( 1 ) ; expect ( callback ) . toHaveBeenCalledTimes ( 0 ) ; } ) ; it ( 'should call provider use with callback function BEFORE' , ( ) => { providerMock . middlewares = [ ] ; const callback = jest . fn ( ) ; const ctx = { path : OidcProviderMiddlewarePattern . USERINFO } ; const next = async ( ) => Promise . resolve ( ) ; service [ 'runMiddlewareBeforePattern' ] = jest . fn ( ) ; service . registerMiddleware ( OidcProviderMiddlewareStep . BEFORE , OidcProviderMiddlewarePattern . USERINFO , callback ) ; providerMock . middlewares [ 0 ] . call ( null , ctx , next ) ; expect ( service [ 'runMiddlewareBeforePattern' ] ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should call provider use with callback function AFTER' , async ( ) => { providerMock . middlewares = [ ] ; const callback = jest . fn ( ) ; const ctx = { oidc : { route : OidcProviderMiddlewarePattern . USERINFO } , } ; const next = ( ) => async ( ) => Promise . resolve ( ) ; service [ 'runMiddlewareAfterPattern' ] = jest . fn ( ) ; service . registerMiddleware ( OidcProviderMiddlewareStep . AFTER , OidcProviderMiddlewarePattern . USERINFO , callback ) ; await providerMock . middlewares [ 0 ] . call ( null , ctx , next ) ; expect ( service [ 'runMiddlewareAfterPattern' ] ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should match on path' , async ( ) => { providerMock . middlewares = [ ] ; const callback = jest . fn ( ) ; const ctx = { path : OidcProviderMiddlewarePattern . USERINFO , } ; const next = ( ) => async ( ) => Promise . resolve ( ) ; service . registerMiddleware ( OidcProviderMiddlewareStep . AFTER , OidcProviderMiddlewarePattern . USERINFO , callback ) ; await providerMock . middlewares [ 0 ] . call ( null , ctx , next ) ; expect ( callback ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; } ) ; describe ( 'runMiddlewareBeforePattern' , ( ) => { it ( 'should call the callback if path param equal pattern with good step' , async ( ) => { const callback = jest . fn ( ) ; const ctx = { oidc : { route : OidcProviderMiddlewarePattern . USERINFO } , } ; service [ 'runMiddlewareBeforePattern' ] ( { step : OidcProviderMiddlewareStep . BEFORE , path : OidcProviderMiddlewarePattern . USERINFO , pattern : OidcProviderMiddlewarePattern . USERINFO , ctx , } , callback ) ; expect ( callback ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should not call the callback method if wrong path is send' , ( ) => { const callback = jest . fn ( ) ; const ctx = { oidc : { route : OidcProviderMiddlewarePattern . USERINFO } , } ; service [ 'runMiddlewareBeforePattern' ] ( { step : OidcProviderMiddlewareStep . BEFORE , path : '' , pattern : OidcProviderMiddlewarePattern . USERINFO , ctx , } , callback ) ; expect ( callback ) . toHaveBeenCalledTimes ( 0 ) ; } ) ; it ( 'should not call the callback method if wrong step is send' , ( ) => { const callback = jest . fn ( ) ; const ctx = { oidc : { route : OidcProviderMiddlewarePattern . USERINFO } , } ; service [ 'runMiddlewareBeforePattern' ] ( { step : OidcProviderMiddlewareStep . AFTER , path : OidcProviderMiddlewarePattern . USERINFO , pattern : OidcProviderMiddlewarePattern . USERINFO , ctx , } , callback ) ; expect ( callback ) . toHaveBeenCalledTimes ( 0 ) ; } ) ; } ) ; describe ( 'runMiddlewareAfterPattern' , ( ) => { it ( 'should call the callback if route param equal pattern with good step' , async ( ) => { const callback = jest . fn ( ) ; const ctx = { oidc : { route : OidcProviderMiddlewarePattern . USERINFO } , } ; service [ 'runMiddlewareAfterPattern' ] ( { step : OidcProviderMiddlewareStep . AFTER , route : OidcProviderMiddlewarePattern . USERINFO , path : '' , pattern : OidcProviderMiddlewarePattern . USERINFO , ctx , } , callback ) ; expect ( callback ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should call the callback if path param equal pattern with good step' , async ( ) => { const callback = jest . fn ( ) ; const ctx = { oidc : { route : OidcProviderMiddlewarePattern . USERINFO } , } ; service [ 'runMiddlewareAfterPattern' ] ( { step : OidcProviderMiddlewareStep . AFTER , route : '' , path : OidcProviderMiddlewarePattern . USERINFO , pattern : OidcProviderMiddlewarePattern . USERINFO , ctx , } , callback ) ; expect ( callback ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should not call the callback method if wrong step is send' , ( ) => { const callback = jest . fn ( ) ; const ctx = { oidc : { route : OidcProviderMiddlewarePattern . USERINFO } , } ; service [ 'runMiddlewareAfterPattern' ] ( { step : OidcProviderMiddlewareStep . BEFORE , route : OidcProviderMiddlewarePattern . USERINFO , path : OidcProviderMiddlewarePattern . USERINFO , pattern : OidcProviderMiddlewarePattern . USERINFO , ctx , } , callback ) ; expect ( callback ) . toHaveBeenCalledTimes ( 0 ) ; } ) ; it ( 'should not call the callback method if wrong path or route is send' , ( ) => { const callback = jest . fn ( ) ; const ctx = { oidc : { route : OidcProviderMiddlewarePattern . USERINFO } , } ; service [ 'runMiddlewareAfterPattern' ] ( { step : OidcProviderMiddlewareStep . BEFORE , route : '' , path : '' , pattern : OidcProviderMiddlewarePattern . USERINFO , ctx , } , callback ) ; expect ( callback ) . toHaveBeenCalledTimes ( 0 ) ; } ) ; } ) ; describe ( 'getHttpOptions' , ( ) => { const timeoutMock = 42 ; it ( 'Should return the timeout http options' , ( ) => { const options = { timeout : timeoutMock , } ; service [ 'configuration' ] = options ; const result = service [ 'getHttpOptions' ] ( options ) ; expect ( result ) . toStrictEqual ( { timeout : timeoutMock , } ) ; } ) ; } ) ; describe ( 'getInteraction' , ( ) => { it ( 'should return the result of oidc-provider.interactionDetails()' , async ( ) => { const reqMock = { } ; const resMock = { } ; const resolvedValue = { prompt : { name : Symbol ( 'name' ) , reasons : Symbol ( 'reasons' ) , } , } ; providerMock . interactionDetails . mockResolvedValueOnce ( resolvedValue ) ; const result = await service . getInteraction ( reqMock , resMock ) ; expect ( result ) . toBe ( resolvedValue ) ; } ) ; it ( 'should throw OidcProviderRuntimeException' , async ( ) => { const reqMock = { } ; const resMock = { } ; const nativeError = new Error ( 'invalid_request' ) ; providerMock . interactionDetails . mockRejectedValueOnce ( nativeError ) ; await expect ( service . getInteraction ( reqMock , resMock ) ) . rejects . toThrow ( OidcProviderRuntimeException ) ; } ) ; } ) ; describe ( 'abortInteraction' , ( ) => { it ( 'Should throw if' , async ( ) => { const resMock = Symbol ( 'mock result' ) ; const reqMock = Symbol ( 'mock request' ) ; const mockErr = 'this is an error message' ; const mockErrDescription = 'this is an error description' ; const nativeError = new Error ( 'invalid_request' ) ; providerMock . interactionFinished . mockRejectedValueOnce ( nativeError ) ; await expect ( service . abortInteraction ( reqMock , resMock , mockErr , mockErrDescription ) ) . rejects . toThrow ( OidcProviderRuntimeException ) ; } ) ; it ( 'Should have called this.provider.interactionFinished with parameters' , async ( ) => { const resMock = Symbol ( 'mock result' ) ; const reqMock = Symbol ( 'mock request' ) ; const mockErr = 'this is an error message' ; const mockErrDescription = 'this is an error description' ; service . abortInteraction ( reqMock , resMock , mockErr , mockErrDescription ) ; expect ( providerMock . interactionFinished ) . toHaveBeenCalledTimes ( 1 ) ; expect ( providerMock . interactionFinished ) . toHaveBeenCalledWith ( reqMock , resMock , { error : mockErr , error_description : mockErrDescription , } ) ; } ) ; } ) ; } ) ;