import { mocked } from 'jest-mock' ; import { encode } from 'querystring' ; import { Test , TestingModule } from '@nestjs/testing' ; import { ConfigService } from '@fc/config' ; import { CryptographyService } from '@fc/cryptography' ; import { IdentityProviderAdapterEnvService } from '@fc/identity-provider-adapter-env' ; import { LoggerService } from '@fc/logger' ; import { IdentityProviderMetadata } from '@fc/oidc' ; import { OidcClientService } from '@fc/oidc-client' ; import { SessionNotFoundException , SessionService } from '@fc/session' ; import { MockServiceProviderTokenRevocationException , MockServiceProviderUserinfoException , } from '../exceptions' ; import { MockServiceProviderController } from './mock-service-provider.controller' ; jest . mock ( 'querystring' , ( ) => ( { encode : jest . fn ( ) , } ) ) ; describe ( 'MockServiceProviderController' , ( ) => { let controller ; let res ; let req ; let query ; const oidcClientServiceMock = { utils : { getAuthorizeUrl : jest . fn ( ) , getTokenSet : jest . fn ( ) , getUserInfo : jest . fn ( ) , wellKnownKeys : jest . fn ( ) , buildAuthorizeParameters : jest . fn ( ) , revokeToken : jest . fn ( ) , } , getTokenFromProvider : jest . fn ( ) , getUserInfosFromProvider : jest . fn ( ) , getEndSessionUrlFromProvider : jest . fn ( ) , } ; const loggerServiceMock = { setContext : jest . fn ( ) , trace : jest . fn ( ) , } as unknown as LoggerService ; const oidcErrorMock = { error : 'error' , error_description : 'error_description' , } ; const sessionServiceMock = { set : jest . fn ( ) , get : jest . fn ( ) , } ; const randomStringMock = 'randomStringMockValue' ; const interactionIdMock = 'interactionIdMockValue' ; const nonceMock = randomStringMock ; const idpStateMock = 'idpStateMockValue' ; const idpNonceMock = 'idpNonceMockValue' ; const idpIdTokenMock = 'idpIdTokenMockValue' ; const idpIdMock = 'idpIdMockValue' ; const sessionDataMock = { interactionId : interactionIdMock , idpNonce : idpNonceMock , idpState : idpStateMock , idpIdToken : idpIdTokenMock , idpId : idpIdMock , } ; const cryptographyMock = { genRandomString : jest . fn ( ) , } ; const configMock = { get : jest . fn ( ) , } ; const scopeMock = 'openid profile' ; const acrMock = 'acrMock' ; const claimsMock = 'claimsMock' ; const identityProviderMock = { getList : jest . fn ( ) , } ; const identityProviderMinimum = { client : { redirect_uris : [ 'https://foo.bar.com/buz' ] , uid : 'providerUidMock' , client_id : 'mock client_id' , } , } ; const identityProviderFull = { uid : 'providerUidMock' , title : 'envIssuer Title' , name : 'envIssuer' , display : true , active : true , discovery : true , discoveryUrl : 'https://core-fcp-high.docker.dev-franceconnect.fr/api/v2/.well-known/openid-configuration' , issuer : { jwks_uri : 'https://fsp1v2.docker.dev-franceconnect.fr/jwks_uri' , } , client : { client_id : 'mock client_id' , client_secret : '7vhnwzo1yUVOJT9GJ91gD5oid56effu1' , id_token_signed_response_alg : 'ES256' , post_logout_redirect_uris : [ 'https://fsp1v2.docker.dev-franceconnect.fr/logout-callback' , ] , redirect_uris : [ 'https://foo.bar.com/buz' ] , response_types : [ 'code' ] , token_endpoint_auth_method : 'client_secret_post' , revocation_endpoint_auth_method : 'client_secret_post' , id_token_encrypted_response_alg : 'RSA-OAEP' , id_token_encrypted_response_enc : 'A256GCM' , userinfo_encrypted_response_alg : 'RSA-OAEP' , userinfo_encrypted_response_enc : 'A256GCM' , userinfo_signed_response_alg : 'ES256' , } , } ; const identityProviderList = [ identityProviderMinimum , identityProviderFull ] ; const interactionParametersMock = { authorizationUrl : 'authorizationUrl' , params : { state : 'stateMock' , nonce : 'nonceMock' , } , } ; const queryStringEncodeMock = mocked ( encode ) ; beforeEach ( async ( ) => { jest . resetAllMocks ( ) ; jest . restoreAllMocks ( ) ; const module = await Test . createTestingModule ( { controllers : [ MockServiceProviderController ] , providers : [ OidcClientService , LoggerService , SessionService , CryptographyService , ConfigService , IdentityProviderAdapterEnvService , ] , } ) . overrideProvider ( ConfigService ) . useValue ( configMock ) . overrideProvider ( OidcClientService ) . useValue ( oidcClientServiceMock ) . overrideProvider ( LoggerService ) . useValue ( loggerServiceMock ) . overrideProvider ( SessionService ) . useValue ( sessionServiceMock ) . overrideProvider ( CryptographyService ) . useValue ( cryptographyMock ) . overrideProvider ( IdentityProviderAdapterEnvService ) . useValue ( identityProviderMock ) . compile ( ) ; controller = module . get < MockServiceProviderController > ( MockServiceProviderController ) ; res = { redirect : jest . fn ( ) , } ; req = { fc : { interactionId : interactionIdMock , } , query : { firstQueryParam : 'first' , secondQueryParam : 'second' , } , params : { providerUid : 'secretProviderUid' , } , } ; query = { } ; oidcClientServiceMock . utils . buildAuthorizeParameters . mockReturnValue ( { state : idpStateMock , nonce : nonceMock , providerUid : 'providerUidMock' , acr_values : 'acrMock' , claims : 'claimsMock' , } ) ; sessionServiceMock . get . mockResolvedValue ( sessionDataMock ) ; cryptographyMock . genRandomString . mockReturnValue ( randomStringMock ) ; configMock . get . mockReturnValue ( { scope : scopeMock , acr : acrMock , claims : claimsMock , urlPrefix : '/api/v2' , defaultAcrValue : 'eidas2' , } ) ; identityProviderMock . getList . mockResolvedValue ( identityProviderList ) ; } ) ; it ( 'should be defined' , ( ) => { expect ( controller ) . toBeDefined ( ) ; } ) ; describe ( 'index' , ( ) => { beforeEach ( ( ) => { controller [ 'getInteractionParameters' ] = jest . fn ( ) . mockResolvedValue ( interactionParametersMock ) ; } ) ; it ( "Should throw if the session can't be initialized" , async ( ) => { sessionServiceMock . set . mockRejectedValueOnce ( new Error ( 'test' ) ) ; await expect ( controller . index ( sessionServiceMock ) ) . rejects . toThrow ( ) ; } ) ; it ( 'Should return front title' , async ( ) => { sessionServiceMock . set . mockResolvedValueOnce ( undefined ) ; const result = await controller . index ( sessionServiceMock ) ; expect ( result ) . toEqual ( expect . objectContaining ( { titleFront : 'Mock Service Provider' , } ) ) ; } ) ; it ( 'Should return default ACR value' , async ( ) => { sessionServiceMock . set . mockResolvedValueOnce ( undefined ) ; const result = await controller . index ( sessionServiceMock ) ; expect ( result ) . toEqual ( expect . objectContaining ( { defaultAcrValue : 'eidas2' , } ) ) ; } ) ; } ) ; describe ( 'verify' , ( ) => { it ( 'Should call session.get' , async ( ) => { await controller . getVerify ( sessionServiceMock ) ; expect ( sessionServiceMock . get ) . toHaveBeenCalledTimes ( 1 ) ; expect ( sessionServiceMock . get ) . toHaveBeenCalledWith ( ) ; } ) ; it ( 'Should return session data and hardcoded title' , async ( ) => { const result = await controller . getVerify ( sessionServiceMock ) ; expect ( result ) . toEqual ( { titleFront : 'Mock Service Provider - Login Callback' , ... sessionDataMock , } ) ; } ) ; } ) ; describe ( 'error' , ( ) => { it ( 'Should return error' , async ( ) => { const queryMock = { error : 'error' , error_description : 'Error description' , } ; const result = await controller . error ( queryMock ) ; expect ( result ) . toEqual ( { titleFront : "Mock service provider - Erreur lors de l'authentification" , error : 'error' , error_description : 'Error description' , } ) ; } ) ; } ) ; describe ( 'logout' , ( ) => { const postLogoutRedirectUriMock = 'https://postLogoutRedirectUriMock' ; const endSessionUrlMock = `https://endSessionUrlMockMock?id_token_hint=${ idpIdTokenMock }&post_logout_redirect_uri=${ postLogoutRedirectUriMock }&state=${ idpStateMock }` ; beforeEach ( ( ) => { sessionServiceMock . get . mockReturnValueOnce ( sessionDataMock ) ; oidcClientServiceMock . getEndSessionUrlFromProvider . mockResolvedValueOnce ( endSessionUrlMock ) ; } ) ; it ( 'should call session.get() to retrieves idp current cinematic informations' , async ( ) => { await controller . logout ( res , sessionServiceMock , postLogoutRedirectUriMock ) ; expect ( sessionServiceMock . get ) . toHaveBeenCalledTimes ( 1 ) ; expect ( sessionServiceMock . get ) . toHaveBeenCalledWith ( ) ; } ) ; it ( 'should call clientService.getEndSessionUrlFromProvider() to build the endSessionUrl' , async ( ) => { await controller . logout ( res , sessionServiceMock , postLogoutRedirectUriMock ) ; expect ( oidcClientServiceMock . getEndSessionUrlFromProvider ) . toHaveBeenCalledTimes ( 1 ) ; expect ( oidcClientServiceMock . getEndSessionUrlFromProvider ) . toHaveBeenCalledWith ( idpIdMock , idpStateMock , idpIdTokenMock , postLogoutRedirectUriMock ) ; } ) ; it ( 'should redirect on the logout callback controller' , async ( ) => { await controller . logout ( res , sessionServiceMock , postLogoutRedirectUriMock ) ; expect ( res . redirect ) . toHaveBeenCalledTimes ( 1 ) ; expect ( res . redirect ) . toHaveBeenCalledWith ( endSessionUrlMock ) ; } ) ; } ) ; describe ( 'revocationToken' , ( ) => { it ( 'should display success page when token is revoked' , async ( ) => { const providerUid = 'envIssuer' ; const body = { accessToken : 'access_token' } ; const result = await controller . revocationToken ( res , body ) ; expect ( oidcClientServiceMock . utils . revokeToken ) . toHaveBeenCalledTimes ( 1 ) ; expect ( oidcClientServiceMock . utils . revokeToken ) . toHaveBeenCalledWith ( body . accessToken , providerUid ) ; expect ( result ) . toEqual ( { accessToken : 'access_token' , titleFront : 'Mock Service Provider - Token révoqué' , } ) ; } ) ; it ( 'should redirect to the error page if revokeToken throw an error' , async ( ) => { oidcClientServiceMock . utils . revokeToken . mockRejectedValue ( oidcErrorMock ) ; const body = { accessToken : 'access_token' } ; await controller . revocationToken ( res , body ) ; expect ( res . redirect ) . toHaveBeenCalledTimes ( 1 ) ; expect ( res . redirect ) . toHaveBeenCalledWith ( '/error?error=error&error_description=error_description' ) ; } ) ; it ( 'Should throw mock service provider revoke token exception if error is not an instance of OPError' , ( ) => { const unknowError = { foo : 'bar' } ; const body = { accessToken : 'access_token' } ; oidcClientServiceMock . utils . revokeToken . mockRejectedValue ( unknowError ) ; expect ( async ( ) => await controller . revocationToken ( res , body ) ) . rejects . toThrow ( MockServiceProviderTokenRevocationException ) ; } ) ; } ) ; describe ( 'retrieveUserinfo' , ( ) => { it ( 'should retrieve and display userinfo on userinfo page' , async ( ) => { const providerUid = 'envIssuer' ; const body = { accessToken : 'access_token' } ; oidcClientServiceMock . utils . getUserInfo . mockReturnValueOnce ( { sub : '1' , given_name : 'given_name' , } ) ; const result = await controller . retrieveUserinfo ( res , body ) ; expect ( oidcClientServiceMock . utils . getUserInfo ) . toHaveBeenCalledTimes ( 1 ) ; expect ( oidcClientServiceMock . utils . getUserInfo ) . toHaveBeenCalledWith ( body . accessToken , providerUid ) ; expect ( result ) . toEqual ( { accessToken : 'access_token' , titleFront : 'Mock Service Provider - Userinfo' , idpIdentity : { sub : '1' , given_name : 'given_name' , } , } ) ; } ) ; it ( 'should redirect to the error page if getUserInfo throw an error' , async ( ) => { const body = { accessToken : 'access_token' } ; oidcClientServiceMock . utils . getUserInfo . mockRejectedValue ( oidcErrorMock ) ; await controller . retrieveUserinfo ( res , body ) ; expect ( res . redirect ) . toHaveBeenCalledTimes ( 1 ) ; expect ( res . redirect ) . toHaveBeenCalledWith ( '/error?error=error&error_description=error_description' ) ; } ) ; it ( 'Should throw mock service provider userinfo exception if error is not an instance of OPError' , ( ) => { oidcClientServiceMock . utils . getUserInfo . mockRejectedValue ( { } ) ; const body = { accessToken : 'access_token' } ; expect ( async ( ) => await controller . retrieveUserinfo ( res , body ) ) . rejects . toThrow ( MockServiceProviderUserinfoException ) ; } ) ; } ) ; describe ( 'logoutCallback' , ( ) => { it ( 'should redirect on the home page' , async ( ) => { await controller . logoutCallback ( res ) ; expect ( res . redirect ) . toHaveBeenCalledTimes ( 1 ) ; expect ( res . redirect ) . toHaveBeenCalledWith ( '/' ) ; } ) ; } ) ; describe ( 'getInteractionParameters' , ( ) => { const provider = identityProviderFull as IdentityProviderMetadata ; const urlMock = 'https://somewhere.com/foo' ; beforeEach ( ( ) => { oidcClientServiceMock . utils . getAuthorizeUrl . mockResolvedValue ( urlMock ) ; } ) ; it ( 'should call config.get' , async ( ) => { await controller [ 'getInteractionParameters' ] ( provider ) ; expect ( configMock . get ) . toBeCalledTimes ( 1 ) ; expect ( configMock . get ) . toBeCalledWith ( 'OidcClient' ) ; } ) ; it ( 'should call oidcClient.buildAuthorizeParameters' , async ( ) => { await controller [ 'getInteractionParameters' ] ( provider ) ; expect ( oidcClientServiceMock . utils . buildAuthorizeParameters ) . toBeCalledTimes ( 1 ) ; } ) ; it ( 'should call oidcClient.getAuthorizeUrl' , async ( ) => { await controller [ 'getInteractionParameters' ] ( provider ) ; expect ( oidcClientServiceMock . utils . getAuthorizeUrl ) . toBeCalledTimes ( 1 ) ; expect ( oidcClientServiceMock . utils . getAuthorizeUrl ) . toBeCalledWith ( { state : idpStateMock , scope : scopeMock , idpId : 'providerUidMock' , acr_values : 'acrMock' , nonce : nonceMock , claims : 'claimsMock' , } ) ; } ) ; it ( 'should return object containing results from various calls' , async ( ) => { const idp = identityProviderMinimum ; const result = await controller [ 'getInteractionParameters' ] ( provider ) ; expect ( result ) . toEqual ( { params : { state : idpStateMock , nonce : nonceMock , uid : 'providerUidMock' , acr : 'acrMock' , claims : 'claimsMock' , redirect_uri : idp . client . redirect_uris [ 0 ] , scope : scopeMock , authorization_endpoint : 'https://somewhere.com/foo' , client_id : idp . client . client_id , } , authorizationUrl : urlMock , } ) ; } ) ; } ) ; describe ( 'getOidcCallback' , ( ) => { const accessTokenMock = Symbol ( 'accesToken' ) ; const amrMock = Symbol ( 'amr' ) ; const identityMock = { sub : '1' , given_name : 'given_name' , } ; const tokenParamsMock = { state : idpStateMock , nonce : idpNonceMock , } ; const userInfoParamsMock = { accessToken : accessTokenMock , idpId : idpIdMock , } ; const identityExchangeMock = { idpIdentity : identityMock , idpAcr : acrMock , amr : amrMock , idpAccessToken : accessTokenMock , } ; const redirectMock = `/api/v2/interaction/${ interactionIdMock }/verify` ; beforeEach ( ( ) => { oidcClientServiceMock . getTokenFromProvider . mockReturnValueOnce ( { accessToken : accessTokenMock , acr : acrMock , amr : amrMock , } ) ; oidcClientServiceMock . getUserInfosFromProvider . mockReturnValueOnce ( identityMock ) ; } ) ; it ( 'should call res.redirect with an error if the query contains an error' , async ( ) => { const queryMock = { error : 'toto' , } ; const errorQueryMock = 'error=toto' ; queryStringEncodeMock . mockReturnValueOnce ( errorQueryMock ) ; const expectedErrorUriMock = `/error?${ errorQueryMock }` ; await controller . getOidcCallback ( req , res , queryMock , sessionServiceMock ) ; expect ( res . redirect ) . toHaveBeenCalledTimes ( 1 ) ; expect ( res . redirect ) . toHaveBeenCalledWith ( expectedErrorUriMock ) ; } ) ; it ( 'should call res.redirect with an error and error_description if the query contains an error and error_description' , async ( ) => { const queryMock = { error : 'toto' , error_description : 'toto42' , } ; const errorQueryMock = 'error=toto&error_description=toto42' ; queryStringEncodeMock . mockReturnValueOnce ( errorQueryMock ) ; const expectedErrorUriMock = `/error?${ errorQueryMock }` ; await controller . getOidcCallback ( req , res , queryMock , sessionServiceMock ) ; expect ( res . redirect ) . toHaveBeenCalledTimes ( 1 ) ; expect ( res . redirect ) . toHaveBeenCalledWith ( expectedErrorUriMock ) ; } ) ; it ( 'should throw an error if the session is not found' , async ( ) => { sessionServiceMock . get . mockReset ( ) . mockResolvedValueOnce ( undefined ) ; await expect ( controller . getOidcCallback ( req , res , query , sessionServiceMock ) ) . rejects . toThrow ( SessionNotFoundException ) ; } ) ; it ( 'should not call getTokenFromProvider if the query contains an error' , async ( ) => { const queryMock = { error : 'toto' , error_description : 'toto42' , } ; await controller . getOidcCallback ( req , res , queryMock , sessionServiceMock ) ; expect ( oidcClientServiceMock . getTokenFromProvider ) . toHaveBeenCalledTimes ( 0 ) ; } ) ; it ( 'should call token with providerId' , async ( ) => { await controller . getOidcCallback ( req , res , query , sessionServiceMock ) ; expect ( oidcClientServiceMock . getTokenFromProvider ) . toHaveBeenCalledTimes ( 1 ) ; expect ( oidcClientServiceMock . getTokenFromProvider ) . toHaveBeenCalledWith ( idpIdMock , tokenParamsMock , req ) ; } ) ; it ( 'should call userinfo with acesstoken, dto and context' , async ( ) => { await controller . getOidcCallback ( req , res , query , sessionServiceMock ) ; expect ( oidcClientServiceMock . getUserInfosFromProvider ) . toHaveBeenCalledTimes ( 1 ) ; expect ( oidcClientServiceMock . getUserInfosFromProvider ) . toHaveBeenCalledWith ( userInfoParamsMock , req ) ; } ) ; it ( 'should set session with identity result.' , async ( ) => { await controller . getOidcCallback ( req , res , query , sessionServiceMock ) ; expect ( sessionServiceMock . set ) . toHaveBeenCalledTimes ( 1 ) ; expect ( sessionServiceMock . set ) . toHaveBeenCalledWith ( identityExchangeMock ) ; } ) ; it ( 'should redirect user after token and userinfo received and saved' , async ( ) => { await controller . getOidcCallback ( req , res , query , sessionServiceMock ) ; expect ( res . redirect ) . toHaveBeenCalledTimes ( 1 ) ; expect ( res . redirect ) . toHaveBeenCalledWith ( redirectMock ) ; } ) ; } ) ; } ) ;