import { LoggerService } from '@fc/logger' ; import { OidcProviderService } from '@fc/oidc-provider' ; import { Redis } from '@fc/redis' ; import { OidcProviderParseRedisResponseException , OidcProviderStringifyPayloadForRedisException , } from '../exceptions' ; import { OIDC_PROVIDER_REDIS_PREFIX , OidcProviderRedisAdapter , } from './oidc-provider-redis.adapter' ; describe ( 'OidcProviderRedisAdapter' , ( ) => { let adapter ; const loggerMock = { setContext : jest . fn ( ) , debug : jest . fn ( ) , trace : jest . fn ( ) , } as unknown as LoggerService ; const redisMock = { hgetall : jest . fn ( ) , get : jest . fn ( ) , multi : jest . fn ( ) , hset : jest . fn ( ) , ttl : jest . fn ( ) , lrange : jest . fn ( ) , del : jest . fn ( ) , } ; const multiMock = { hmset : jest . fn ( ) , set : jest . fn ( ) , expire : jest . fn ( ) , rpush : jest . fn ( ) , exec : jest . fn ( ) , del : jest . fn ( ) , } ; const testAdapterName = 'testAdapterName' ; const ServiceProviderAdapterMock = { getList : jest . fn ( ) , shouldExcludeIdp : jest . fn ( ) , getById : jest . fn ( ) , } ; beforeEach ( ( ) => { adapter = new OidcProviderRedisAdapter ( loggerMock , redisMock as unknown as Redis , ServiceProviderAdapterMock , testAdapterName ) ; jest . resetAllMocks ( ) ; redisMock . multi . mockReturnValue ( multiMock ) ; redisMock . ttl . mockResolvedValue ( 42 ) ; redisMock . lrange . mockResolvedValue ( [ 'a' , 'b' , 'c' ] ) ; multiMock . exec . mockImplementation ( ) ; } ) ; describe ( 'getConstructorWithDI' , ( ) => { const nameMock = 'foo' ; const oidcProviderService = { logger : loggerMock , redis : redisMock , } as unknown as OidcProviderService ; const BoundClass = OidcProviderRedisAdapter . getConstructorWithDI ( oidcProviderService , ServiceProviderAdapterMock ) ; it ( 'should pass controls in oidc-provider' , ( ) => { see https expect ( BoundClass . prototype ) . toBeDefined ( ) ; expect ( BoundClass . prototype . constructor ) . toBeDefined ( ) ; expect ( BoundClass . prototype . constructor . name ) . toBeDefined ( ) ; } ) ; it ( 'Should return an instantiable class' , ( ) => { const result = new BoundClass ( nameMock ) ; expect ( result ) . toBeInstanceOf ( OidcProviderRedisAdapter ) ; } ) ; it ( 'Should return an object having injected services' , ( ) => { const result = new BoundClass ( nameMock ) as any ; expect ( result . logger ) . toBe ( loggerMock ) ; expect ( result . redis ) . toBe ( redisMock ) ; } ) ; it ( 'Should return an object having original argument handled' , ( ) => { const result = new BoundClass ( nameMock ) as any ; expect ( result . contextName ) . toBe ( nameMock ) ; } ) ; } ) ; describe ( 'upsert' , ( ) => { const idMock = 'foo' ; const defaultPayload = { } ; const expiresIn = 6000 ; it ( 'should throw if expires is not a number' , async ( ) => { const expires = 'not a number' ; expect ( adapter . upsert ( idMock , defaultPayload , expires ) ) . rejects . toThrow ( TypeError ) ; } ) ; it ( 'should call expires if expiresIn is provided' , async ( ) => { await adapter . upsert ( idMock , defaultPayload , expiresIn ) ; expect ( multiMock . expire ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should not call expires if expiresIn is not provided' , async ( ) => { await adapter . upsert ( idMock , defaultPayload ) ; expect ( multiMock . expire ) . toHaveBeenCalledTimes ( 0 ) ; } ) ; it ( 'should call set for userCode' , async ( ) => { const payload = { userCode : 'userCode' } ; adapter [ 'userCodeKeyFor' ] = jest . fn ( ) ; adapter [ 'userCodeKeyFor' ] . mockReturnValue ( `${ OIDC_PROVIDER_REDIS_PREFIX }:userCode:userCode` ) ; await adapter . upsert ( idMock , payload ) ; expect ( multiMock . set ) . toHaveBeenCalledTimes ( 2 ) ; expect ( multiMock . set ) . toHaveBeenCalledWith ( `${ OIDC_PROVIDER_REDIS_PREFIX }:testAdapterName:foo` , '{"userCode":"userCode"}' ) ; expect ( adapter [ 'userCodeKeyFor' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( adapter [ 'userCodeKeyFor' ] ) . toHaveBeenCalledWith ( 'userCode' ) ; } ) ; it ( 'should call set for uid' , async ( ) => { const payload = { uid : 'uid' } ; adapter [ 'uidKeyFor' ] = jest . fn ( ) ; adapter [ 'uidKeyFor' ] . mockReturnValue ( `${ OIDC_PROVIDER_REDIS_PREFIX }:uid:uid` ) ; await adapter . upsert ( idMock , payload ) ; expect ( multiMock . set ) . toHaveBeenCalledTimes ( 2 ) ; expect ( multiMock . set ) . toHaveBeenCalledWith ( `${ OIDC_PROVIDER_REDIS_PREFIX }:testAdapterName:foo` , '{"uid":"uid"}' ) ; expect ( adapter [ 'uidKeyFor' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( adapter [ 'uidKeyFor' ] ) . toHaveBeenCalledWith ( 'uid' ) ; } ) ; it ( 'should call set and expires for all properties' , async ( ) => { const payload = { grantId : 'grantId' , userCode : 'userCode' , uid : 'uid' , } ; const SET_CALL_COUNT = 3 ; const RPUSH_CALL_COUNT = 1 ; const EXPIRE_CALL_COUNT = 4 ; adapter [ 'userCodeKeyFor' ] = jest . fn ( ) ; adapter [ 'userCodeKeyFor' ] . mockReturnValue ( `${ OIDC_PROVIDER_REDIS_PREFIX }:userCode:userCode` ) ; adapter [ 'uidKeyFor' ] = jest . fn ( ) ; adapter [ 'uidKeyFor' ] . mockReturnValue ( `${ OIDC_PROVIDER_REDIS_PREFIX }:uid:uid` ) ; adapter [ 'grantKeyFor' ] = jest . fn ( ) ; adapter [ 'grantKeyFor' ] . mockReturnValue ( `${ OIDC_PROVIDER_REDIS_PREFIX }:grant:grantId` ) ; await adapter . upsert ( idMock , payload , expiresIn ) ; expect ( multiMock . set ) . toHaveBeenCalledTimes ( SET_CALL_COUNT ) ; expect ( multiMock . rpush ) . toHaveBeenCalledTimes ( RPUSH_CALL_COUNT ) ; expect ( multiMock . expire ) . toHaveBeenCalledTimes ( EXPIRE_CALL_COUNT ) ; expect ( adapter [ 'userCodeKeyFor' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( adapter [ 'userCodeKeyFor' ] ) . toHaveBeenCalledWith ( 'userCode' ) ; expect ( adapter [ 'uidKeyFor' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( adapter [ 'uidKeyFor' ] ) . toHaveBeenCalledWith ( 'uid' ) ; expect ( adapter [ 'grantKeyFor' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( adapter [ 'grantKeyFor' ] ) . toHaveBeenCalledWith ( 'grantId' ) ; } ) ; it ( 'should throw an error if exec fails ' , async ( ) => { const error = new Error ( 'exec failed' ) ; multiMock . exec . mockRejectedValueOnce ( error ) ; expect ( adapter . upsert ( idMock , defaultPayload ) ) . rejects . toThrow ( error ) ; } ) ; } ) ; describe ( 'addSetAndExpireOnMulti' , ( ) => { it ( 'should call set and expires if key is defined' , ( ) => { const keyMock = 'foo' ; const idMock = 'bar' ; const expiresIn = 6000 ; adapter [ 'addSetAndExpireOnMulti' ] ( keyMock , idMock , expiresIn , multiMock ) ; expect ( multiMock . set ) . toHaveBeenCalledTimes ( 1 ) ; expect ( multiMock . set ) . toHaveBeenCalledWith ( 'foo' , 'bar' ) ; expect ( multiMock . expire ) . toHaveBeenCalledTimes ( 1 ) ; expect ( multiMock . expire ) . toHaveBeenCalledWith ( `foo` , 6000 ) ; } ) ; } ) ; describe ( 'saveGrantId' , ( ) => { it ( 'should call rpush and not expire for grantId' , async ( ) => { const grantId = 'grantId' ; const keyMock = 'foo' ; const expiresIn = 0 ; adapter [ 'grantKeyFor' ] = jest . fn ( ) ; adapter [ 'grantKeyFor' ] . mockReturnValue ( `${ OIDC_PROVIDER_REDIS_PREFIX }:grant:grantId` ) ; await adapter [ 'saveGrantId' ] ( multiMock , grantId , keyMock , expiresIn ) ; expect ( multiMock . rpush ) . toHaveBeenCalledTimes ( 1 ) ; expect ( multiMock . rpush ) . toHaveBeenCalledWith ( `${ OIDC_PROVIDER_REDIS_PREFIX }:grant:grantId` , 'foo' ) ; expect ( multiMock . expire ) . toHaveBeenCalledTimes ( 0 ) ; expect ( adapter [ 'grantKeyFor' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( adapter [ 'grantKeyFor' ] ) . toHaveBeenCalledWith ( 'grantId' ) ; } ) ; it ( 'should call rpush and expire for grantId' , async ( ) => { const grantId = 'grantId' ; const keyMock = 'foo' ; const expiresIn = 6000 ; adapter [ 'grantKeyFor' ] = jest . fn ( ) ; adapter [ 'grantKeyFor' ] . mockReturnValue ( `${ OIDC_PROVIDER_REDIS_PREFIX }:grant:grantId` ) ; await adapter [ 'saveGrantId' ] ( multiMock , grantId , keyMock , expiresIn ) ; expect ( multiMock . rpush ) . toHaveBeenCalledTimes ( 1 ) ; expect ( multiMock . rpush ) . toHaveBeenCalledWith ( `${ OIDC_PROVIDER_REDIS_PREFIX }:grant:grantId` , 'foo' ) ; expect ( multiMock . expire ) . toHaveBeenCalledTimes ( 1 ) ; expect ( multiMock . expire ) . toHaveBeenCalledWith ( `${ OIDC_PROVIDER_REDIS_PREFIX }:grant:grantId` , 6000 ) ; expect ( adapter [ 'grantKeyFor' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( adapter [ 'grantKeyFor' ] ) . toHaveBeenCalledWith ( 'grantId' ) ; } ) ; } ) ; describe ( 'saveKey' , ( ) => { it ( 'Should throw if JSON.stringiy fails' , async ( ) => { const payload = { foo : 'bar' , circularRef : null } ; const keyMock = 'foo' ; payload . circularRef = payload ; expect ( ( ) => adapter [ 'saveKey' ] ( multiMock , keyMock , payload ) ) . toThrow ( OidcProviderStringifyPayloadForRedisException ) ; } ) ; it ( 'should call hmset if name is in consumable var' , async ( ) => { const authorizationCodeAdapter = new OidcProviderRedisAdapter ( loggerMock , redisMock as unknown as Redis , ServiceProviderAdapterMock , 'AuthorizationCode' ) ; const keyMock = 'foo' ; const defaultPayload = { } ; authorizationCodeAdapter [ 'saveKey' ] ( multiMock , keyMock , defaultPayload ) ; expect ( multiMock . set ) . toHaveBeenCalledTimes ( 0 ) ; expect ( multiMock . hmset ) . toHaveBeenCalledTimes ( 1 ) ; expect ( multiMock . hmset ) . toHaveBeenCalledWith ( 'foo' , { payload : '{}' , } ) ; } ) ; it ( 'should call set if name is not in consumable var' , async ( ) => { const key = 'foo' ; const defaultPayload = { } ; adapter [ 'saveKey' ] ( multiMock , key , defaultPayload ) ; expect ( multiMock . hmset ) . toHaveBeenCalledTimes ( 0 ) ; expect ( multiMock . set ) . toHaveBeenCalledTimes ( 1 ) ; expect ( multiMock . set ) . toHaveBeenCalledWith ( 'foo' , '{}' ) ; } ) ; } ) ; describe ( 'uidKeyFor' , ( ) => { it ( 'Should return null if no id is provide' , ( ) => { const uidMock = '' ; const result = adapter [ 'uidKeyFor' ] ( uidMock ) ; expect ( result ) . toEqual ( null ) ; } ) ; it ( 'Should return a valid key if id is provide' , ( ) => { const uidMock = 'foo' ; const result = adapter [ 'uidKeyFor' ] ( uidMock ) ; expect ( result ) . toEqual ( `${ OIDC_PROVIDER_REDIS_PREFIX }:uid:foo` ) ; } ) ; } ) ; describe ( 'userCodeKeyFor' , ( ) => { it ( 'Should return null if no id is provide' , ( ) => { const codeMock = '' ; const result = adapter [ 'userCodeKeyFor' ] ( codeMock ) ; expect ( result ) . toEqual ( null ) ; } ) ; it ( 'Should return a valid key if id is provide' , ( ) => { const codeMock = 'foo' ; const result = adapter [ 'userCodeKeyFor' ] ( codeMock ) ; expect ( result ) . toEqual ( `${ OIDC_PROVIDER_REDIS_PREFIX }:userCode:foo` ) ; } ) ; } ) ; describe ( 'grantKeyFor' , ( ) => { it ( 'Should return null if no id is provide' , ( ) => { const idMock = '' ; const result = adapter [ 'grantKeyFor' ] ( idMock ) ; expect ( result ) . toEqual ( null ) ; } ) ; it ( 'Should return a valid key if id is provide' , ( ) => { const idMock = 'foo' ; const result = adapter [ 'grantKeyFor' ] ( idMock ) ; expect ( result ) . toEqual ( `${ OIDC_PROVIDER_REDIS_PREFIX }:grant:foo` ) ; } ) ; } ) ; describe ( 'findServiceProvider' , ( ) => { it ( 'should call serviceProvider getById' , async ( ) => { const id = 'greatId' ; adapter [ 'findServiceProvider' ] ( id ) ; expect ( ServiceProviderAdapterMock . getById ) . toHaveBeenCalledTimes ( 1 ) ; expect ( ServiceProviderAdapterMock . getById ) . toHaveBeenCalledWith ( id ) ; } ) ; it ( 'should call the logger' , async ( ) => { const spId = 'greatId' ; const sp = { name : 'greatFS' } ; ServiceProviderAdapterMock . getById . mockResolvedValueOnce ( sp ) ; await adapter [ 'findServiceProvider' ] ( spId ) ; expect ( loggerMock . trace ) . toHaveBeenCalledTimes ( 1 ) ; expect ( loggerMock . trace ) . toHaveBeenCalledWith ( { spId , sp } ) ; } ) ; it ( 'should return the found service provider' , async ( ) => { const id = 'greatId' ; const sp = { name : 'greatFS' } ; ServiceProviderAdapterMock . getById . mockResolvedValueOnce ( sp ) ; const result = await adapter [ 'findServiceProvider' ] ( id ) ; expect ( result ) . toEqual ( sp ) ; } ) ; } ) ; describe ( 'findInRedis' , ( ) => { it ( 'should call hgetall rather than get if name is in consumable var' , async ( ) => { const authorizationCodeAdapter = new OidcProviderRedisAdapter ( loggerMock , redisMock as unknown as Redis , ServiceProviderAdapterMock , 'AuthorizationCode' ) ; const idMock = 'foo' ; await authorizationCodeAdapter [ 'findInRedis' ] ( idMock ) ; expect ( redisMock . hgetall ) . toHaveBeenCalledTimes ( 1 ) ; expect ( redisMock . hgetall ) . toHaveBeenCalledWith ( `${ OIDC_PROVIDER_REDIS_PREFIX }:AuthorizationCode:foo` ) ; expect ( redisMock . get ) . toHaveBeenCalledTimes ( 0 ) ; } ) ; it ( 'should call get rather than hgetall if name is not in consumable var' , async ( ) => { const idMock = 'foo' ; await adapter [ 'findInRedis' ] ( idMock ) ; expect ( redisMock . get ) . toHaveBeenCalledTimes ( 1 ) ; expect ( redisMock . get ) . toHaveBeenCalledWith ( `${ OIDC_PROVIDER_REDIS_PREFIX }:testAdapterName:foo` ) ; expect ( redisMock . hgetall ) . toHaveBeenCalledTimes ( 0 ) ; } ) ; it ( 'should return undefined if response is empty' , async ( ) => { const idMock = 'foo' ; redisMock . get . mockResolvedValue ( null ) ; const result = await adapter [ 'findInRedis' ] ( idMock ) ; expect ( result ) . toBe ( undefined ) ; } ) ; it ( 'should return an object parsed from JSON if response is a string' , async ( ) => { const idMock = 'foo' ; redisMock . get . mockResolvedValue ( '{"foo":"bar"}' ) ; adapter [ 'parsedPayload' ] = jest . fn ( ) ; adapter [ 'parsedPayload' ] . mockReturnValue ( { foo : 'bar' } ) ; const result = await adapter [ 'findInRedis' ] ( idMock ) ; expect ( result ) . toEqual ( { foo : 'bar' } ) ; expect ( adapter [ 'parsedPayload' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( adapter [ 'parsedPayload' ] ) . toHaveBeenCalledWith ( '{"foo":"bar"}' ) ; } ) ; it ( 'should return a merged object if response is an object' , async ( ) => { const idMock = 'foo' ; redisMock . get . mockResolvedValue ( { payload : '{"fizz":"buzz"}' , foo : 'bar' , } ) ; adapter [ 'parsedPayload' ] = jest . fn ( ) ; adapter [ 'parsedPayload' ] . mockReturnValue ( { fizz : 'buzz' } ) ; const result = await adapter [ 'findInRedis' ] ( idMock ) ; expect ( result ) . toEqual ( { fizz : 'buzz' , foo : 'bar' } ) ; expect ( adapter [ 'parsedPayload' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( adapter [ 'parsedPayload' ] ) . toHaveBeenCalledWith ( '{"fizz":"buzz"}' ) ; } ) ; } ) ; describe ( 'find' , ( ) => { const id = 'greatId' ; it ( 'should call the logger' , async ( ) => { adapter . find ( id ) ; expect ( loggerMock . debug ) . toHaveBeenCalledTimes ( 1 ) ; expect ( loggerMock . debug ) . toHaveBeenCalledWith ( 'Find' ) ; } ) ; it ( 'should return found Service Provider if the contextName is client' , async ( ) => { adapter [ 'contextName' ] = 'Client' ; adapter [ 'findServiceProvider' ] = jest . fn ( ) ; adapter . find ( id ) ; expect ( adapter [ 'findServiceProvider' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( adapter [ 'findServiceProvider' ] ) . toHaveBeenCalledWith ( id ) ; } ) ; it ( 'should return findInRedis if the contextName is not client' , async ( ) => { adapter [ 'findInRedis' ] = jest . fn ( ) ; adapter . find ( id ) ; expect ( adapter [ 'findInRedis' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( adapter [ 'findInRedis' ] ) . toHaveBeenCalledWith ( id ) ; } ) ; } ) ; describe ( 'parsedPayload' , ( ) => { it ( 'should return a merged object if response is an object' , async ( ) => { const payloadMock = '{"fizz":"buzz"}' ; const result = adapter [ 'parsedPayload' ] ( payloadMock ) ; expect ( result ) . toEqual ( { fizz : 'buzz' } ) ; } ) ; it ( 'should throw if raw response can not be JSON parsed' , async ( ) => { const payloadMock = 'not so much json' ; expect ( ( ) => adapter [ 'parsedPayload' ] ( payloadMock ) ) . toThrow ( OidcProviderParseRedisResponseException ) ; } ) ; } ) ; describe ( 'findByUid' , ( ) => { it ( 'should call find with the result of redis.get' , async ( ) => { const idMock = 'foo' ; adapter . find = jest . fn ( ) ; const redisGetResolvedValue = Symbol ( 'redisGetResolvedValue' ) ; redisMock . get . mockResolvedValueOnce ( redisGetResolvedValue ) ; await adapter . findByUid ( idMock ) ; expect ( redisMock . get ) . toHaveBeenCalledTimes ( 1 ) ; expect ( redisMock . get ) . toHaveBeenCalledWith ( `${ OIDC_PROVIDER_REDIS_PREFIX }:uid:foo` ) ; expect ( adapter . find ) . toHaveBeenCalledTimes ( 1 ) ; expect ( adapter . find ) . toHaveBeenCalledWith ( redisGetResolvedValue ) ; } ) ; } ) ; describe ( 'findByUserCode' , ( ) => { it ( 'should call find with the result of redis.get' , async ( ) => { const idMock = 'foo' ; adapter . find = jest . fn ( ) ; const redisGetResolvedValue = Symbol ( 'redisGetResolvedValue' ) ; redisMock . get . mockResolvedValueOnce ( redisGetResolvedValue ) ; await adapter . findByUserCode ( idMock ) ; expect ( redisMock . get ) . toHaveBeenCalledTimes ( 1 ) ; expect ( redisMock . get ) . toHaveBeenCalledWith ( `${ OIDC_PROVIDER_REDIS_PREFIX }:userCode:foo` ) ; expect ( adapter . find ) . toHaveBeenCalledTimes ( 1 ) ; expect ( adapter . find ) . toHaveBeenCalledWith ( redisGetResolvedValue ) ; } ) ; } ) ; describe ( 'destroy' , ( ) => { it ( 'should call del' , async ( ) => { const idMock = 'foo' ; await adapter . destroy ( idMock ) ; expect ( redisMock . del ) . toHaveBeenCalledTimes ( 1 ) ; expect ( redisMock . del ) . toHaveBeenCalledWith ( `${ OIDC_PROVIDER_REDIS_PREFIX }:testAdapterName:foo` ) ; } ) ; } ) ; describe ( 'revokeByGrantId' , ( ) => { it ( 'should call redis multi' , async ( ) => { const idMock = 'foo' ; await adapter . revokeByGrantId ( idMock ) ; expect ( redisMock . multi ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should call redis lrange' , async ( ) => { const idMock = 'foo' ; await adapter . revokeByGrantId ( idMock ) ; expect ( redisMock . lrange ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( 'should call muti.del for each token found by lrange and one time for grant' , async ( ) => { const idMock = 'foo' ; const CALL_COUNT = 4 ; await adapter . revokeByGrantId ( idMock ) ; expect ( multiMock . del ) . toHaveBeenCalledTimes ( CALL_COUNT ) ; } ) ; it ( 'should throw an error if exec fails ' , async ( ) => { const idMock = 'foo' ; const errorMock = new Error ( 'exec failed' ) ; multiMock . exec . mockRejectedValueOnce ( errorMock ) ; expect ( adapter . revokeByGrantId ( idMock ) ) . rejects . toThrow ( errorMock ) ; } ) ; } ) ; describe ( 'consume' , ( ) => { it ( 'should cann redis.hset' , async ( ) => { const idMock = 'foo' ; await adapter . consume ( idMock ) ; expect ( redisMock . hset ) . toHaveBeenCalledTimes ( 1 ) ; expect ( redisMock . hset ) . toHaveBeenCalledWith ( `${ OIDC_PROVIDER_REDIS_PREFIX }:testAdapterName:foo` , 'consumed' , expect . any ( Number ) ) ; } ) ; } ) ; } ) ;