import { EventBus } from '@nestjs/cqrs' ; import { getModelToken } from '@nestjs/mongoose' ; import { Test , TestingModule } from '@nestjs/testing' ; import { ConfigService } from '@fc/config' ; import { CryptographyService } from '@fc/cryptography' ; import { LoggerService } from '@fc/logger' ; import { ServiceProviderMetadata } from '@fc/oidc' ; import { ServiceProvider } from './schemas' ; import { ServiceProviderAdapterMongoService } from './service-provider-adapter-mongo.service' ; describe ( 'ServiceProviderAdapterMongoService' , ( ) => { let service ; const validServiceProviderMock = { key : '987654321987654321987654321987654' , entityId : '123456789101112131415161718192021' , active : true , name : 'foo' , title : 'title' , client_secret : "This is an encrypted string, don't ask !" , scopes : [ 'openid' , 'profile' ] , redirect_uris : [ 'https://sp-site.fr/redirect_uris' ] , post_logout_redirect_uris : [ 'https://sp-site.fr/post_logout_redirect_uris' ] , id_token_signed_response_alg : 'ES256' , id_token_encrypted_response_alg : 'RS256' , id_token_encrypted_response_enc : 'AES256GCM' , userinfo_encrypted_response_alg : 'RS256' , userinfo_encrypted_response_enc : 'AES256GCM' , userinfo_signed_response_alg : 'ES256' , jwks_uri : 'https://sp-site.fr/jwks-uri' , idpFilterExclude : true , idpFilterList : [ ] , type : 'public' , identityConsent : false , } ; const invalidServiceProviderMock = { ... validServiceProviderMock , active : 'NOT_A_BOOLEAN' , } ; const serviceProviderListMock = [ validServiceProviderMock ] ; const loggerMock = { setContext : jest . fn ( ) , warn : jest . fn ( ) , debug : jest . fn ( ) , trace : jest . fn ( ) , } ; const cryptographyMock = { decrypt : jest . fn ( ) , } ; const repositoryMock = { find : jest . fn ( ) , lean : jest . fn ( ) , watch : jest . fn ( ) , } ; const eventBusMock = { publish : jest . fn ( ) , } ; const serviceProviderModel = getModelToken ( 'ServiceProvider' ) ; const configMock = { get : jest . fn ( ) , } ; beforeEach ( async ( ) => { jest . clearAllMocks ( ) ; jest . resetAllMocks ( ) ; jest . restoreAllMocks ( ) ; const module = await Test . createTestingModule ( { providers : [ CryptographyService , ServiceProviderAdapterMongoService , { provide : serviceProviderModel , 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 < ServiceProviderAdapterMongoService > ( ServiceProviderAdapterMongoService ) ; repositoryMock . find . mockReturnValueOnce ( repositoryMock ) ; repositoryMock . lean . mockResolvedValueOnce ( serviceProviderListMock ) ; } ) ; it ( 'should be defined' , ( ) => { expect ( service ) . toBeDefined ( ) ; } ) ; describe ( 'onModuleInit' , ( ) => { it ( 'should call initOperationTypeWatcher' , ( ) => { service [ 'initOperationTypeWatcher' ] = jest . fn ( ) ; service . onModuleInit ( ) ; expect ( service [ 'initOperationTypeWatcher' ] ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; } ) ; 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 = { INSERT : 'insert' , UPDATE : 'update' , DELETE : 'delete' , RENAME : 'rename' , REPLACE : 'replace' , } ; 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 service provider with change legacy property name by openid property name' , ( ) => { const expected = { ... validServiceProviderMock , client_id : validServiceProviderMock . key , client_secret : 'client_secret' , scope : validServiceProviderMock . scopes . join ( ' ' ) , } ; delete expected . key ; delete expected . scopes ; service [ 'decryptClientSecret' ] = jest . fn ( ) . mockReturnValueOnce ( expected . client_secret ) ; const result = service [ 'legacyToOpenIdPropertyName' ] ( validServiceProviderMock as unknown as ServiceProvider ) ; expect ( result ) . toStrictEqual ( expected ) ; } ) ; } ) ; describe ( 'findAllServiceProvider' , ( ) => { it ( 'should resolve' , async ( ) => { const result = service [ 'findAllServiceProvider' ] ( ) ; expect ( result ) . toBeInstanceOf ( Promise ) ; } ) ; it ( 'should have called find once' , async ( ) => { await service [ 'findAllServiceProvider' ] ( ) ; expect ( repositoryMock . find ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should return result of type list' , async ( ) => { const result = await service [ 'findAllServiceProvider' ] ( ) ; expect ( result ) . toStrictEqual ( serviceProviderListMock ) ; } ) ; it ( 'should log a warning if an entry is excluded by the DTO' , async ( ) => { const invalidServiceProviderListMock = [ validServiceProviderMock , invalidServiceProviderMock , ] ; repositoryMock . lean = jest . fn ( ) . mockResolvedValueOnce ( invalidServiceProviderListMock ) ; await service [ 'findAllServiceProvider' ] ( ) ; expect ( loggerMock . warn ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should filter out any entry excluded by the DTO' , async ( ) => { const invalidServiceProviderListMock = [ validServiceProviderMock , invalidServiceProviderMock , ] ; repositoryMock . lean = jest . fn ( ) . mockResolvedValueOnce ( invalidServiceProviderListMock ) ; const result = await service [ 'findAllServiceProvider' ] ( ) ; expect ( result ) . toEqual ( serviceProviderListMock ) ; } ) ; } ) ; describe ( 'getList' , ( ) => { beforeEach ( ( ) => { service [ 'findAllServiceProvider' ] = jest . fn ( ) . mockResolvedValueOnce ( serviceProviderListMock ) ; } ) ; it ( 'should resolve' , async ( ) => { const legacyToOpenIdMock = jest . spyOn < ServiceProviderAdapterMongoService , any > ( service , 'legacyToOpenIdPropertyName' ) ; legacyToOpenIdMock . mockImplementationOnce ( ( data ) => data ) ; const result = service . getList ( true ) ; expect ( result ) . toBeInstanceOf ( Promise ) ; await result ; } ) ; it ( 'should return service provider list refreshed (refresh forced)' , async ( ) => { const expected = [ { ... validServiceProviderMock , client_id : '987654321987654321987654321987654' , client_secret : 'client_secret' , scope : 'openid profile' , } , ] ; delete expected [ 0 ] . key ; delete expected [ 0 ] . scopes ; service [ 'decryptClientSecret' ] = jest . fn ( ) . mockReturnValueOnce ( expected [ 0 ] . client_secret ) ; const result = await service . getList ( true ) ; expect ( service [ 'findAllServiceProvider' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( result ) . toStrictEqual ( expected ) ; } ) ; it ( 'should return service provider list if serviceProviderListCache is not defined' , async ( ) => { service [ 'listCache' ] = [ { client_id : 'foo' , } , { client_id : 'bar' , } , ] as unknown as ServiceProviderMetadata [ ] ; service [ 'findAllServiceProvider' ] = jest . fn ( ) ; const result = await service . getList ( ) ; expect ( result ) . toBe ( service [ 'listCache' ] ) ; expect ( service [ 'findAllServiceProvider' ] ) . toHaveBeenCalledTimes ( 0 ) ; } ) ; it ( 'should return service provider list with the cached version' , async ( ) => { const expected = [ { ... validServiceProviderMock , client_id : '987654321987654321987654321987654' , client_secret : 'client_secret' , scope : 'openid profile' , } , ] ; delete expected [ 0 ] . key ; delete expected [ 0 ] . scopes ; service [ 'decryptClientSecret' ] = jest . fn ( ) . mockReturnValueOnce ( expected [ 0 ] . client_secret ) ; const result = await service . getList ( true ) ; expect ( service [ 'findAllServiceProvider' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( result ) . toStrictEqual ( expected ) ; } ) ; } ) ; describe ( 'getById' , ( ) => { const spListMock = [ { client_id : 'wizz' , } , { client_id : 'foo' , } , { client_id : 'bar' , } , ] ; it ( 'should return an existing SP' , async ( ) => { const idMock = 'foo' ; service . getList = jest . fn ( ) . mockResolvedValueOnce ( spListMock ) ; const result = await service . getById ( idMock ) ; expect ( result ) . toEqual ( { client_id : 'foo' , } ) ; } ) ; it ( 'should return undefined for non existing SP' , async ( ) => { const idMock = 'nope' ; service . getList = jest . fn ( ) . mockResolvedValueOnce ( spListMock ) ; 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 ( spListMock ) ; await service . getById ( idMock , refresh ) ; expect ( service . getList ) . toHaveBeenCalledTimes ( 1 ) ; expect ( service . getList ) . toHaveBeenCalledWith ( refresh ) ; } ) ; } ) ; describe ( 'shouldExcludeIdp' , ( ) => { const spListMock = [ { client_id : 'wizz' , idpFilterExclude : true , idpFilterList : [ 'idp1' ] , } , { client_id : 'foo' , idpFilterExclude : false , idpFilterList : [ 'idp1' ] , } , { client_id : 'bar' , idpFilterExclude : true , idpFilterList : [ 'idp2' ] , } , ] ; it ( 'Should return true because idp1 is blacklisted' , async ( ) => { service . getById = jest . fn ( ) . mockReturnValueOnce ( spListMock [ 0 ] ) ; const result = await service . shouldExcludeIdp ( 'wizz' , 'idp1' ) ; expect ( result ) . toBeTruthy ( ) ; } ) ; it ( 'Should return false because idp1 is whitelist' , async ( ) => { service . getById = jest . fn ( ) . mockReturnValueOnce ( spListMock [ 1 ] ) ; const result = await service . shouldExcludeIdp ( 'foo' , 'idp1' ) ; expect ( result ) . toBeFalsy ( ) ; } ) ; it ( 'Should return false because idp1 is not blacklisted' , async ( ) => { service . getById = jest . fn ( ) . mockReturnValueOnce ( spListMock [ 2 ] ) ; const result = await service . shouldExcludeIdp ( 'bar' , 'idp1' ) ; expect ( result ) . toBeFalsy ( ) ; } ) ; } ) ; describe ( 'legacyToOpenIdPropertyName' , ( ) => { it ( 'should return service provider with change legacy property name by openid property name' , ( ) => { const expected = { ... validServiceProviderMock , client_id : '987654321987654321987654321987654' , client_secret : 'client_secret' , scope : 'openid profile' , } ; delete expected . key ; delete expected . scopes ; service [ 'decryptClientSecret' ] = jest . fn ( ) . mockReturnValueOnce ( 'client_secret' ) ; const result = service [ 'legacyToOpenIdPropertyName' ] ( validServiceProviderMock as unknown as ServiceProvider ) ; expect ( result ) . toStrictEqual ( expected ) ; } ) ; } ) ; 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 ( 'consentRequired' , ( ) => { it ( 'should return true if the service provider is private and consent required' , ( ) => { const givenServiceProvider = { ... validServiceProviderMock , type : 'private' , identityConsent : true , } ; const result = service . consentRequired ( givenServiceProvider . type , givenServiceProvider . identityConsent ) ; expect ( result ) . toEqual ( true ) ; } ) ; it ( 'should return false if the service provider is private and consent not required' , ( ) => { const givenServiceProvider = { ... validServiceProviderMock , type : 'private' , } ; const result = service . consentRequired ( givenServiceProvider . type , givenServiceProvider . identityConsent ) ; expect ( result ) . toEqual ( false ) ; } ) ; it ( 'should return false if the service provider is public and consent required' , ( ) => { const givenServiceProvider = { ... validServiceProviderMock , identityConsent : true , } ; const result = service . consentRequired ( givenServiceProvider . type , givenServiceProvider . identityConsent ) ; expect ( result ) . toEqual ( false ) ; } ) ; it ( 'should return false if the service provider is public and consent not required' , ( ) => { const result = service . consentRequired ( validServiceProviderMock . type , validServiceProviderMock . identityConsent ) ; expect ( result ) . toEqual ( false ) ; } ) ; } ) ; } ) ;