import { mocked } from 'jest-mock' ; import { Test , TestingModule } from '@nestjs/testing' ; import { validateDto } from '@fc/common' ; import { ConfigService } from '@fc/config' ; import { EidasLevelOfAssurances , EidasNameIdFormats , EidasResponse , } from '@fc/eidas' ; import { EidasCountryService , IEidasCountryElement } from '@fc/eidas-country' ; import { EidasToOidcService , OidcToEidasService } from '@fc/eidas-oidc-mapper' ; import { LoggerService } from '@fc/logger' ; import { OidcClientSession } from '@fc/oidc-client' ; import { OidcProviderService } from '@fc/oidc-provider' ; import { SessionService } from '@fc/session' ; import { AppConfig , EidasBridgeIdentityDto } from '../dto' ; import { EidasBridgeInvalidIdentityException } from '../exceptions' ; import { EuIdentityToFrController } from './eu-identity-to-fr.controller' ; jest . mock ( '@fc/common' , ( ) => ( { ... ( jest . requireActual ( '@fc/common' ) as any ) , validateDto : jest . fn ( ) , } ) ) ; const countryListMock = [ { iso : 'iso1Value' , name : 'name1Value' , icon : 'icon1Value' , } , { iso : 'iso2Value' , name : 'name2Value' , icon : 'icon2Value' , } , ] ; const availableListMock = [ 'iso1Value' , 'iso2Value' , ] ; describe ( 'EuIdentityToFrController' , ( ) => { let euIdentityToFrController ; const configMock = { apiOutputContentType : 'html' , name : 'notUsedValue' , urlPrefix : '' , countryIsoList : availableListMock , httpsOptions : { } , } ; const configServiceMock = { get : jest . fn ( ) , } ; const oidcProviderServiceMock = { getInteraction : jest . fn ( ) , finishInteraction : jest . fn ( ) , } ; const loggerServiceMock = { setContext : jest . fn ( ) , warn : jest . fn ( ) , trace : jest . fn ( ) , } as unknown as LoggerService ; const sessionServiceOidcMock = { set : jest . fn ( ) , get : jest . fn ( ) , } ; const sessionServiceEidasMock = { get : jest . fn ( ) , set : jest . fn ( ) , } ; const sessionDataMock = { spName : 'spNameMockValue' , } ; const res = { redirect : jest . fn ( ) , } ; const req = { body : { country : 'BE' , } , fc : { interactionId : 'interactionIdMock' , } , params : { uid : '1234456' , } , } ; const interactionMock = { uid : Symbol ( 'uidMockValue' ) , params : Symbol ( 'interactionMockValue' ) , } ; const oidcToEidasServiceMock = { mapPartialRequest : jest . fn ( ) , } ; const eidasToOidcServiceMock = { mapPartialResponseFailure : jest . fn ( ) , mapPartialResponseSuccess : jest . fn ( ) , } ; const eidasCountryServiceMock = { getListByIso : jest . fn ( ) , } ; const sessionServiceMock = { setAlias : jest . fn ( ) , } ; beforeEach ( async ( ) => { const app = await Test . createTestingModule ( { controllers : [ EuIdentityToFrController ] , providers : [ ConfigService , LoggerService , SessionService , OidcProviderService , OidcToEidasService , EidasToOidcService , EidasCountryService , SessionService , ] , } ) . overrideProvider ( ConfigService ) . useValue ( configServiceMock ) . overrideProvider ( LoggerService ) . useValue ( loggerServiceMock ) . overrideProvider ( SessionService ) . useValue ( sessionServiceOidcMock ) . overrideProvider ( OidcProviderService ) . useValue ( oidcProviderServiceMock ) . overrideProvider ( OidcToEidasService ) . useValue ( oidcToEidasServiceMock ) . overrideProvider ( EidasToOidcService ) . useValue ( eidasToOidcServiceMock ) . overrideProvider ( EidasCountryService ) . useValue ( eidasCountryServiceMock ) . overrideProvider ( SessionService ) . useValue ( sessionServiceMock ) . compile ( ) ; euIdentityToFrController = await app . get < EuIdentityToFrController > ( EuIdentityToFrController ) ; jest . resetAllMocks ( ) ; jest . restoreAllMocks ( ) ; configServiceMock . get . mockReturnValue ( configMock ) ; oidcProviderServiceMock . getInteraction . mockResolvedValue ( interactionMock ) ; sessionServiceOidcMock . get . mockResolvedValue ( sessionDataMock ) ; } ) ; describe ( 'getInteraction' , ( ) => { beforeEach ( ( ) => { eidasCountryServiceMock . getListByIso . mockResolvedValueOnce ( countryListMock ) ; } ) ; it ( 'should call oidcProvider.getInteraction' , async ( ) => { await euIdentityToFrController . getInteraction ( req , res , sessionServiceEidasMock , sessionServiceOidcMock ) ; expect ( oidcProviderServiceMock . getInteraction ) . toBeCalledTimes ( 1 ) ; expect ( oidcProviderServiceMock . getInteraction ) . toBeCalledWith ( req , res ) ; } ) ; it ( 'should call get the country list from the config' , async ( ) => { await euIdentityToFrController . getInteraction ( req , res , sessionServiceEidasMock , sessionServiceOidcMock ) ; expect ( configServiceMock . get ) . toBeCalledTimes ( 1 ) ; expect ( configServiceMock . get ) . toBeCalledWith ( 'App' ) ; } ) ; it ( 'should call session.get with interactionId' , async ( ) => { await euIdentityToFrController . getInteraction ( req , res , sessionServiceEidasMock , sessionServiceOidcMock ) ; expect ( sessionServiceOidcMock . get ) . toBeCalledTimes ( 1 ) ; expect ( sessionServiceOidcMock . get ) . toBeCalledWith ( ) ; } ) ; it ( 'should return an object with data from session and oidcProvider interaction' , async ( ) => { const result = await euIdentityToFrController . getInteraction ( req , res , sessionServiceEidasMock , sessionServiceOidcMock ) ; expect ( result ) . toStrictEqual ( { uid : interactionMock . uid , countryList : countryListMock , params : interactionMock . params , spName : sessionDataMock . spName , } ) ; } ) ; it ( 'should throw an error if country service failed' , async ( ) => { const errorMock = new Error ( 'Unknown error' ) ; eidasCountryServiceMock . getListByIso . mockReset ( ) . mockRejectedValueOnce ( errorMock ) ; await expect ( euIdentityToFrController . getInteraction ( req , res , sessionServiceEidasMock , sessionServiceOidcMock ) ) . rejects . toThrow ( errorMock ) ; } ) ; } ) ; describe ( 'finishInteraction()' , ( ) => { const successEidasMandatoryJsonMock = { id : '_BmPONbKyIB64fyNTQoyzZr_r5pXeyDGwUTS-bfo_zzhb_.Us9f.XZE2.mcqyM1u' , inResponseToId : '1602861970744' , issuer : 'https://eidas-fr.docker.dev-franceconnect.fr/EidasNode/ConnectorMetadata' , subject : '0123456' , subjectNameIdFormat : EidasNameIdFormats . UNSPECIFIED , levelOfAssurance : EidasLevelOfAssurances . SUBSTANTIAL , status : { failure : false , } , attributes : { personIdentifier : [ 'BE/FR/12345' ] , currentFamilyName : [ 'Garcia' ] , currentGivenName : [ 'javier' ] , dateOfBirth : [ '1964-12-31' ] , } , } ; const successOidcJson = { acr : 'eidas2' , userinfos : { sub : 'BE/FR/12345' , family_name : 'Garcia' , given_name : 'javier' , birthdate : '1964-12-31' , } , } ; const failureEidasMandatoryJsonMock = { id : '_BmPONbKyIB64fyNTQoyzZr_r5pXeyDGwUTS-bfo_zzhb_.Us9f.XZE2.mcqyM1u' , inResponseToId : '1602861970744' , issuer : 'https://eidas-fr.docker.dev-franceconnect.fr/EidasNode/ConnectorMetadata' , status : { failure : true , } , } ; const failureOidcJson = { error : 'error' , error_description : 'error_description' , } ; const buildRedirectUriErrorUrlMock = jest . fn ( ) ; const redirectUriErrorUrlMock = 'https://redirect-uri-error.url' ; let validateIdentityMock ; beforeEach ( ( ) => { validateIdentityMock = jest . spyOn < EuIdentityToFrController , any > ( euIdentityToFrController , 'validateIdentity' ) ; validateIdentityMock . mockResolvedValueOnce ( ) ; } ) ; it ( 'should get the eidas response from the session' , async ( ) => { sessionServiceEidasMock . get . mockResolvedValue ( { eidasResponse : successEidasMandatoryJsonMock , } ) ; eidasToOidcServiceMock . mapPartialResponseSuccess . mockReturnValue ( successOidcJson ) ; await euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( sessionServiceEidasMock . get ) . toHaveBeenCalledTimes ( 1 ) ; expect ( sessionServiceEidasMock . get ) . toHaveBeenCalledWith ( ) ; } ) ; describe ( 'eidas response is a success' , ( ) => { beforeEach ( ( ) => { sessionServiceEidasMock . get . mockResolvedValueOnce ( { eidasResponse : successEidasMandatoryJsonMock , } ) ; eidasToOidcServiceMock . mapPartialResponseSuccess . mockReturnValueOnce ( successOidcJson ) ; } ) ; it ( 'should not call getInteraction from oidcProvider' , async ( ) => { await euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( oidcProviderServiceMock . getInteraction ) . not . toHaveBeenCalled ( ) ; } ) ; it ( 'should not call mapPartialResponseFailure from eidasToOidc' , async ( ) => { await euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( eidasToOidcServiceMock . mapPartialResponseFailure ) . not . toHaveBeenCalled ( ) ; } ) ; it ( 'should not call mapPartialResponseFailure from eidasToOidc' , async ( ) => { await euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( eidasToOidcServiceMock . mapPartialResponseFailure ) . not . toHaveBeenCalled ( ) ; } ) ; it ( 'should not call mapPartialResponseFailure from eidasToOidc' , async ( ) => { await euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( buildRedirectUriErrorUrlMock ) . not . toHaveBeenCalled ( ) ; } ) ; it ( 'should call mapPartialResponseSuccess with the eidas response to get the partial oidc response' , async ( ) => { await euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( eidasToOidcServiceMock . mapPartialResponseSuccess ) . toHaveBeenCalledTimes ( 1 ) ; expect ( eidasToOidcServiceMock . mapPartialResponseSuccess ) . toHaveBeenCalledWith ( successEidasMandatoryJsonMock ) ; } ) ; it ( 'should failed if userinfos are wrong and validate identity failed' , async ( ) => { const errorMock = new Error ( 'Unknown Error' ) ; validateIdentityMock . mockReset ( ) . mockRejectedValueOnce ( errorMock ) ; await expect ( ( ) => euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ) . rejects . toThrow ( 'Unknown Error' ) ; expect ( eidasToOidcServiceMock . mapPartialResponseSuccess ) . toHaveBeenCalledTimes ( 1 ) ; expect ( sessionServiceOidcMock . set ) . toHaveBeenCalledTimes ( 0 ) ; } ) ; it ( 'should `set` to update the oidc session with the identity to send to the SP' , async ( ) => { const expectedUpdatedSession = { idpIdentity : { sub : successOidcJson . userinfos . sub } , spAcr : successOidcJson . acr , spIdentity : successOidcJson . userinfos , } ; await euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( sessionServiceOidcMock . set ) . toHaveBeenCalledTimes ( 1 ) ; expect ( sessionServiceOidcMock . set ) . toHaveBeenCalledWith ( expectedUpdatedSession ) ; } ) ; it ( 'should finish the oidc provider interaction passing req and res' , async ( ) => { await euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( oidcProviderServiceMock . finishInteraction ) . toHaveBeenCalledTimes ( 1 ) ; expect ( oidcProviderServiceMock . finishInteraction ) . toHaveBeenCalledWith ( req , res , sessionDataMock ) ; } ) ; it ( 'should return the promise from the finishInteraction call' , async ( ) => { const expectedPromise = new Promise ( ( resolves ) => { resolves ( true ) ; } ) ; oidcProviderServiceMock . finishInteraction . mockReturnValueOnce ( expectedPromise ) ; const result = euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( result ) . toStrictEqual ( expectedPromise ) ; } ) ; } ) ; describe ( 'eidas response is a failure' , ( ) => { const interactionMock = { params : { scope : 'openid' , acr : 'eidas2' } } ; beforeEach ( ( ) => { sessionServiceEidasMock . get . mockResolvedValueOnce ( { eidasResponse : failureEidasMandatoryJsonMock , } ) ; oidcProviderServiceMock . getInteraction . mockResolvedValueOnce ( interactionMock ) ; euIdentityToFrController [ 'buildRedirectUriErrorUrl' ] = buildRedirectUriErrorUrlMock ; } ) ; it ( 'should retrieves the interaction using req and res' , async ( ) => { await euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( oidcProviderServiceMock . getInteraction ) . toHaveBeenCalledTimes ( 1 ) ; expect ( oidcProviderServiceMock . getInteraction ) . toHaveBeenCalledWith ( req , res ) ; } ) ; it ( 'should call mapPartialResponseFailure with the eidas response to get the partial oidc response' , async ( ) => { await euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( eidasToOidcServiceMock . mapPartialResponseFailure ) . toHaveBeenCalledTimes ( 1 ) ; expect ( eidasToOidcServiceMock . mapPartialResponseFailure ) . toHaveBeenCalledWith ( failureEidasMandatoryJsonMock ) ; } ) ; it ( 'should call buildRedirectUriErrorUrl with with the params from the interaction and the oidcError returned by the mapper' , async ( ) => { eidasToOidcServiceMock . mapPartialResponseFailure . mockReturnValueOnce ( failureOidcJson ) ; await euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( buildRedirectUriErrorUrlMock ) . toHaveBeenCalledTimes ( 1 ) ; expect ( buildRedirectUriErrorUrlMock ) . toHaveBeenCalledWith ( interactionMock . params , failureOidcJson ) ; } ) ; it ( 'should redirect the user to the SP callback with the oidc error' , async ( ) => { eidasToOidcServiceMock . mapPartialResponseFailure . mockReturnValueOnce ( failureOidcJson ) ; buildRedirectUriErrorUrlMock . mockReturnValueOnce ( redirectUriErrorUrlMock ) ; await euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( res . redirect ) . toHaveBeenCalledTimes ( 1 ) ; expect ( res . redirect ) . toHaveBeenCalledWith ( redirectUriErrorUrlMock ) ; } ) ; it ( 'should return the promise from the res.redirect' , async ( ) => { eidasToOidcServiceMock . mapPartialResponseFailure . mockReturnValueOnce ( failureOidcJson ) ; const expectedPromise = new Promise ( ( resolves ) => resolves ( true ) ) ; res . redirect . mockReturnValueOnce ( expectedPromise ) ; const result = euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( result ) . toStrictEqual ( expectedPromise ) ; } ) ; it ( 'should not call mapPartialResponseSuccess' , async ( ) => { euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( eidasToOidcServiceMock . mapPartialResponseSuccess ) . not . toHaveBeenCalled ( ) ; } ) ; it ( 'should not call `sessionOidc.set()`' , async ( ) => { euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( sessionServiceOidcMock . set ) . not . toHaveBeenCalled ( ) ; } ) ; it ( 'should not call finishInteraction' , async ( ) => { euIdentityToFrController . finishInteraction ( req , res , sessionServiceOidcMock , sessionServiceEidasMock ) ; expect ( oidcProviderServiceMock . finishInteraction ) . not . toHaveBeenCalled ( ) ; } ) ; } ) ; } ) ; describe ( 'validateIdentity' , ( ) => { let validateDtoMock ; let identityMock ; beforeEach ( ( ) => { identityMock = { given_name : 'given_nameValue' , family_name : 'family_nameValue' , } ; validateDtoMock = mocked ( validateDto ) ; } ) ; it ( 'should succeed to validate identity' , async ( ) => { validateDtoMock . mockResolvedValueOnce ( [ ] ) ; await euIdentityToFrController [ 'validateIdentity' ] ( identityMock ) ; expect ( validateDtoMock ) . toHaveBeenCalledTimes ( 1 ) ; expect ( validateDtoMock ) . toHaveBeenCalledWith ( identityMock , EidasBridgeIdentityDto , { forbidNonWhitelisted : true , forbidUnknownValues : true , whitelist : true , } , { excludeExtraneousValues : true } ) ; } ) ; it ( 'should failed to validate identity' , async ( ) => { validateDtoMock . mockResolvedValueOnce ( [ 'Unknown Error' ] ) ; await expect ( euIdentityToFrController [ 'validateIdentity' ] ( identityMock ) ) . rejects . toThrow ( EidasBridgeInvalidIdentityException ) ; } ) ; } ) ; describe ( 'buildRedirectUriErrorUrl' , ( ) => { it ( 'should build a redirect uri with oidc params error' , ( ) => { const oidcErrorMock = { error : 'error' , error_description : 'error_description' , } ; const paramsMock = { redirect_uri : 'https://redirect.url' , state : 'texas' , } ; const expectedUrl = 'https://redirect.url?error=error&error_description=error_description&state=texas' ; const result = euIdentityToFrController [ 'buildRedirectUriErrorUrl' ] ( paramsMock , oidcErrorMock ) ; expect ( result ) . toStrictEqual ( expectedUrl ) ; } ) ; } ) ; describe ( 'redirectToFrNodeConnector' , ( ) => { it ( 'should return a status code and a url' , async ( ) => { const result = await euIdentityToFrController . redirectToFrNodeConnector ( req . body ) ; expect ( result ) . toEqual ( { statusCode : 302 , url : '/eidas-client/redirect-to-fr-node-connector?country=BE' , } ) ; } ) ; } ) ; } ) ;