jest . mock ( '../../../lib/content_stream' , ( ) => ( { getContentStream : jest . fn ( ) , } ) ) ; import type { DeeplyMockedKeys } from '@kbn/utility-types/jest' ; import { ElasticsearchClient } from 'kibana/server' ; import { BehaviorSubject } from 'rxjs' ; import { setupServer } from 'src/core/server/test_utils' ; import { Readable } from 'stream' ; import supertest from 'supertest' ; import { ReportingCore } from '../../../' ; import { licensingMock } from '../../../../../licensing/server/mocks' ; import { ReportingInternalSetup , ReportingInternalStart } from '../../../core' ; import { ContentStream , ExportTypesRegistry , getContentStream } from '../../../lib' ; import { createMockConfigSchema , createMockPluginSetup , createMockPluginStart , createMockReportingCore , } from '../../../test_helpers' ; import { ExportTypeDefinition , ReportingRequestHandlerContext } from '../../../types' ; import { registerJobInfoRoutes } from '../jobs' ; type SetupServerReturn = Awaited < ReturnType < typeof setupServer > > ; describe ( 'GET /api/reporting/jobs/download' , ( ) => { const reportingSymbol = Symbol ( 'reporting' ) ; let server ; let httpSetup ; let exportTypesRegistry ; let core ; let mockSetupDeps ; let mockStartDeps ; let mockEsClient ; let stream ; const getHits = ( ... sources ) => { return { hits : { hits : sources . map ( ( source ) => ( { _source : source } ) ) , } , } ; } ; beforeEach ( async ( ) => { ( { server , httpSetup } = await setupServer ( reportingSymbol ) ) ; httpSetup . registerRouteHandlerContext < ReportingRequestHandlerContext , 'reporting' > ( reportingSymbol , 'reporting' , ( ) => ( { usesUiCapabilities : jest . fn ( ) } ) ) ; const mockConfigSchema = createMockConfigSchema ( { roles : { enabled : false } } ) ; mockSetupDeps = createMockPluginSetup ( { security : { license : { isEnabled : ( ) => true } , authc : { getCurrentUser : ( ) => ( { id : '123' , roles : [ 'superuser' ] , username : 'Tom Riddle' } ) , } , } , router : httpSetup . createRouter ( '' ) , } ) ; mockStartDeps = await createMockPluginStart ( { licensing : { ... licensingMock . createStart ( ) , license$ : new BehaviorSubject ( { isActive : true , isAvailable : true , type : 'gold' } ) , } , } , mockConfigSchema ) ; core = await createMockReportingCore ( mockConfigSchema , mockSetupDeps , mockStartDeps ) ; exportTypesRegistry = new ExportTypesRegistry ( ) ; exportTypesRegistry . register ( { id : 'unencoded' , jobType : 'unencodedJobType' , jobContentExtension : 'csv' , validLicenses : [ 'basic' , 'gold' ] , } as ExportTypeDefinition ) ; exportTypesRegistry . register ( { id : 'base64Encoded' , jobType : 'base64EncodedJobType' , jobContentEncoding : 'base64' , jobContentExtension : 'pdf' , validLicenses : [ 'basic' , 'gold' ] , } as ExportTypeDefinition ) ; core . getExportTypesRegistry = ( ) => exportTypesRegistry ; mockEsClient = ( await core . getEsClient ( ) ) . asInternalUser as typeof mockEsClient ; stream = new Readable ( { read ( ) { this . push ( 'test' ) ; this . push ( null ) ; } , } ) as typeof stream ; ( getContentStream as jest . MockedFunction < typeof getContentStream > ) . mockResolvedValue ( stream ) ; } ) ; afterEach ( async ( ) => { await server . stop ( ) ; } ) ; it ( 'fails on malformed download IDs' , async ( ) => { mockEsClient . search . mockResolvedValueOnce ( { body : getHits ( ) } as any ) ; registerJobInfoRoutes ( core ) ; await server . start ( ) ; await supertest ( httpSetup . server . listener ) . get ( '/api/reporting/jobs/download/1' ) . expect ( 400 ) . then ( ( { body } ) => expect ( body . message ) . toMatchInlineSnapshot ( '"[request params.docId]: value has length [1] but it must have a minimum length of [3]."' ) ) ; } ) ; it ( 'fails on unauthenticated users' , async ( ) => { core . pluginSetupDeps = { ... core . pluginSetupDeps , security : { license : { isEnabled : ( ) => true , } , authc : { getCurrentUser : ( ) => undefined , } , } , } as unknown as ReportingInternalSetup ; registerJobInfoRoutes ( core ) ; await server . start ( ) ; await supertest ( httpSetup . server . listener ) . get ( '/api/reporting/jobs/download/dope' ) . expect ( 401 ) . then ( ( { body } ) => expect ( body . message ) . toMatchInlineSnapshot ( `"Sorry, you aren't authenticated"` ) ) ; } ) ; it ( 'returns 404 if job not found' , async ( ) => { mockEsClient . search . mockResolvedValueOnce ( { body : getHits ( ) } as any ) ; registerJobInfoRoutes ( core ) ; await server . start ( ) ; await supertest ( httpSetup . server . listener ) . get ( '/api/reporting/jobs/download/poo' ) . expect ( 404 ) ; } ) ; it ( 'returns a 401 if not a valid job type' , async ( ) => { mockEsClient . search . mockResolvedValueOnce ( { body : getHits ( { jobtype : 'invalidJobType' , payload : { title : 'invalid!' } , } ) , } as any ) ; registerJobInfoRoutes ( core ) ; await server . start ( ) ; await supertest ( httpSetup . server . listener ) . get ( '/api/reporting/jobs/download/poo' ) . expect ( 401 ) ; } ) ; it ( `returns job's info` , async ( ) => { mockEsClient . search . mockResolvedValueOnce ( { body : getHits ( { jobtype : 'base64EncodedJobType' , payload : { } , } ) , } as any ) ; registerJobInfoRoutes ( core ) ; await server . start ( ) ; await supertest ( httpSetup . server . listener ) . get ( '/api/reporting/jobs/info/test' ) . expect ( 200 ) ; } ) ; it ( `returns 403 if a user cannot view a job's info` , async ( ) => { mockEsClient . search . mockResolvedValueOnce ( { body : getHits ( { jobtype : 'customForbiddenJobType' , payload : { } , } ) , } as any ) ; registerJobInfoRoutes ( core ) ; await server . start ( ) ; await supertest ( httpSetup . server . listener ) . get ( '/api/reporting/jobs/info/test' ) . expect ( 403 ) ; } ) ; it ( 'when a job is incomplete' , async ( ) => { mockEsClient . search . mockResolvedValueOnce ( { body : getHits ( { jobtype : 'unencodedJobType' , status : 'pending' , payload : { title : 'incomplete!' } , } ) , } as any ) ; registerJobInfoRoutes ( core ) ; await server . start ( ) ; await supertest ( httpSetup . server . listener ) . get ( '/api/reporting/jobs/download/dank' ) . expect ( 503 ) . expect ( 'Content-Type' , 'text/plain; charset=utf-8' ) . expect ( 'Retry-After' , '30' ) . then ( ( { text } ) => expect ( text ) . toEqual ( 'pending' ) ) ; } ) ; it ( 'when a job fails' , async ( ) => { mockEsClient . search . mockResolvedValue ( { body : getHits ( { jobtype : 'unencodedJobType' , status : 'failed' , output : { content : 'job failure message' } , payload : { title : 'failing job!' } , } ) , } as any ) ; registerJobInfoRoutes ( core ) ; await server . start ( ) ; await supertest ( httpSetup . server . listener ) . get ( '/api/reporting/jobs/download/dank' ) . expect ( 500 ) . expect ( 'Content-Type' , 'application/json; charset=utf-8' ) . then ( ( { body } ) => expect ( body . message ) . toEqual ( 'Reporting generation failed: job failure message' ) ) ; } ) ; describe ( 'successful downloads' , ( ) => { const getCompleteHits = ( { jobType = 'unencodedJobType' , outputContentType = 'text/plain' , title = '' , } = { } ) => { return getHits ( { jobtype : jobType , status : 'completed' , output : { content_type : outputContentType } , payload : { title } , } ) ; } ; it ( 'when a known job-type is complete' , async ( ) => { mockEsClient . search . mockResolvedValueOnce ( { body : getCompleteHits ( ) } as any ) ; registerJobInfoRoutes ( core ) ; await server . start ( ) ; await supertest ( httpSetup . server . listener ) . get ( '/api/reporting/jobs/download/dank' ) . expect ( 200 ) . expect ( 'Content-Type' , 'text/plain; charset=utf-8' ) . expect ( 'content-disposition' , 'inline; filename="report.csv"' ) ; } ) ; it ( 'succeeds when security is not there or disabled' , async ( ) => { mockEsClient . search . mockResolvedValueOnce ( { body : getCompleteHits ( ) } as any ) ; core . pluginSetupDeps . security = null ; registerJobInfoRoutes ( core ) ; await server . start ( ) ; await supertest ( httpSetup . server . listener ) . get ( '/api/reporting/jobs/download/dope' ) . expect ( 200 ) . expect ( 'Content-Type' , 'text/plain; charset=utf-8' ) . expect ( 'content-disposition' , 'inline; filename="report.csv"' ) ; } ) ; it ( 'forwards job content stream' , async ( ) => { mockEsClient . search . mockResolvedValueOnce ( { body : getCompleteHits ( { jobType : 'unencodedJobType' , } ) , } as any ) ; registerJobInfoRoutes ( core ) ; await server . start ( ) ; await supertest ( httpSetup . server . listener ) . get ( '/api/reporting/jobs/download/dank' ) . expect ( 200 ) . expect ( 'Content-Type' , 'text/plain; charset=utf-8' ) . then ( ( { text } ) => expect ( text ) . toEqual ( 'test' ) ) ; } ) ; it ( 'refuses to return unknown content-types' , async ( ) => { mockEsClient . search . mockResolvedValueOnce ( { body : getCompleteHits ( { jobType : 'unencodedJobType' , outputContentType : 'application/html' , } ) , } as any ) ; registerJobInfoRoutes ( core ) ; await server . start ( ) ; await supertest ( httpSetup . server . listener ) . get ( '/api/reporting/jobs/download/dank' ) . expect ( 400 ) . then ( ( { body } ) => { expect ( body ) . toEqual ( { error : 'Bad Request' , message : 'Unsupported content-type of application/html specified by job output' , statusCode : 400 , } ) ; } ) ; } ) ; } ) ; describe ( 'Deprecated: role-based access control' , ( ) => { it ( 'fails on users without the appropriate role' , async ( ) => { const deprecatedConfig = createMockConfigSchema ( { roles : { enabled : true } } ) ; core = await createMockReportingCore ( deprecatedConfig , mockSetupDeps ) ; core . pluginSetupDeps = { ... core . pluginSetupDeps , security : { license : { isEnabled : ( ) => true , } , authc : { getCurrentUser : ( ) => ( { id : '123' , roles : [ 'peasant' ] , username : 'Tom Riddle' , } ) , } , } , } as unknown as ReportingInternalSetup ; registerJobInfoRoutes ( core ) ; await server . start ( ) ; await supertest ( httpSetup . server . listener ) . get ( '/api/reporting/jobs/download/dope' ) . expect ( 403 ) . then ( ( { body } ) => expect ( body . message ) . toMatchInlineSnapshot ( `"Sorry, you don't have access to Reporting"` ) ) ; } ) ; } ) ; } ) ;