diff --git a/src/lib/polling/common.ts b/src/lib/polling/common.ts index cb0109965a..450fcdb3d3 100644 --- a/src/lib/polling/common.ts +++ b/src/lib/polling/common.ts @@ -1,6 +1,11 @@ import { sleep } from '../common'; import { ScanResult } from '../ecosystems/types'; -import { ResolutionMeta } from './types'; +import { + ResolutionMeta, + ResolveAndMonitorFactsResponse, + ResolveAndTestFactsResponse, +} from './types'; +import { FailedToRunTestError } from '../errors'; export async function delayNextStep( attemptsCount: number, @@ -35,3 +40,13 @@ export function extractResolutionMetaFromScanResult({ targetReference, }; } + +export function handleProcessingStatus( + response: ResolveAndMonitorFactsResponse | ResolveAndTestFactsResponse, +): void { + if (response?.status === 'CANCELLED' || response?.status === 'ERROR') { + throw new FailedToRunTestError( + 'Failed to process the project. Please run the command again with the `-d` flag and contact support@snyk.io with the contents of the output', + ); + } +} diff --git a/src/lib/polling/polling-monitor.ts b/src/lib/polling/polling-monitor.ts index 75f6d9c02f..8091255289 100644 --- a/src/lib/polling/polling-monitor.ts +++ b/src/lib/polling/polling-monitor.ts @@ -11,7 +11,7 @@ import { ResolveAndMonitorFactsResponse, ResolveFactsState, } from './types'; -import { delayNextStep } from './common'; +import { delayNextStep, handleProcessingStatus } from './common'; import { generateProjectAttributes, generateTags, @@ -76,6 +76,9 @@ export async function pollingMonitorWithTokenUntilDone( }; const response = await makeRequest(payload); + + handleProcessingStatus(response); + if (response.ok && response.isMonitored) { return response; } diff --git a/src/lib/polling/polling-test.ts b/src/lib/polling/polling-test.ts index 608a3e41c5..be592971a2 100644 --- a/src/lib/polling/polling-test.ts +++ b/src/lib/polling/polling-test.ts @@ -8,7 +8,7 @@ import { getAuthHeader } from '../api-token'; import { ScanResult } from '../ecosystems/types'; import { ResolveAndTestFactsResponse } from './types'; -import { delayNextStep } from './common'; +import { delayNextStep, handleProcessingStatus } from './common'; import { TestDependenciesResult } from '../snyk-test/legacy'; export async function requestTestPollingToken( @@ -54,6 +54,8 @@ export async function pollingTestWithTokenUntilDone( const response = await makeRequest(payload); + handleProcessingStatus(response); + if (response.result) { const { issues, diff --git a/test/jest/unit/lib/ecosystems/common.spec.ts b/test/jest/unit/lib/ecosystems/common.spec.ts index afd81184b7..b3c71079a1 100644 --- a/test/jest/unit/lib/ecosystems/common.spec.ts +++ b/test/jest/unit/lib/ecosystems/common.spec.ts @@ -1,4 +1,6 @@ import { isUnmanagedEcosystem } from '../../../../../src/lib/ecosystems/common'; +import { handleProcessingStatus } from '../../../../../src/lib/polling/common'; +import { FailedToRunTestError } from '../../../../../src/lib/errors'; describe('isUnmanagedEcosystem fn', () => { it.each` @@ -13,3 +15,18 @@ describe('isUnmanagedEcosystem fn', () => { }, ); }); + +describe('handleProcessingStatus fn', () => { + it.each` + actual | expected + ${'CANCELLED'} | ${'Failed to process the project. Please run the command again with the `-d` flag and contact support@snyk.io with the contents of the output'} + ${'ERROR'} | ${'Failed to process the project. Please run the command again with the `-d` flag and contact support@snyk.io with the contents of the output'} + `( + 'should validate that given $actual as input, is considered or not an unmanaged ecosystem', + ({ actual, expected }) => { + expect(() => { + handleProcessingStatus({ status: actual } as any); + }).toThrowError(new FailedToRunTestError(expected)); + }, + ); +}); diff --git a/test/jest/unit/lib/ecosystems/resolve-monitor.facts.spec.ts b/test/jest/unit/lib/ecosystems/resolve-monitor.facts.spec.ts index 3916099e57..a67bcc3dfb 100644 --- a/test/jest/unit/lib/ecosystems/resolve-monitor.facts.spec.ts +++ b/test/jest/unit/lib/ecosystems/resolve-monitor.facts.spec.ts @@ -5,6 +5,7 @@ import { resolveAndMonitorFacts } from '../../../../../src/lib/ecosystems/resolv import * as pluginAnalytics from '../../../../../src/lib/ecosystems/plugin-analytics'; import * as analytics from '../../../../../src/lib/analytics'; import * as httpClient from '../../../../../src/lib/request/promise'; +import * as promise from '../../../../../src/lib/request/promise'; describe('resolve and test facts', () => { afterEach(() => jest.restoreAllMocks()); @@ -82,6 +83,68 @@ describe('resolve and test facts', () => { }); }); + it('failing to resolve and monitor file-signatures fact for c/c++ projects when the job is cancelled', async () => { + const requestMonitorPollingTokenSpy = jest.spyOn( + pollingMonitor, + 'requestMonitorPollingToken', + ); + const makeRequestSpy = jest.spyOn(promise, 'makeRequest'); + + requestMonitorPollingTokenSpy.mockResolvedValueOnce({ + token, + status: 'OK', + pollingTask, + }); + + makeRequestSpy.mockResolvedValueOnce({ + token, + status: 'CANCELLED', + pollingTask, + }); + + const [testResults, errors] = await resolveAndMonitorFacts( + scanResults, + {} as Options, + ); + + expect(testResults).toEqual([]); + expect(errors[0]).toEqual({ + error: 'Could not monitor dependencies in path', + path: 'path', + scanResult: { + analytics: [ + { + data: { + totalFileSignatures: 3, + totalSecondsElapsedToGenerateFileSignatures: 0, + }, + name: 'fileSignaturesAnalyticsContext', + }, + ], + facts: [ + { + data: [ + { + hashes_ffm: [ + { data: 'ucMc383nMM/wkFRM4iOo5Q', format: 1 }, + { data: 'k+DxEmslFQWuJsZFXvSoYw', format: 1 }, + ], + path: 'fastlz_example/fastlz.h', + }, + ], + type: 'fileSignatures', + }, + ], + identity: { type: 'cpp' }, + name: 'my-unmanaged-c-project', + target: { + branch: 'master', + remoteUrl: 'https://github.com/some-org/some-unmanaged-project.git', + }, + }, + }); + }); + it('successfully resolves and monitor file-signatures fact for c/c++ projects', async () => { const resolveAndTestFactsSpy = jest.spyOn( pollingMonitor, diff --git a/test/jest/unit/lib/ecosystems/resolve-test-facts.spec.ts b/test/jest/unit/lib/ecosystems/resolve-test-facts.spec.ts index aa02174ddc..14ff84859a 100644 --- a/test/jest/unit/lib/ecosystems/resolve-test-facts.spec.ts +++ b/test/jest/unit/lib/ecosystems/resolve-test-facts.spec.ts @@ -1,5 +1,6 @@ import { Options } from '../../../../../src/lib/types'; import * as pollingTest from '../../../../../src/lib/polling/polling-test'; +import * as promise from '../../../../../src/lib/request/promise'; import { depGraphData, scanResults } from './fixtures/'; import { resolveAndTestFacts } from '../../../../../src/lib/ecosystems/resolve-test-facts'; import * as pluginAnalytics from '../../../../../src/lib/ecosystems/plugin-analytics'; @@ -50,6 +51,42 @@ describe('resolve and test facts', () => { ); }); + it.each` + actual | expected + ${'CANCELLED'} | ${'Failed to process the project. Please run the command again with the `-d` flag and contact support@snyk.io with the contents of the output'} + ${'ERROR'} | ${'Failed to process the project. Please run the command again with the `-d` flag and contact support@snyk.io with the contents of the output'} + `( + 'should handle different file-signatures processing statuses for the testing flow', + async ({ actual, expected }) => { + const requestTestPollingTokenSpy = jest.spyOn( + pollingTest, + 'requestTestPollingToken', + ); + const makeRequestSpy = jest.spyOn(promise, 'makeRequest'); + + requestTestPollingTokenSpy.mockResolvedValueOnce({ + token, + status: 'OK', + pollingTask, + }); + + makeRequestSpy.mockResolvedValueOnce({ + token, + status: actual, + pollingTask, + }); + + const [testResults, errors] = await resolveAndTestFacts( + 'cpp', + scanResults, + {} as Options, + ); + + expect(testResults).toEqual([]); + expect(errors[0]).toContain(expected); + }, + ); + it('successfully resolving and testing file-signatures fact for c/c++ projects', async () => { const resolveAndTestFactsSpy = jest.spyOn( pollingTest,