import { mocked } from 'jest-mock' ; import { EventBus } from '@nestjs/cqrs' ; import { getModelToken } from '@nestjs/mongoose' ; import { Test , TestingModule } from '@nestjs/testing' ; import { validateDto } from '@fc/common' ; import { ConfigService } from '@fc/config' ; import { CryptographyService } from '@fc/cryptography' ; import { LoggerService } from '@fc/logger' ; import { IdentityProviderMetadata } from '@fc/oidc' ; import { DiscoveryIdpAdapterMongoDTO , IdentityProviderAdapterMongoDTO , } from './dto' ; import { IdentityProviderAdapterMongoService } from './identity-provider-adapter-mongo.service' ; import { FilteringOptions } from './interfaces' ; import { IdentityProvider } from './schemas' ; jest . mock ( '@fc/common' , ( ) => ( { ... ( jest . requireActual ( '@fc/common' ) as any ) , validateDto : jest . fn ( ) , } ) ) ; describe ( 'IdentityProviderAdapterMongoService' , ( ) => { let service ; const legacyIdentityProviderMock = { active : true , authzURL : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/authorize' , clientID : 'clientID' , client_secret : '7vhnwzo1yUVOJT9GJ91gD5oid56effu1' , discovery : false , display : false , eidas : 2 , endSessionURL : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/session/end' , featureHandlers : { authenticationEmail : null , coreVerify : 'core-fcp-default-verify' , } , id_token_encrypted_response_alg : 'RSA-OAEP' , id_token_encrypted_response_enc : 'A256GCM' , id_token_signed_response_alg : 'HS256' , image : 'provider1.png' , jwksURL : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/certs' , name : 'provider1' , post_logout_redirect_uris : [ 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/logout-from-provider' , ] , redirect_uris : [ 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/oidc-callback/fip1-high' , ] , response_types : [ 'code' ] , revocation_endpoint_auth_method : 'client_secret_post' , title : 'provider 1' , tokenURL : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/token' , token_endpoint_auth_method : 'client_secret_post' , uid : 'uid' , url : 'https://core-fcp-high.docker.dev-franceconnect.fr' , userInfoURL : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/userinfo' , userinfo_encrypted_response_alg : 'RSA-OAEP' , userinfo_encrypted_response_enc : 'A256GCM' , userinfo_signed_response_alg : 'HS256' , } ; const legacyToOpenIdPropertyNameOutputMock = { active : true , authorization_endpoint : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/authorize' , client_id : 'clientID' , client_secret : '7vhnwzo1yUVOJT9GJ91gD5oid56effu1' , discovery : false , display : false , maxAuthorizedAcr : 'eidas2' , end_session_endpoint : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/session/end' , featureHandlers : { authenticationEmail : null , coreVerify : 'core-fcp-default-verify' , } , id_token_encrypted_response_alg : 'RSA-OAEP' , id_token_encrypted_response_enc : 'A256GCM' , id_token_signed_response_alg : 'HS256' , image : 'provider1.png' , issuer : 'https://core-fcp-high.docker.dev-franceconnect.fr' , jwks_uri : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/certs' , name : 'provider1' , post_logout_redirect_uris : [ 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/logout-from-provider' , ] , redirect_uris : [ 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/oidc-callback/fip1-high' , ] , response_types : [ 'code' ] , revocation_endpoint_auth_method : 'client_secret_post' , title : 'provider 1' , token_endpoint : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/token' , token_endpoint_auth_method : 'client_secret_post' , uid : 'uid' , userinfo_encrypted_response_alg : 'RSA-OAEP' , userinfo_encrypted_response_enc : 'A256GCM' , userinfo_endpoint : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/userinfo' , userinfo_signed_response_alg : 'HS256' , } ; const validIdentityProviderMock = { active : true , client : { client_id : 'clientID' , client_secret : '7vhnwzo1yUVOJT9GJ91gD5oid56effu1' , id_token_encrypted_response_alg : 'RSA-OAEP' , id_token_encrypted_response_enc : 'A256GCM' , id_token_signed_response_alg : 'HS256' , post_logout_redirect_uris : [ 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/logout-from-provider' , ] , redirect_uris : [ 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/oidc-callback/fip1-high' , ] , response_types : [ 'code' ] , revocation_endpoint_auth_method : 'client_secret_post' , token_endpoint_auth_method : 'client_secret_post' , userinfo_encrypted_response_alg : 'RSA-OAEP' , userinfo_encrypted_response_enc : 'A256GCM' , userinfo_signed_response_alg : 'HS256' , } , discovery : false , display : false , maxAuthorizedAcr : 'eidas2' , featureHandlers : { authenticationEmail : null , coreVerify : 'core-fcp-default-verify' , } , image : 'provider1.png' , issuer : { authorization_endpoint : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/authorize' , end_session_endpoint : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/session/end' , issuer : 'https://core-fcp-high.docker.dev-franceconnect.fr' , jwks_uri : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/certs' , token_endpoint : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/token' , userinfo_endpoint : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/userinfo' , } , name : 'provider1' , title : 'provider 1' , uid : 'uid' , } ; const invalidIdentityProviderMock = { ... legacyIdentityProviderMock , active : 'NOT_A_BOOLEAN' , } ; const identityProviderListMock = [ legacyIdentityProviderMock ] ; const loggerMock = { debug : jest . fn ( ) , setContext : jest . fn ( ) , trace : jest . fn ( ) , warn : jest . fn ( ) , } ; const cryptographyMock = { decrypt : jest . fn ( ) , } ; const configMock = { get : jest . fn ( ) , } ; const repositoryMock = { lean : jest . fn ( ) , find : jest . fn ( ) , watch : jest . fn ( ) , } ; const eventBusMock = { publish : jest . fn ( ) , } ; const identityProviderModel = getModelToken ( 'IdentityProvider' ) ; beforeEach ( async ( ) => { jest . clearAllMocks ( ) ; jest . resetAllMocks ( ) ; jest . restoreAllMocks ( ) ; const module = await Test . createTestingModule ( { providers : [ CryptographyService , IdentityProviderAdapterMongoService , { provide : identityProviderModel , useValue : repositoryMock , } , LoggerService , EventBus , ConfigService , ] , } ) . overrideProvider ( CryptographyService ) . useValue ( cryptographyMock ) . overrideProvider ( ConfigService ) . useValue ( configMock ) . overrideProvider ( LoggerService ) . useValue ( loggerMock ) . overrideProvider ( EventBus ) . useValue ( eventBusMock ) . compile ( ) ; service = module . get < IdentityProviderAdapterMongoService > ( IdentityProviderAdapterMongoService ) ; repositoryMock . lean . mockResolvedValueOnce ( identityProviderListMock ) ; repositoryMock . find . mockReturnValueOnce ( repositoryMock ) ; } ) ; it ( 'should be defined' , ( ) => { expect ( service ) . toBeDefined ( ) ; } ) ; describe ( 'onModuleInit' , ( ) => { beforeEach ( ( ) => { service [ 'initOperationTypeWatcher' ] = jest . fn ( ) ; service [ 'getList' ] = jest . fn ( ) ; } ) ; it ( 'should call initOperationTypeWatcher' , async ( ) => { await service . onModuleInit ( ) ; expect ( service [ 'initOperationTypeWatcher' ] ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should call getList' , async ( ) => { await service . onModuleInit ( ) ; expect ( service [ 'getList' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( service [ 'getList' ] ) . toHaveBeenCalledWith ( ) ; } ) ; } ) ; describe ( 'initOperationTypeWatcher' , ( ) => { it ( 'should call initOperationTypeWatcher' , ( ) => { const streamMock = { on : jest . fn ( ) } ; repositoryMock . watch = jest . fn ( ) . mockReturnValueOnce ( streamMock ) ; service [ 'initOperationTypeWatcher' ] ( ) ; expect ( repositoryMock . watch ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; } ) ; describe ( 'operationTypeWatcher' , ( ) => { const operationTypes = { DELETE : 'delete' , INSERT : 'insert' , RENAME : 'rename' , REPLACE : 'replace' , UPDATE : 'update' , } ; it ( 'should call eventBus.publish() if DB stream.operationType = INSERT' , ( ) => { const streamInsertMock = { operationType : operationTypes . INSERT } ; service [ 'operationTypeWatcher' ] ( streamInsertMock ) ; expect ( service [ 'eventBus' ] . publish ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should call eventBus.publish() if DB stream.operationType = UPDATE' , ( ) => { const streamInsertMock = { operationType : operationTypes . UPDATE } ; service [ 'operationTypeWatcher' ] ( streamInsertMock ) ; expect ( service [ 'eventBus' ] . publish ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should call eventBus.publish() if DB stream.operationType = DELETE' , ( ) => { const streamInsertMock = { operationType : operationTypes . DELETE } ; service [ 'operationTypeWatcher' ] ( streamInsertMock ) ; expect ( service [ 'eventBus' ] . publish ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should call eventBus.publish() if DB stream.operationType = RENAME' , ( ) => { const streamInsertMock = { operationType : operationTypes . RENAME } ; service [ 'operationTypeWatcher' ] ( streamInsertMock ) ; expect ( service [ 'eventBus' ] . publish ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should call eventBus.publish() if DB stream.operationType = REPLACE' , ( ) => { const streamInsertMock = { operationType : operationTypes . REPLACE } ; service [ 'operationTypeWatcher' ] ( streamInsertMock ) ; expect ( service [ 'eventBus' ] . publish ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( "shouldn't call eventBus.publish() if DB stream.operationType = null" , ( ) => { const streamInsertMock = { operationType : null } ; service [ 'operationTypeWatcher' ] ( streamInsertMock ) ; expect ( service [ 'eventBus' ] . publish ) . toHaveBeenCalledTimes ( 0 ) ; } ) ; } ) ; describe ( 'legacyToOpenIdPropertyName' , ( ) => { it ( 'should return identity provider with change legacy property name by openid property name' , ( ) => { service [ 'decryptClientSecret' ] = jest . fn ( ) . mockReturnValueOnce ( legacyIdentityProviderMock . client_secret ) ; const result = service [ 'legacyToOpenIdPropertyName' ] ( legacyIdentityProviderMock as unknown as IdentityProvider ) ; expect ( result ) . toEqual ( validIdentityProviderMock ) ; } ) ; } ) ; describe ( 'toPanvaFormat' , ( ) => { it ( 'should return an object with data for FC and properties issuer and client for panva' , ( ) => { const result = service [ 'toPanvaFormat' ] ( legacyToOpenIdPropertyNameOutputMock as unknown ) ; expect ( result ) . toEqual ( validIdentityProviderMock ) ; } ) ; } ) ; describe ( 'findAllIdentityProvider' , ( ) => { let validateDtoMock ; beforeEach ( ( ) => { validateDtoMock = mocked ( validateDto ) ; } ) ; it ( 'should resolve' , async ( ) => { validateDtoMock . mockResolvedValueOnce ( [ ] ) ; const result = service [ 'findAllIdentityProvider' ] ( ) ; expect ( result ) . toBeInstanceOf ( Promise ) ; await result ; } ) ; it ( 'should have called find once' , async ( ) => { validateDtoMock . mockResolvedValueOnce ( [ ] ) ; await service [ 'findAllIdentityProvider' ] ( ) ; expect ( repositoryMock . find ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should return result of type list' , async ( ) => { validateDtoMock . mockResolvedValueOnce ( [ ] ) ; const result = await service [ 'findAllIdentityProvider' ] ( ) ; expect ( result ) . toEqual ( [ legacyIdentityProviderMock ] ) ; } ) ; it ( 'should log a warning if an entry is excluded by the DTO' , async ( ) => { const invalidIdentityProviderListMock = [ legacyIdentityProviderMock , invalidIdentityProviderMock , ] ; validateDtoMock . mockResolvedValueOnce ( [ ] ) . mockResolvedValueOnce ( [ 'there is an error' ] ) ; repositoryMock . lean = jest . fn ( ) . mockResolvedValueOnce ( invalidIdentityProviderListMock ) ; await service [ 'findAllIdentityProvider' ] ( ) ; expect ( loggerMock . warn ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should filter out any entry exluded by the DTO' , async ( ) => { const invalidIdentityProviderListMock = [ legacyIdentityProviderMock , invalidIdentityProviderMock , ] ; validateDtoMock . mockResolvedValueOnce ( [ ] ) . mockResolvedValueOnce ( [ 'there is an error' ] ) ; repositoryMock . lean = jest . fn ( ) . mockResolvedValueOnce ( invalidIdentityProviderListMock ) ; const result = await service [ 'findAllIdentityProvider' ] ( ) ; expect ( result ) . toEqual ( identityProviderListMock ) ; } ) ; } ) ; describe ( 'getList' , ( ) => { beforeEach ( ( ) => { service [ 'findAllIdentityProvider' ] = jest . fn ( ) . mockResolvedValueOnce ( identityProviderListMock ) ; } ) ; it ( 'should resolve' , async ( ) => { const legacyToOpenIdMock = jest . spyOn < IdentityProviderAdapterMongoService , any > ( service , 'legacyToOpenIdPropertyName' ) ; legacyToOpenIdMock . mockImplementationOnce ( ( data ) => data ) ; const result = service . getList ( ) ; expect ( result ) . toBeInstanceOf ( Promise ) ; await result ; } ) ; it ( 'should return a list of valides identity providers' , async ( ) => { const expected = [ Object . assign ( validIdentityProviderMock , { client : Object . assign ( { client_id : 'clientID' , client_secret : 'client_secret' , } , validIdentityProviderMock . client ) , } ) , ] ; service [ 'decryptClientSecret' ] = jest . fn ( ) . mockReturnValueOnce ( expected [ 0 ] . client . client_secret ) ; const result = await service . getList ( ) ; expect ( result ) . toEqual ( expected ) ; } ) ; it ( 'should call findAll method if refreshCache is true' , async ( ) => { const refresh = true ; const listMock = [ { client_id : 'foo' , } , { client_id : 'bar' , } , ] ; service [ 'findAllIdentityProvider' ] = jest . fn ( ) . mockResolvedValue ( listMock ) ; service [ 'legacyToOpenIdPropertyName' ] = jest . fn ( ) . mockImplementation ( ( input ) => input ) ; const result = await service . getList ( refresh ) ; expect ( service [ 'findAllIdentityProvider' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( service [ 'findAllIdentityProvider' ] ) . toHaveBeenCalledWith ( ) ; expect ( service [ 'legacyToOpenIdPropertyName' ] ) . toHaveBeenCalledTimes ( listMock . length ) ; expect ( result ) . toEqual ( listMock ) ; } ) ; it ( 'should not call findAll method if refreshCache is not set and cache exists' , async ( ) => { service [ 'listCache' ] = [ { client : { client_id : 'foo' , } , } as IdentityProviderMetadata , { client : { client_id : 'bar' , } , } as IdentityProviderMetadata , ] ; service [ 'findAllIdentityProvider' ] = jest . fn ( ) ; const result = await service . getList ( ) ; expect ( result ) . toBe ( service [ 'listCache' ] ) ; expect ( service [ 'findAllIdentityProvider' ] ) . toHaveBeenCalledTimes ( 0 ) ; } ) ; } ) ; describe ( 'getFilteredList' , ( ) => { const blacklist = true ; const idpList = [ 'idp1' ] ; const defaultOptionsMock = { blacklist , idpList , } ; const defaultProvidersMock = [ { eidas : 1 , name : 'idp1' , uid : 'idp1' , } , { eidas : 1 , name : 'idp2' , uid : 'idp2' , } , { eidas : 2 , name : 'idp3' , uid : 'idp3' , } , { eidas : 3 , name : 'idp4' , uid : 'idp4' , } , ] ; beforeEach ( ( ) => { service . getList = jest . fn ( ) . mockResolvedValueOnce ( defaultProvidersMock ) ; } ) ; it ( 'should resolve' , async ( ) => { const result = service . getFilteredList ( defaultOptionsMock ) ; expect ( result ) . toBeInstanceOf ( Promise ) ; } ) ; it ( 'should return a list of providers whithout blacklisted ones' , async ( ) => { const optionsMock = { ... defaultOptionsMock , acrValues : null , } ; const expected = [ { eidas : 1 , name : 'idp2' , uid : 'idp2' , } , { eidas : 2 , name : 'idp3' , uid : 'idp3' , } , { eidas : 3 , name : 'idp4' , uid : 'idp4' , } , ] ; const result = await service . getFilteredList ( optionsMock ) ; expect ( result ) . toEqual ( expected ) ; } ) ; it ( 'should return a list of providers containing whitelisted ones' , async ( ) => { const optionsMock = { ... defaultOptionsMock , acrValues : null , blacklist : false , idpList : [ 'idp2' ] , } ; const expected = [ { eidas : 1 , name : 'idp2' , uid : 'idp2' , } , ] ; const result = await service . getFilteredList ( optionsMock ) ; expect ( result ) . toEqual ( expected ) ; } ) ; } ) ; describe ( 'getById' , ( ) => { const idpListMock = [ { uid : 'wizz' } , { uid : 'foo' } , { uid : 'bar' } ] ; it ( 'should return an existing IdP' , async ( ) => { const idMock = 'foo' ; service . getList = jest . fn ( ) . mockResolvedValueOnce ( idpListMock ) ; const result = await service . getById ( idMock ) ; expect ( result ) . toEqual ( { uid : 'foo' } ) ; } ) ; it ( 'should return undefined for non existing IdP' , async ( ) => { const idMock = 'nope' ; service . getList = jest . fn ( ) . mockResolvedValueOnce ( idpListMock ) ; const result = await service . getById ( idMock ) ; expect ( result ) . toBeUndefined ( ) ; } ) ; it ( 'should pass refresh flag to getList method' , async ( ) => { const idMock = 'foo' ; const refresh = true ; service . getList = jest . fn ( ) . mockResolvedValueOnce ( idpListMock ) ; await service . getById ( idMock , refresh ) ; expect ( service . getList ) . toHaveBeenCalledTimes ( 1 ) ; expect ( service . getList ) . toHaveBeenCalledWith ( refresh ) ; } ) ; } ) ; describe ( 'decryptClientSecret' , ( ) => { it ( 'should get clientSecretEncryptKey from config' , ( ) => { const clientSecretMock = 'some string' ; const clientSecretEncryptKey = 'Key' ; configMock . get . mockReturnValue ( { clientSecretEncryptKey } ) ; service [ 'decryptClientSecret' ] ( clientSecretMock ) ; expect ( configMock . get ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should call decrypt with enc key from config' , ( ) => { const clientSecretMock = 'some string' ; const clientSecretEncryptKey = 'Key' ; configMock . get . mockReturnValue ( { clientSecretEncryptKey } ) ; cryptographyMock . decrypt . mockReturnValue ( 'totoIsDecrypted' ) ; service [ 'decryptClientSecret' ] ( clientSecretMock ) ; expect ( cryptographyMock . decrypt ) . toHaveBeenCalledTimes ( 1 ) ; expect ( cryptographyMock . decrypt ) . toHaveBeenCalledWith ( clientSecretEncryptKey , Buffer . from ( clientSecretMock , 'base64' ) ) ; } ) ; it ( 'should return clientSecretEncryptKey' , ( ) => { const clientSecretMock = 'some string' ; const clientSecretEncryptKey = 'Key' ; configMock . get . mockReturnValue ( { clientSecretEncryptKey } ) ; cryptographyMock . decrypt . mockReturnValue ( 'totoIsDecrypted' ) ; const result = service [ 'decryptClientSecret' ] ( clientSecretMock ) ; expect ( result ) . toEqual ( 'totoIsDecrypted' ) ; } ) ; } ) ; describe ( 'getIdentityProviderDTO' , ( ) => { it ( 'should return discovery identity provider DTO' , ( ) => { const discovery = true ; const result = service [ 'getIdentityProviderDTO' ] ( discovery ) ; expect ( result ) . toBe ( DiscoveryIdpAdapterMongoDTO ) ; } ) ; it ( 'should return identity provider DTO' , ( ) => { const discovery = false ; const result = service [ 'getIdentityProviderDTO' ] ( discovery ) ; expect ( result ) . toBe ( IdentityProviderAdapterMongoDTO ) ; } ) ; } ) ; } ) ;