import * as crypto from 'crypto' ; import { mocked } from 'jest-mock' ; import * as moment from 'moment' ; import { Test , TestingModule } from '@nestjs/testing' ; import { ConfigService } from '@fc/config' ; import { EidasInvalidTokenChecksumException , EidasOversizedTokenException , } from '../exceptions' ; import { LightProtocolCommonsService } from './light-protocol-commons.service' ; jest . mock ( 'moment' , ( ) => jest . fn ( ) ) ; describe ( 'LightProtocolCommonsService' , ( ) => { let service ; const issuer = 'yeltsA-kciR' ; const id = 'NGGYU' ; const date = '1969-07-20 20:17:40 000' ; const secretMock = '?v=dQw4w9WgXcQ' ; const maxTokenSize = 2048 ; const tokenDigestMock = 'fO57Zc0k+8SZQ4kxKSmgMbCNMXQSDFWOz2DJfaqt8+s=' ; const decodedTokenMock = `${ issuer }|${ id }|${ date }|${ tokenDigestMock }` ; const encodedTokenMock = Buffer . from ( decodedTokenMock , 'utf8' ) . toString ( 'base64' ) ; const mockConfigService = { get : jest . fn ( ) , } ; beforeEach ( async ( ) => { jest . clearAllMocks ( ) ; jest . resetAllMocks ( ) ; jest . restoreAllMocks ( ) ; const module = await Test . createTestingModule ( { providers : [ LightProtocolCommonsService , ConfigService ] , } ) . overrideProvider ( ConfigService ) . useValue ( mockConfigService ) . compile ( ) ; service = module . get < LightProtocolCommonsService > ( LightProtocolCommonsService ) ; } ) ; it ( 'should be defined' , ( ) => { expect ( service ) . toBeDefined ( ) ; } ) ; describe ( 'generateToken' , ( ) => { const formatMock = jest . fn ( ) ; beforeEach ( ( ) => { mocked ( moment , true ) . mockReturnValueOnce ( { format : formatMock , } as unknown as moment . Moment ) ; formatMock . mockReturnValueOnce ( date ) ; service [ 'generateTokenDigest' ] = jest . fn ( ) . mockReturnValueOnce ( tokenDigestMock ) ; } ) ; it ( 'should create a date with format "YYYY-MM-DD HH:mm:ss SSS"' , ( ) => { const expectedDate = undefined ; const expectedFormat = 'YYYY-MM-DD HH:mm:ss SSS' ; service . generateToken ( id , issuer , secretMock ) ; expect ( moment ) . toHaveBeenCalledTimes ( 1 ) ; expect ( moment ) . toHaveBeenCalledWith ( expectedDate ) ; expect ( formatMock ) . toHaveBeenCalledTimes ( 1 ) ; expect ( formatMock ) . toHaveBeenCalledWith ( expectedFormat ) ; } ) ; it ( 'should call moment with the date argument if present' , ( ) => { const expectedDate = new Date ( '2012-06-04' ) ; service . generateToken ( id , issuer , secretMock , expectedDate ) ; expect ( moment ) . toHaveBeenCalledTimes ( 1 ) ; expect ( moment ) . toHaveBeenCalledWith ( expectedDate ) ; } ) ; it ( 'should generate the token digest with the id, the issuer and the date extracted from the token and the secret' , ( ) => { service . generateToken ( id , issuer , secretMock ) ; expect ( service [ 'generateTokenDigest' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( service [ 'generateTokenDigest' ] ) . toHaveBeenCalledWith ( id , issuer , secretMock , date ) ; } ) ; it ( 'should create a base64 encoded light request with the issuer, id, date, and the signature' , ( ) => { const formatMock = jest . fn ( ) . mockReturnValueOnce ( date ) ; mocked ( moment , true ) . mockReturnValueOnce ( { format : formatMock , } as unknown as moment . Moment ) ; const result = service . generateToken ( id , issuer , secretMock ) ; expect ( result ) . toStrictEqual ( encodedTokenMock ) ; } ) ; } ) ; describe ( 'parseToken' , ( ) => { const toDateMock = jest . fn ( ) ; const toStringMock = jest . fn ( ) ; const fakeDate = 'By Osiris and Isis, this is a date, *o*, a date !' ; beforeEach ( ( ) => { mockConfigService . get . mockReturnValueOnce ( { maxTokenSize } ) ; service [ 'generateTokenDigest' ] = jest . fn ( ) . mockReturnValueOnce ( tokenDigestMock ) ; toDateMock . mockReturnValueOnce ( fakeDate ) ; mocked ( moment , true ) . mockReturnValueOnce ( { toDate : toDateMock , } as unknown as moment . Moment ) ; toStringMock . mockReturnValueOnce ( decodedTokenMock ) ; jest . spyOn ( Buffer , 'from' ) . mockReturnValueOnce ( { toString : toStringMock } as unknown as Buffer ) ; } ) ; it ( 'sould retrieve the maxTokenSize from the config' , ( ) => { service . parseToken ( encodedTokenMock , secretMock ) ; expect ( mockConfigService . get ) . toHaveBeenCalledTimes ( 1 ) ; expect ( mockConfigService . get ) . toHaveBeenCalledWith ( 'EidasLightProtocol' ) ; } ) ; it ( 'should decode the base64 to utf8' , ( ) => { service . parseToken ( encodedTokenMock , secretMock ) ; expect ( Buffer . from ) . toHaveBeenCalledTimes ( 1 ) ; expect ( Buffer . from ) . toHaveBeenCalledWith ( encodedTokenMock , 'base64' ) ; expect ( toStringMock ) . toHaveBeenCalledTimes ( 1 ) ; expect ( toStringMock ) . toHaveBeenCalledWith ( 'utf8' ) ; } ) ; it ( 'should generate the token digest with the id, the issuer and the date extracted from the token and the secret' , ( ) => { service . parseToken ( encodedTokenMock , secretMock ) ; expect ( service [ 'generateTokenDigest' ] ) . toHaveBeenCalledTimes ( 1 ) ; expect ( service [ 'generateTokenDigest' ] ) . toHaveBeenCalledWith ( id , issuer , secretMock , date ) ; } ) ; it ( 'should create a native date object from the string fromated "YYYY-MM-DD HH:mm:ss SSS"' , ( ) => { service . parseToken ( encodedTokenMock , secretMock ) ; expect ( moment ) . toHaveBeenCalledTimes ( 1 ) ; expect ( moment ) . toHaveBeenCalledWith ( date , 'YYYY-MM-DD HH:mm:ss SSS' ) ; expect ( toDateMock ) . toHaveBeenCalledTimes ( 1 ) ; expect ( toDateMock ) . toHaveBeenCalledWith ( ) ; } ) ; it ( 'should return the payload elements parsed but not the digest' , ( ) => { const expected = { id , issuer , date : fakeDate , } ; const result = service . parseToken ( encodedTokenMock , secretMock ) ; expect ( result ) . toStrictEqual ( expected ) ; } ) ; it ( 'should throw an EidasOversizedTokenException if the token size is over the config maxTokenSize' , ( ) => { mockConfigService . get . mockReset ( ) . mockReturnValueOnce ( { maxTokenSize : 0 } ) ; expect ( ( ) => service . parseToken ( encodedTokenMock , secretMock ) ) . toThrowError ( EidasOversizedTokenException ) ; } ) ; it ( 'should throw an EidasInvalidTokenChecksumException if the calculated digest does not equal the light token digest' , ( ) => { service [ 'generateTokenDigest' ] = jest . fn ( ) . mockReturnValueOnce ( 'Not the expected digest' ) ; expect ( ( ) => service . parseToken ( encodedTokenMock , secretMock ) ) . toThrowError ( EidasInvalidTokenChecksumException ) ; } ) ; } ) ; describe ( 'generateTokenDigest' , ( ) => { const mockFormat = jest . fn ( ) ; beforeEach ( ( ) => { mockFormat . mockReturnValueOnce ( date ) ; mocked ( moment , true ) . mockReturnValueOnce ( { format : mockFormat , } as unknown as moment . Moment ) ; } ) ; it ( 'should create a base64 digested sha256 signature hash with id, issuer, mockSecret, date and lightRequestSecret in this order' , ( ) => { const expectedHash = 'sha256' ; const expectedData = `${ id }|${ issuer }|${ date }|${ secretMock }` ; const expectedDigest = 'base64' ; const mockUpdate = jest . fn ( ) ; const mockDigest = jest . fn ( ) ; jest . spyOn ( crypto , 'createHash' ) . mockReturnValueOnce ( { update : mockUpdate , digest : mockDigest , } as unknown as crypto . Hash ) ; service . generateToken ( id , issuer , secretMock ) ; expect ( crypto . createHash ) . toHaveBeenCalledTimes ( 1 ) ; expect ( crypto . createHash ) . toHaveBeenCalledWith ( expectedHash ) ; expect ( mockUpdate ) . toHaveBeenCalledTimes ( 1 ) ; expect ( mockUpdate ) . toHaveBeenCalledWith ( expectedData ) ; expect ( mockDigest ) . toHaveBeenCalledTimes ( 1 ) ; expect ( mockDigest ) . toHaveBeenCalledWith ( expectedDigest ) ; } ) ; it ( 'should return the digest' , ( ) => { const result = service . generateToken ( id , issuer , secretMock ) ; expect ( result ) . toStrictEqual ( encodedTokenMock ) ; } ) ; } ) ; describe ( 'getLastElementInUrlOrUrn' , ( ) => { it ( 'should return all the string after the last slash' , ( ) => { const url = 'http://eidas.europa.eu/attributes/naturalperson/personIdentifier' ; const result = service [ 'getLastElementInUrlOrUrn' ] ( url ) ; expect ( result ) . toEqual ( 'personIdentifier' ) ; } ) ; it ( 'should return all the string after the last semicolon' , ( ) => { const urn = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified' ; const result = service [ 'getLastElementInUrlOrUrn' ] ( urn ) ; expect ( result ) . toEqual ( 'unspecified' ) ; } ) ; } ) ; } ) ;