Skip to content

Commit

Permalink
feat: add report upload to code test
Browse files Browse the repository at this point in the history
  • Loading branch information
patricia-v committed Feb 17, 2023
1 parent 49cb904 commit 62e492d
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 8 deletions.
29 changes: 24 additions & 5 deletions src/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@ import {
GetAnalysisOptions,
} from './http';
import { createBundleFromFolders, remoteBundleFactory } from './bundles';
import { reportBundle } from './report';
import { emitter } from './emitter';
import {
AnalysisResult,
AnalysisResultLegacy,
AnalysisResultSarif,
AnalysisFiles,
Suggestion,
ReportUploadResult,
} from './interfaces/analysis-result.interface';
import { FileAnalysisOptions } from './interfaces/analysis-options.interface';
import { FileAnalysisOptions, ReportOptions } from './interfaces/analysis-options.interface';
import { FileAnalysis } from './interfaces/files.interface';

const sleep = (duration: number) => new Promise(resolve => setTimeout(resolve, duration));
Expand Down Expand Up @@ -102,21 +104,38 @@ export async function analyzeFolders(options: FileAnalysisOptions): Promise<File
});
if (fileBundle === null) return null;

// Analyze bundle
const analysisResults = await analyzeBundle({
const config = {
bundleHash: fileBundle.bundleHash,
...options.connection,
...options.analysisOptions,
shard: calcHash(fileBundle.baseDir),
...(options.analysisContext ? { analysisContext: options.analysisContext } : {}),
});
};

let analysisResults: AnalysisResult;

// Whether this is a report/result upload operation.
const isReport = options.reportOptions?.enabled ?? false;
let reportResults: ReportUploadResult | undefined;
if (isReport && options.reportOptions) {
// Analyze and upload bundle results.
const reportRes = await reportBundle({
...config,
report: options.reportOptions,
});
analysisResults = reportRes.analysisResult;
reportResults = reportRes.uploadResult;
} else {
// Analyze bundle.
analysisResults = await analyzeBundle(config);
}

if (analysisResults.type === 'legacy') {
// expand relative file names to absolute ones only for legacy results
analysisResults.files = normalizeResultFiles(analysisResults.files, fileBundle.baseDir);
}

return { fileBundle, analysisResults, ...options };
return { fileBundle, analysisResults, reportResults, ...options };
}

function mergeBundleResults(
Expand Down
70 changes: 68 additions & 2 deletions src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { promisify } from 'util';
import { ErrorCodes, GenericErrorTypes, DEFAULT_ERROR_MESSAGES, MAX_RETRY_ATTEMPTS } from './constants';

import { BundleFiles, SupportedFiles } from './interfaces/files.interface';
import { AnalysisResult } from './interfaces/analysis-result.interface';
import { AnalysisResult, ReportResult } from './interfaces/analysis-result.interface';
import { FailedResponse, makeRequest, Payload } from './needle';
import { AnalysisOptions, AnalysisContext } from './interfaces/analysis-options.interface';
import { AnalysisOptions, AnalysisContext, ReportOptions } from './interfaces/analysis-options.interface';

type ResultSuccess<T> = { type: 'success'; value: T };
type ResultError<E> = {
Expand Down Expand Up @@ -386,6 +386,72 @@ export async function getAnalysis(
return generateError<GetAnalysisErrorCodes>(res.errorCode, GET_ANALYSIS_ERROR_MESSAGES, 'getAnalysis');
}

export interface UploadReportOptions extends GetAnalysisOptions {
report: ReportOptions;
}
export interface GetReportOptions extends ConnectionOptions {
reportId: string;
}

export type InitUploadResponseDto = {
reportId: string;
};

export type UploadReportResponseDto = ReportResult | AnalysisFailedResponse | AnalysisResponseProgress;

export async function initReport(
options: UploadReportOptions,
): Promise<Result<InitUploadResponseDto, GetAnalysisErrorCodes>> {
const config: Payload = {
headers: {
...prepareTokenHeaders(options.sessionToken),
source: options.source,
...(options.requestId && { 'snyk-request-id': options.requestId }),
...(options.org && { 'snyk-org-name': options.org }),
},
url: `${options.baseURL}/report`,
method: 'post',
body: {
workflowData: {
projectName: options.report.projectName,
},
key: {
type: 'file',
hash: options.bundleHash,
limitToFiles: options.limitToFiles || [],
...(options.shard ? { shard: options.shard } : null),
},
...pick(options, ['severity', 'prioritized', 'legacy', 'analysisContext']),
},
};

const res = await makeRequest<InitUploadResponseDto>(config);
if (res.success) return { type: 'success', value: res.body };
return generateError<GetAnalysisErrorCodes>(res.errorCode, GET_ANALYSIS_ERROR_MESSAGES, 'initReport');
}

export async function getReport(
options: GetReportOptions,
): Promise<Result<UploadReportResponseDto, GetAnalysisErrorCodes>> {
const config: Payload = {
headers: {
...prepareTokenHeaders(options.sessionToken),
source: options.source,
...(options.requestId && { 'snyk-request-id': options.requestId }),
...(options.org && { 'snyk-org-name': options.org }),
},
url: `${options.baseURL}/report/${options.reportId}`,
method: 'get',
body: {
reportId: options.reportId,
},
};

const res = await makeRequest<UploadReportResponseDto>(config);
if (res.success) return { type: 'success', value: res.body };
return generateError<GetAnalysisErrorCodes>(res.errorCode, GET_ANALYSIS_ERROR_MESSAGES, 'getReport');
}

export function getVerifyCallbackUrl(authHost: string): string {
return `${authHost}/api/verify/callback`;
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import { SupportedFiles, FileAnalysis } from './interfaces/files.interface';
import { AnalysisSeverity, AnalysisContext } from './interfaces/analysis-options.interface';
import {
AnalysisResult,
AnalysisResultSarif,
AnalysisResultLegacy,
FilePath,
FileSuggestion,
Suggestion,
Marker,
ReportResult,
} from './interfaces/analysis-result.interface';

export {
Expand All @@ -27,6 +29,7 @@ export {
constants,
AnalysisSeverity,
AnalysisResult,
AnalysisResultSarif,
AnalysisResultLegacy,
SupportedFiles,
FileAnalysis,
Expand All @@ -40,4 +43,5 @@ export {
getIpFamily,
IpFamily,
AnalysisContext,
ReportResult,
};
7 changes: 7 additions & 0 deletions src/interfaces/analysis-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface FileAnalysisOptions extends AnalysisContext {
connection: ConnectionOptions;
analysisOptions: AnalysisOptions;
fileOptions: AnalyzeFoldersOptions;
reportOptions?: ReportOptions;
languages?: string[];
}

Expand All @@ -58,3 +59,9 @@ export interface CollectBundleFilesOptions extends AnalyzeFoldersOptions {
baseDir: string;
fileIgnores: string[];
}

export interface ReportOptions {
enabled: boolean;
projectName?: string;
targetRef?: string;
}
12 changes: 12 additions & 0 deletions src/interfaces/analysis-result.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,16 @@ export interface AnalysisResultLegacy extends AnalysisResultBase {
files: AnalysisFiles;
}

export interface ReportUploadResult {
projectId: string;
snapshotId: string;
reportUrl: string;
}

export interface ReportResult {
status: 'COMPLETE';
uploadResult: ReportUploadResult;
analysisResult: AnalysisResultSarif;
}

export type AnalysisResult = AnalysisResultSarif | AnalysisResultLegacy;
2 changes: 2 additions & 0 deletions src/interfaces/files.interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AnalysisResult } from '..';
import { FileBundle } from '../bundles';
import { FileAnalysisOptions } from './analysis-options.interface';
import { ReportUploadResult } from './analysis-result.interface';

export interface File {
hash: string;
Expand All @@ -27,4 +28,5 @@ export type SupportedFiles = {
export interface FileAnalysis extends FileAnalysisOptions {
fileBundle: FileBundle;
analysisResults: AnalysisResult;
reportResults?: ReportUploadResult;
}
89 changes: 89 additions & 0 deletions src/report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import pick from 'lodash.pick';
import { POLLING_INTERVAL } from './constants';
import { emitter } from './emitter';
import {
AnalysisFailedResponse,
GetAnalysisErrorCodes,
UploadReportResponseDto,
initReport,
AnalysisStatus,
Result,
UploadReportOptions,
getReport,
} from './http';
import { ReportResult } from './interfaces/analysis-result.interface';

const sleep = (duration: number) => new Promise(resolve => setTimeout(resolve, duration));

async function pollReport(
options: UploadReportOptions,
): Promise<Result<AnalysisFailedResponse | ReportResult, GetAnalysisErrorCodes>> {
// Return early if project name is not provided.
const projectName = options.report?.projectName?.trim();
if (!projectName || projectName.length === 0) {
throw new Error('"project-name" must be provided for "report"');
}

emitter.analyseProgress({
status: AnalysisStatus.waiting,
progress: 0,
});

// First init the report
const initResponse = await initReport(options);

if (initResponse.type === 'error') {
return initResponse;
}
const { reportId } = initResponse.value;

let apiResponse: Result<UploadReportResponseDto, GetAnalysisErrorCodes>;
let response: UploadReportResponseDto;
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop
apiResponse = await getReport({
...pick(options, ['baseURL', 'sessionToken', 'source', 'requestId', 'org']),
reportId,
});

if (apiResponse.type === 'error') {
return apiResponse;
}

response = apiResponse.value;

if (
response.status === AnalysisStatus.waiting ||
response.status === AnalysisStatus.fetching ||
response.status === AnalysisStatus.analyzing ||
response.status === AnalysisStatus.done
) {
// Report progress of fetching
emitter.analyseProgress(response);
} else if (response.status === AnalysisStatus.complete) {
// Return data of analysis
return apiResponse as Result<ReportResult, GetAnalysisErrorCodes>;
// deepcode ignore DuplicateIfBody: false positive it seems that interface is not taken into account
} else if (response.status === AnalysisStatus.failed) {
// Report failure of analysing
return apiResponse as Result<AnalysisFailedResponse, GetAnalysisErrorCodes>;
}

// eslint-disable-next-line no-await-in-loop
await sleep(POLLING_INTERVAL);
}
}

export async function reportBundle(options: UploadReportOptions): Promise<ReportResult> {
// Call remote bundle for analysis results and emit intermediate progress
const response = await pollReport(options);

if (response.type === 'error') {
throw response.error;
} else if (response.value.status === AnalysisStatus.failed) {
throw new Error('Analysis has failed');
}

return response.value;
}
25 changes: 25 additions & 0 deletions tests/analysis.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SupportedFiles } from '../src/interfaces/files.interface';
import { AnalysisSeverity, AnalysisContext } from '../src/interfaces/analysis-options.interface';
import * as sarifSchema from './sarif-schema-2.1.0.json';
import * as needle from '../src/needle';
import * as report from '../src/report';

describe('Functional test of analysis', () => {
describe('analyzeFolders', () => {
Expand Down Expand Up @@ -327,5 +328,29 @@ describe('Functional test of analysis', () => {
}),
);
});

// TODO: this test is being skipped for now since the /report flow hasn't been fully rolled out and it can't succeed for now
it.skip('should successfully analyze folder with the report option enabled', async () => {
const mockReportBundle = jest.spyOn(report, 'reportBundle');

const bundle = await analyzeFolders({
connection: { baseURL, sessionToken, source },
analysisOptions: { severity: AnalysisSeverity.info, prioritized: true, legacy: true },
fileOptions: {
paths: [sampleProjectPath],
symlinksEnabled: false,
defaultFileIgnores: undefined,
},
reportOptions: {
enabled: true,
projectName: 'test-project',
},
});

expect(mockReportBundle).toHaveBeenCalledTimes(1);

// TODO: check if bundle was successfully created
console.log(bundle);
});
});
});
Loading

0 comments on commit 62e492d

Please sign in to comment.