import 'reflect-metadata' ; import { Context } from '@azure/functions' ; import { GuidGenerator , RestApiConfig , ServiceConfiguration } from 'common' ; import { ScanRequestReceivedMeasurements } from 'logger' ; import { HttpResponse , ScanDataProvider , ScanRunResponse , WebApiErrorCodes } from 'service-library' ; import { ScanRunBatchRequest } from 'storage-documents' ; import { IMock , It , Mock , Times } from 'typemoq' ; import { MockableLogger } from '../test-utilities/mockable-logger' ; import { ScanRequestController } from './scan-request-controller' ; interface DataItem { url : string ; } describe ( ScanRequestController , ( ) => { let scanRequestController ; let context ; let scanDataProviderMock ; let serviceConfigurationMock ; let loggerMock ; let guidGeneratorMock ; beforeEach ( ( ) => { context = < Context > ( < unknown > { req : { method : 'POST' , rawBody : `{ url: 'https://abc/path/' }` , headers : { } , query : { } , } , } ) ; context . req . query [ 'api-version' ] = '1.0' ; context . req . headers [ 'content-type' ] = 'application/json' ; scanDataProviderMock = Mock . ofType < ScanDataProvider > ( ) ; scanDataProviderMock . setup ( async ( o ) => o . writeScanRunBatchRequest ( It . isAny ( ) , It . isAny ( ) ) ) ; guidGeneratorMock = Mock . ofType ( GuidGenerator ) ; serviceConfigurationMock = Mock . ofType < ServiceConfiguration > ( ) ; serviceConfigurationMock . setup ( async ( s ) => s . getConfigValue ( 'restApiConfig' ) ) . returns ( async ( ) => { return { maxScanRequestBatchCount : 4 , minScanPriorityValue : - 10 , maxScanPriorityValue : 10 , } as RestApiConfig ; } ) ; loggerMock = Mock . ofType < MockableLogger > ( ) ; } ) ; function createScanRequestController ( contextReq ) { const controller = new ScanRequestController ( scanDataProviderMock . object , guidGeneratorMock . object , serviceConfigurationMock . object , loggerMock . object ) ; controller . context = contextReq ; return controller ; } function sortData < T extends DataItem > ( array ) { return array . sort ( ( a , b ) => ( a . url > b . url ? 1 : b . url > a . url ? - 1 : 0 ) ) ; } describe ( ScanRequestController , ( ) => { it ( 'rejects request with large payload' , async ( ) => { context . req . rawBody = JSON . stringify ( [ { url : '' } , { url : '' } , { url : '' } , { url : '' } , { url : '' } ] ) ; scanRequestController = createScanRequestController ( context ) ; await scanRequestController . handleRequest ( ) ; expect ( context . res ) . toEqual ( HttpResponse . getErrorResponse ( WebApiErrorCodes . requestBodyTooLarge ) ) ; } ) ; it ( 'rejects request invalid scan notify url' , async ( ) => { context . req . rawBody = JSON . stringify ( [ { url : 'https://abs/path/' , scanNotifyUrl : 'invalid-url' } ] ) ; scanRequestController = createScanRequestController ( context ) ; await scanRequestController . handleRequest ( ) ; expect ( context . res . body [ 0 ] . error ) . toEqual ( WebApiErrorCodes . invalidScanNotifyUrl . error ) ; } ) ; it ( "rejects request with incomplete 'site' or 'reportGroups' property" , async ( ) => { context . req . rawBody = JSON . stringify ( [ { url : 'https://abc/path/' , site : { baseUrl : 'https://base/path' } , reportGroups : [ { consolidatedId : undefined } , { consolidatedId : 'reportGroupId' } ] , } , { url : 'https://def/path/' , site : 'https://base/path' , reportGroups : [ { consolidatedId : 'reportGroupId' } ] , } , { url : 'https://hij/path/' , reportGroups : [ { consolidatedId : 'reportGroupId' } ] , } , { url : 'https://klm/path/' , site : { baseUrl : 'https://base/path' } , } , ] ) ; const expectedResponse = [ { url : 'https://abc/path/' , error : WebApiErrorCodes . missingSiteOrReportGroups . error } , { url : 'https://def/path/' , error : WebApiErrorCodes . missingSiteOrReportGroups . error } , { url : 'https://hij/path/' , error : WebApiErrorCodes . missingSiteOrReportGroups . error } , { url : 'https://klm/path/' , error : WebApiErrorCodes . missingSiteOrReportGroups . error } , ] ; scanRequestController = createScanRequestController ( context ) ; await scanRequestController . handleRequest ( ) ; const expectedResponseSorted = sortData ( expectedResponse ) ; const responseSorted = sortData ( < ScanRunResponse [ ] > ( < unknown > context . res . body ) ) ; expect ( responseSorted ) . toEqual ( expectedResponseSorted ) ; } ) ; it ( 'rejects deepScan requests if they are missing required properties' , async ( ) => { context . req . rawBody = JSON . stringify ( [ { deepScan : true , url : 'https://abc/path/' , } , { deepScan : true , url : 'https://def/path/' , reportGroups : [ { consolidatedId : 'reportGroupId' } ] , } , { deepScan : true , url : 'https://hij/path/' , site : { baseUrl : 'https://base/path' } , } , ] ) ; const expectedResponse = [ { url : 'https://abc/path/' , error : WebApiErrorCodes . missingRequiredDeepScanProperties . error } , { url : 'https://def/path/' , error : WebApiErrorCodes . missingRequiredDeepScanProperties . error } , { url : 'https://hij/path/' , error : WebApiErrorCodes . missingRequiredDeepScanProperties . error } , ] ; scanRequestController = createScanRequestController ( context ) ; await scanRequestController . handleRequest ( ) ; const expectedResponseSorted = sortData ( expectedResponse ) ; const responseSorted = sortData ( < ScanRunResponse [ ] > ( < unknown > context . res . body ) ) ; expect ( responseSorted ) . toEqual ( expectedResponseSorted ) ; } ) ; it ( 'accepts valid request only' , async ( ) => { const guid1 = '1e9cefa6-538a-6df0-aaaa-ffffffffffff' ; const guid2 = '1e9cefa6-538a-6df0-bbbb-ffffffffffff' ; guidGeneratorMock . setup ( ( g ) => g . createGuid ( ) ) . returns ( ( ) => guid1 ) ; guidGeneratorMock . setup ( ( g ) => g . createGuidFromBaseGuid ( guid1 ) ) . returns ( ( ) => guid2 ) ; context . req . rawBody = JSON . stringify ( [ { url : 'https://abs/path/' , priority : 1 , scanNotifyUrl : 'https://notify/path/' , site : { baseUrl : 'https://base/path' , knownPages : [ 'https://base/path/known1' , 'https://base/path/known2' ] , discoveryPatterns : [ 'pattern1' , 'pattern2' ] , } , reportGroups : [ { consolidatedId : 'reportGroupId' } ] , deepScan : true , } , { url : '/invalid/url' } , { url : 'https://cde/path/' , priority : 9999 } , ] ) ; const expectedResponse = [ { scanId : guid2 , url : 'https://abs/path/' } , { url : '/invalid/url' , error : WebApiErrorCodes . invalidURL . error } , { url : 'https://cde/path/' , error : WebApiErrorCodes . outOfRangePriority . error } , ] ; const expectedSavedRequest = [ { scanId : guid2 , url : 'https://abs/path/' , priority : 1 , scanNotifyUrl : 'https://notify/path/' , site : { baseUrl : 'https://base/path' , knownPages : [ 'https://base/path/known1' , 'https://base/path/known2' ] , discoveryPatterns : [ 'pattern1' , 'pattern2' ] , } , reportGroups : [ { consolidatedId : 'reportGroupId' } ] , deepScan : true , } , ] ; scanDataProviderMock . setup ( async ( o ) => o . writeScanRunBatchRequest ( guid1 , expectedSavedRequest ) ) . verifiable ( Times . once ( ) ) ; scanRequestController = createScanRequestController ( context ) ; await scanRequestController . handleRequest ( ) ; const expectedResponseSorted = sortData ( expectedResponse ) ; const responseSorted = sortData ( < ScanRunResponse [ ] > ( < unknown > context . res . body ) ) ; expect ( context . res . status ) . toEqual ( 202 ) ; expect ( responseSorted ) . toEqual ( expectedResponseSorted ) ; scanDataProviderMock . verifyAll ( ) ; guidGeneratorMock . verifyAll ( ) ; } ) ; it ( 'accepts request with priority' , async ( ) => { const guid1 = '1e9cefa6-538a-6df0-aaaa-ffffffffffff' ; const guid2 = '1e9cefa6-538a-6df0-bbbb-ffffffffffff' ; guidGeneratorMock . setup ( ( g ) => g . createGuid ( ) ) . returns ( ( ) => guid1 ) ; guidGeneratorMock . setup ( ( g ) => g . createGuidFromBaseGuid ( guid1 ) ) . returns ( ( ) => guid2 ) ; const priority = 10 ; context . req . rawBody = JSON . stringify ( [ { url : 'https://abs/path/' , priority : priority } ] ) ; const expectedResponse = [ { scanId : guid2 , url : 'https://abs/path/' } ] ; const expectedSavedRequest = [ { scanId : guid2 , url : 'https://abs/path/' , priority : priority } ] ; scanDataProviderMock . setup ( async ( o ) => o . writeScanRunBatchRequest ( guid1 , expectedSavedRequest ) ) . verifiable ( Times . once ( ) ) ; scanRequestController = createScanRequestController ( context ) ; await scanRequestController . handleRequest ( ) ; expect ( context . res . status ) . toEqual ( 202 ) ; expect ( context . res . body ) . toEqual ( expectedResponse ) ; scanDataProviderMock . verifyAll ( ) ; guidGeneratorMock . verifyAll ( ) ; } ) ; it ( 'sends telemetry event' , async ( ) => { const guid1 = '1e9cefa6-538a-6df0-aaaa-ffffffffffff' ; const guid2 = '1e9cefa6-538a-6df0-bbbb-ffffffffffff' ; guidGeneratorMock . setup ( ( g ) => g . createGuid ( ) ) . returns ( ( ) => guid1 ) ; guidGeneratorMock . setup ( ( g ) => g . createGuidFromBaseGuid ( guid1 ) ) . returns ( ( ) => guid2 ) ; context . req . rawBody = JSON . stringify ( [ { url : 'https://abs/path/' , priority : 1 } , { url : '/invalid/url' } , { url : 'https://cde/path/' , priority : 9999 } , ] ) ; const expectedMeasurements = { totalScanRequests : 3 , pendingScanRequests : 1 , rejectedScanRequests : 2 , } ; loggerMock . setup ( ( lm ) => lm . trackEvent ( 'ScanRequestReceived' , null , expectedMeasurements ) ) . verifiable ( ) ; scanRequestController = createScanRequestController ( context ) ; await scanRequestController . handleRequest ( ) ; loggerMock . verifyAll ( ) ; } ) ; it ( 'v1.0 accepts an array' , async ( ) => { const guid1 = '1e9cefa6-538a-6df0-aaaa-ffffffffffff' ; const guid2 = '1e9cefa6-538a-6df0-bbbb-ffffffffffff' ; const guid3 = '1e9cefa6-538a-6df0-dddd-ffffffffffff' ; guidGeneratorMock . setup ( ( g ) => g . createGuid ( ) ) . returns ( ( ) => guid1 ) ; guidGeneratorMock . setup ( ( g ) => g . createGuidFromBaseGuid ( guid1 ) ) . returns ( ( ) => guid2 ) ; guidGeneratorMock . setup ( ( g ) => g . createGuidFromBaseGuid ( guid1 ) ) . returns ( ( ) => guid3 ) ; const priority = 10 ; context . req . rawBody = JSON . stringify ( [ { url : 'https://abs/path/' , priority : priority } , { url : 'https://bing.com/path/' , priority : priority } , ] ) ; const expectedResponse = [ { scanId : guid2 , url : 'https://abs/path/' } , { scanId : guid3 , url : 'https://bing.com/path/' } , ] ; const expectedSaveRequest = [ { scanId : guid2 , url : 'https://abs/path/' , priority : priority } , { scanId : guid3 , url : 'https://bing.com/path/' , priority : priority } , ] ; scanDataProviderMock . setup ( async ( o ) => o . writeScanRunBatchRequest ( guid1 , expectedSaveRequest ) ) . verifiable ( Times . once ( ) ) ; scanRequestController = createScanRequestController ( context ) ; await scanRequestController . handleRequest ( ) ; expect ( context . res . status ) . toEqual ( 202 ) ; expect ( context . res . body ) . toEqual ( expectedResponse ) ; scanDataProviderMock . verifyAll ( ) ; guidGeneratorMock . verifyAll ( ) ; } ) ; it ( 'v2.0 accepts a single url' , async ( ) => { const guid1 = '1e9cefa6-538a-6df0-aaaa-ffffffffffff' ; const guid2 = '1e9cefa6-538a-6df0-bbbb-ffffffffffff' ; context . req . query [ 'api-version' ] = '2.0' ; guidGeneratorMock . setup ( ( g ) => g . createGuid ( ) ) . returns ( ( ) => guid1 ) ; guidGeneratorMock . setup ( ( g ) => g . createGuidFromBaseGuid ( guid1 ) ) . returns ( ( ) => guid2 ) ; const priority = 10 ; context . req . rawBody = JSON . stringify ( { url : 'https://abs/path/' , priority : priority } ) ; const expectedResponse = { scanId : guid2 , url : 'https://abs/path/' } ; const expectedSaveRequest = [ { scanId : guid2 , url : 'https://abs/path/' , priority : priority } ] ; scanDataProviderMock . setup ( async ( o ) => o . writeScanRunBatchRequest ( guid1 , expectedSaveRequest ) ) . verifiable ( Times . once ( ) ) ; scanRequestController = createScanRequestController ( context ) ; await scanRequestController . handleRequest ( ) ; expect ( context . res . status ) . toEqual ( 202 ) ; expect ( context . res . body ) . toEqual ( expectedResponse ) ; scanDataProviderMock . verifyAll ( ) ; guidGeneratorMock . verifyAll ( ) ; } ) ; it ( 'v2.0 accepts does not accept an array' , async ( ) => { context . req . query [ 'api-version' ] = '2.0' ; const priority = 10 ; context . req . rawBody = JSON . stringify ( [ { url : 'https://abs/path/' , priority : priority } ] ) ; scanRequestController = createScanRequestController ( context ) ; await scanRequestController . handleRequest ( ) ; expect ( context . res . status ) . toEqual ( 400 ) ; expect ( context . res ) . toEqual ( HttpResponse . getErrorResponse ( WebApiErrorCodes . malformedRequest ) ) ; } ) ; } ) ; } ) ;