From 052e5c5b697cccb876a785d2bf36d0a303bd23e6 Mon Sep 17 00:00:00 2001 From: ghe Date: Tue, 18 Oct 2022 18:55:12 +0100 Subject: [PATCH 1/5] chore: move FF API calls to src/lib/api --- .../feature-flags/index.ts} | 2 +- .../feature-flags/index.test.ts} | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) rename src/lib/{get-feature-flag-for-snyk-org.ts => api/feature-flags/index.ts} (95%) rename test/lib/{get-feature-flag.test.ts => api/feature-flags/index.test.ts} (92%) diff --git a/src/lib/get-feature-flag-for-snyk-org.ts b/src/lib/api/feature-flags/index.ts similarity index 95% rename from src/lib/get-feature-flag-for-snyk-org.ts rename to src/lib/api/feature-flags/index.ts index 6f787b82..ac7dc0ee 100644 --- a/src/lib/get-feature-flag-for-snyk-org.ts +++ b/src/lib/api/feature-flags/index.ts @@ -1,4 +1,4 @@ -import { requestsManager } from 'snyk-request-manager'; +import type { requestsManager } from 'snyk-request-manager'; import * as debugLib from 'debug'; const debug = debugLib('snyk:get-feature-flag'); diff --git a/test/lib/get-feature-flag.test.ts b/test/lib/api/feature-flags/index.test.ts similarity index 92% rename from test/lib/get-feature-flag.test.ts rename to test/lib/api/feature-flags/index.test.ts index 03de1da8..15f6140e 100644 --- a/test/lib/get-feature-flag.test.ts +++ b/test/lib/api/feature-flags/index.test.ts @@ -1,5 +1,5 @@ import { requestsManager } from 'snyk-request-manager'; -import { getFeatureFlag } from '../../src/lib/get-feature-flag-for-snyk-org'; +import { getFeatureFlag } from '../../../../src/lib/api/feature-flags'; jest.unmock('snyk-request-manager'); jest.requireActual('snyk-request-manager'); @@ -8,6 +8,7 @@ const orgId = process.env.TEST_ORG_ID as string; describe('getFeatureFlag', () => { const requestManager = new requestsManager({ userAgentPrefix: 'snyk-api-import:tests', + maxRetryCount: 1, }); afterAll(async () => { jest.resetAllMocks(); From cca02980a5844d85b35734e53a25348cd9c4f71b Mon Sep 17 00:00:00 2001 From: ghe Date: Tue, 18 Oct 2022 18:59:18 +0100 Subject: [PATCH 2/5] refactor: default branch call --- src/lib/source-handlers/github/get-default-branch.ts | 11 +++++++---- src/scripts/generate-imported-targets-from-snyk.ts | 10 ++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/lib/source-handlers/github/get-default-branch.ts b/src/lib/source-handlers/github/get-default-branch.ts index 01a45e46..1631086c 100644 --- a/src/lib/source-handlers/github/get-default-branch.ts +++ b/src/lib/source-handlers/github/get-default-branch.ts @@ -1,21 +1,24 @@ import { Octokit } from '@octokit/rest'; import * as debugLib from 'debug'; +import type { Target } from '../../types'; import { getGithubToken } from './get-github-token'; import { getGithubBaseUrl } from './github-base-url'; const debug = debugLib('snyk:get-github-defaultBranch-script'); export async function getGithubReposDefaultBranch( - RepoName: string, + target: Target, host?: string, ): Promise { const githubToken = getGithubToken(); const baseUrl = getGithubBaseUrl(host); const octokit: Octokit = new Octokit({ baseUrl, auth: githubToken }); - debug(`fetching the default branch for repo: ${RepoName}`); - - const response = await octokit.request(`GET /repos/${RepoName}`); + debug(`fetching the default branch for repo: ${target.owner}/${target.name}`); + const response = await octokit.repos.get({ + owner: target.owner!, + repo: target.name!, + }); return response.data.default_branch as string; } diff --git a/src/scripts/generate-imported-targets-from-snyk.ts b/src/scripts/generate-imported-targets-from-snyk.ts index 3704d35f..8115f429 100644 --- a/src/scripts/generate-imported-targets-from-snyk.ts +++ b/src/scripts/generate-imported-targets-from-snyk.ts @@ -4,12 +4,14 @@ import * as path from 'path'; import * as pMap from 'p-map'; import * as _ from 'lodash'; -import { +import type { FilePath, SnykProject, - SupportedIntegrationTypesToListSnykTargets, Target, } from '../lib/types'; +import { + SupportedIntegrationTypesToListSnykTargets, +} from '../lib/types'; import { getAllOrgs, getLoggingPath, @@ -69,8 +71,8 @@ export function imageProjectToTarget( name: project.name, }; } - -const targetGenerators = { +// TODO: move to it's own lib? +export const targetGenerators = { [SupportedIntegrationTypesToListSnykTargets.GITHUB]: projectToTarget, [SupportedIntegrationTypesToListSnykTargets.GITLAB]: gitlabProjectToImportLogTarget, [SupportedIntegrationTypesToListSnykTargets.GHE]: projectToTarget, From 71a2f84d8c9caafe0fb2a6cdb47a4c2fb26ea7fe Mon Sep 17 00:00:00 2001 From: ghe Date: Tue, 18 Oct 2022 19:07:52 +0100 Subject: [PATCH 3/5] chore: reset mocks to original implementation --- test/lib/api/feature-flags/index.test.ts | 7 ++++--- test/lib/api/project/index.test.ts | 2 +- test/lib/import-projects.test.ts | 2 +- test/lib/org.test.ts | 7 ++++--- test/lib/orgs.test.ts | 2 +- test/scripts/generate-org-data.test.ts | 6 +++--- test/system/help.test.ts | 2 +- test/system/import:data.test.ts | 2 +- test/system/list:imported.test.ts | 2 +- test/system/orgs:create.test.ts | 2 +- test/system/orgs:data/bitbucket.test.ts | 2 +- test/system/orgs:data/errors.test.ts | 2 +- test/system/orgs:data/generic.test.ts | 2 +- test/system/orgs:data/github.test.ts | 2 +- test/system/orgs:data/gitlab.test.ts | 2 +- 15 files changed, 23 insertions(+), 21 deletions(-) diff --git a/test/lib/api/feature-flags/index.test.ts b/test/lib/api/feature-flags/index.test.ts index 15f6140e..4dee3e6a 100644 --- a/test/lib/api/feature-flags/index.test.ts +++ b/test/lib/api/feature-flags/index.test.ts @@ -10,9 +10,10 @@ describe('getFeatureFlag', () => { userAgentPrefix: 'snyk-api-import:tests', maxRetryCount: 1, }); - afterAll(async () => { - jest.resetAllMocks(); - }, 1000); + afterAll(() => { + jest.restoreAllMocks(); + }); + it('get feature flag for org - mock', async () => { jest.spyOn(requestManager, 'request').mockResolvedValueOnce({ diff --git a/test/lib/api/project/index.test.ts b/test/lib/api/project/index.test.ts index 1d5ad1bc..db301e93 100644 --- a/test/lib/api/project/index.test.ts +++ b/test/lib/api/project/index.test.ts @@ -10,7 +10,7 @@ describe('UpdateProject', () => { process.env.SNYK_TOKEN = process.env.SNYK_TOKEN_TEST; }); afterAll(() => { - jest.resetAllMocks(); + jest.restoreAllMocks(); process.env = { ...OLD_ENV }; }, 1000); diff --git a/test/lib/import-projects.test.ts b/test/lib/import-projects.test.ts index 4e5fbef5..315785a3 100644 --- a/test/lib/import-projects.test.ts +++ b/test/lib/import-projects.test.ts @@ -137,7 +137,7 @@ describe('importTarget()', () => { const requestManager = new requestsManager({ userAgentPrefix: 'snyk-api-import:tests', }); - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); it('Target is always sanitized', async () => { diff --git a/test/lib/org.test.ts b/test/lib/org.test.ts index 0b068f8f..f0808cf1 100644 --- a/test/lib/org.test.ts +++ b/test/lib/org.test.ts @@ -93,9 +93,10 @@ describe('listTargets', () => { const requestManager = new requestsManager({ userAgentPrefix: 'snyk-api-import:tests', }); - afterAll(async () => { - jest.resetAllMocks(); - }, 1000); + afterAll(() => { + jest.restoreAllMocks(); + }); + it('list the targets in a given Org without pagination - mock', async () => { jest.spyOn(requestManager, 'request').mockResolvedValue({ diff --git a/test/lib/orgs.test.ts b/test/lib/orgs.test.ts index e8056a45..a86a6bee 100644 --- a/test/lib/orgs.test.ts +++ b/test/lib/orgs.test.ts @@ -16,7 +16,7 @@ describe('Orgs API', () => { const requestManager = new requestsManager({ userAgentPrefix: 'snyk-api-import:tests', }); - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); diff --git a/test/scripts/generate-org-data.test.ts b/test/scripts/generate-org-data.test.ts index 015e5797..faac578b 100644 --- a/test/scripts/generate-org-data.test.ts +++ b/test/scripts/generate-org-data.test.ts @@ -9,7 +9,7 @@ describe('generateOrgImportDataFile Github script', () => { __dirname + '/group-groupIdExample-github-com-orgs.json', __dirname + '/group-groupIdExample-github-enterprise-orgs.json', ]; - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); @@ -142,7 +142,7 @@ describe('generateOrgImportDataFile Gitlab script', () => { const filesToCleanup: string[] = [ __dirname + '/group-groupIdExample-gitlab-orgs.json', ]; - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); @@ -233,7 +233,7 @@ describe('generateOrgImportDataFile Bitbucket Cloud script', () => { const filesToCleanup: string[] = [ __dirname + '/group-groupIdExample-bitbucket-cloud-orgs.json', ]; - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); diff --git a/test/system/help.test.ts b/test/system/help.test.ts index 5901ee23..77d78e5d 100644 --- a/test/system/help.test.ts +++ b/test/system/help.test.ts @@ -7,7 +7,7 @@ describe('`snyk-api-import help <...>`', () => { process.env.SNYK_API = process.env.SNYK_API_TEST; process.env.SNYK_TOKEN = process.env.SNYK_TOKEN_TEST; - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); it('Shows help text as expected', (done) => { diff --git a/test/system/import:data.test.ts b/test/system/import:data.test.ts index 5ed05680..21502f46 100644 --- a/test/system/import:data.test.ts +++ b/test/system/import:data.test.ts @@ -6,7 +6,7 @@ const main = './dist/index.js'.replace(/\//g, sep); describe('`snyk-api-import import:data <...>`', () => { const OLD_ENV = process.env; - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); it('Shows help text as expected', (done) => { diff --git a/test/system/list:imported.test.ts b/test/system/list:imported.test.ts index 0103aad6..aa7d146b 100644 --- a/test/system/list:imported.test.ts +++ b/test/system/list:imported.test.ts @@ -11,7 +11,7 @@ describe('`snyk-api-import list:imported <...>`', () => { const GROUP_ID = process.env.TEST_GROUP_ID as string; const ORG_ID = process.env.TEST_ORG_ID as string; - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); it('Shows help text as expected', (done) => { diff --git a/test/system/orgs:create.test.ts b/test/system/orgs:create.test.ts index e538a9cb..95a6b6fe 100644 --- a/test/system/orgs:create.test.ts +++ b/test/system/orgs:create.test.ts @@ -9,7 +9,7 @@ describe('`snyk-api-import help <...>`', () => { const OLD_ENV = process.env; const GROUP_ID = process.env.TEST_GROUP_ID as string; - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); it('Shows help text as expected', (done) => { diff --git a/test/system/orgs:data/bitbucket.test.ts b/test/system/orgs:data/bitbucket.test.ts index 7369f03f..d27af136 100644 --- a/test/system/orgs:data/bitbucket.test.ts +++ b/test/system/orgs:data/bitbucket.test.ts @@ -5,7 +5,7 @@ const main = './dist/index.js'.replace(/\//g, path.sep); describe('General `snyk-api-import orgs:data <...>`', () => { const OLD_ENV = process.env; - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); it('Generates orgs data as expected for Bitbucket Server', (done) => { diff --git a/test/system/orgs:data/errors.test.ts b/test/system/orgs:data/errors.test.ts index 6572a5fe..13404862 100644 --- a/test/system/orgs:data/errors.test.ts +++ b/test/system/orgs:data/errors.test.ts @@ -4,7 +4,7 @@ const main = './dist/index.js'.replace(/\//g, path.sep); describe('General `snyk-api-import orgs:data <...>`', () => { const OLD_ENV = process.env; - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); it('Shows error when missing groupId', (done) => { diff --git a/test/system/orgs:data/generic.test.ts b/test/system/orgs:data/generic.test.ts index 51d03f39..ad987709 100644 --- a/test/system/orgs:data/generic.test.ts +++ b/test/system/orgs:data/generic.test.ts @@ -4,7 +4,7 @@ const main = './dist/index.js'.replace(/\//g, path.sep); describe('General `snyk-api-import orgs:data <...>`', () => { const OLD_ENV = process.env; - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); it('Shows help text as expected', (done) => { diff --git a/test/system/orgs:data/github.test.ts b/test/system/orgs:data/github.test.ts index 4c141591..c3ce16bf 100644 --- a/test/system/orgs:data/github.test.ts +++ b/test/system/orgs:data/github.test.ts @@ -5,7 +5,7 @@ const main = './dist/index.js'.replace(/\//g, path.sep); describe('General `snyk-api-import orgs:data <...>`', () => { const OLD_ENV = process.env; - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); it('Generates orgs data as expected', (done) => { diff --git a/test/system/orgs:data/gitlab.test.ts b/test/system/orgs:data/gitlab.test.ts index c3a0e627..4c6f4e64 100644 --- a/test/system/orgs:data/gitlab.test.ts +++ b/test/system/orgs:data/gitlab.test.ts @@ -5,7 +5,7 @@ const main = './dist/index.js'.replace(/\//g, path.sep); describe('General `snyk-api-import orgs:data <...>`', () => { const OLD_ENV = process.env; - afterAll(async () => { + afterAll(() => { process.env = { ...OLD_ENV }; }); it('Generates orgs data as expected for Gitlab', (done) => { From 38d7bf41b85d56768e1a4a30221457421cc76a0e Mon Sep 17 00:00:00 2001 From: ghe Date: Sat, 15 Oct 2022 13:24:01 +0100 Subject: [PATCH 4/5] feat: sync all projects in a given Org for Github Sync all projects in a given org for Github.com Co-authored-by: mathild3r --- package.json | 3 +- src/lib/api/feature-flags/index.ts | 9 +- src/lib/api/org/index.ts | 12 +- src/lib/project/compare-branches.ts | 11 +- src/lib/types.ts | 4 + src/scripts/sync/sync-org-projects.ts | 174 ++++++ src/scripts/sync/sync-projects-per-target.ts | 56 ++ test/lib/org.test.ts | 1 - test/lib/project/compare-branches.test.ts | 30 +- .../project/update-project-per-target.test.ts | 101 ++++ test/scripts/import-projects.test.ts | 3 +- test/scripts/sync/sync-org-projects.test.ts | 539 ++++++++++++++++++ 12 files changed, 926 insertions(+), 17 deletions(-) create mode 100644 src/scripts/sync/sync-org-projects.ts create mode 100644 src/scripts/sync/sync-projects-per-target.ts create mode 100644 test/lib/project/update-project-per-target.test.ts create mode 100644 test/scripts/sync/sync-org-projects.test.ts diff --git a/package.json b/package.json index 1c55635b..f687e8f8 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,8 @@ "semantic-release": "17.3.0", "ts-jest": "27.0.3", "tsc-watch": "^4.1.0", - "typescript": "4.3.5" + "typescript": "4.3.5", + "uuid": "9.0.0" }, "pkg": { "scripts": [ diff --git a/src/lib/api/feature-flags/index.ts b/src/lib/api/feature-flags/index.ts index ac7dc0ee..45a7f252 100644 --- a/src/lib/api/feature-flags/index.ts +++ b/src/lib/api/feature-flags/index.ts @@ -18,12 +18,17 @@ export async function getFeatureFlag( url: url, useRESTApi: false, }); + debug(`Feature flag ${featureFlagName} is enabled for Org ${orgId}`); - return res.data['ok']; + + const enabled: boolean = res.data['ok']; + + return enabled; } catch (err) { if (err instanceof Error) { //Currently this is the only way to distinguish between an actual 403 and a 403 that is returned when an org hasn't got that FF enabled - if (JSON.stringify(err).search('"ok":false') > 0) { + const errorMessage = JSON.stringify(err); + if (errorMessage.includes('"ok":false')) { debug( `Feature flag ${featureFlagName} is not enabled for Org ${orgId}, please advise with your Snyk representative`, ); diff --git a/src/lib/api/org/index.ts b/src/lib/api/org/index.ts index 92b07914..16997217 100644 --- a/src/lib/api/org/index.ts +++ b/src/lib/api/org/index.ts @@ -139,7 +139,7 @@ export async function deleteOrg( return res.data; } -interface ProjectsResponse { +export interface ProjectsResponse { org: { id: string; }; @@ -166,6 +166,7 @@ interface ProjectsFilters { origin?: string; //If supplied, only projects that exactly match this origin will be returned type?: string; //If supplied, only projects that exactly match this type will be returned isMonitored?: boolean; // If set to true, only include projects which are monitored, if set to false, only include projects which are not monitored + targetId?: string; // The target ID to be used in sunc functions } export async function listProjects( @@ -175,8 +176,7 @@ export async function listProjects( ): Promise { getApiToken(); getSnykHost(); - debug(`Listing all projects for org: ${orgId}`); - + debug(`Listing all projects for org: ${orgId} with filter ${JSON.stringify(filters)}`); if (!orgId) { throw new Error( `Missing required parameters. Please ensure you have set: orgId, settings. @@ -280,7 +280,7 @@ function convertToSnykProject(projectData: RESTProjectData[]): SnykProject[] { return projects; } -interface TargetFilters { +export interface TargetFilters { remoteUrl?: string; limit?: number; isPrivate?: boolean; @@ -304,12 +304,12 @@ export async function listTargets( ); } - const targets = await listAllSnykTarget(requestManager, orgId, config); + const targets = await listAllSnykTargets(requestManager, orgId, config); return { targets }; } -export async function listAllSnykTarget( +export async function listAllSnykTargets( requestManager: requestsManager, orgId: string, config?: TargetFilters, diff --git a/src/lib/project/compare-branches.ts b/src/lib/project/compare-branches.ts index 9a071086..01caddb4 100644 --- a/src/lib/project/compare-branches.ts +++ b/src/lib/project/compare-branches.ts @@ -10,15 +10,20 @@ export async function compareAndUpdateBranches( }, defaultBranch: string, orgId: string, + dryRun = false, ): Promise<{ updated: boolean }> { - const {branch, projectPublicId} = project; + const { branch, projectPublicId } = project; let updated = false try { if (branch != defaultBranch) { debug(`Default branch has changed for Snyk project ${projectPublicId}`); - await updateProject(requestManager, orgId, projectPublicId, { branch: defaultBranch }); - updated = true + if (!dryRun) { + await updateProject(requestManager, orgId, project.projectPublicId, { + branch: defaultBranch, + }); + } + updated = true; } return { updated } diff --git a/src/lib/types.ts b/src/lib/types.ts index 31d15b0b..014b8bfb 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -91,6 +91,10 @@ export enum SupportedIntegrationTypesImportOrgData { BITBUCKET_CLOUD = 'bitbucket-cloud', } +export enum SupportedIntegrationTypesUpdateProject { + GITHUB = 'github', +} + // used to generate imported targets that exist in Snyk // when we need to grab the integrationId from Snyk export enum SupportedIntegrationTypesToListSnykTargets { diff --git a/src/scripts/sync/sync-org-projects.ts b/src/scripts/sync/sync-org-projects.ts new file mode 100644 index 00000000..8e34f3d8 --- /dev/null +++ b/src/scripts/sync/sync-org-projects.ts @@ -0,0 +1,174 @@ +import pMap = require('p-map'); +import * as debugLib from 'debug'; +import * as path from 'path'; +import { requestsManager } from 'snyk-request-manager'; +import { UPDATED_PROJECTS_LOG_NAME } from '../../common'; +import type { + TargetFilters, +} from '../../lib'; +import { + getLoggingPath, + listProjects, + listTargets, +} from '../../lib'; +import { getFeatureFlag } from '../../lib/api/feature-flags'; +import type { + SnykProject, + SnykTarget, +} from '../../lib/types'; +import { + SupportedIntegrationTypesUpdateProject, +} from '../../lib/types'; +import { logUpdatedProjects } from '../../loggers/log-updated-project'; +import { updateProjectForTarget as updateProjectForTarget } from './sync-projects-per-target'; + +const debug = debugLib('snyk:sync-org-projects'); + +export async function updateOrgTargets( + publicOrgId: string, + sources: SupportedIntegrationTypesUpdateProject[], + dryRun = false, +): Promise<{ + fileName: string; + processedTargets: number; + meta: { + projects: { + branchUpdated: string[]; + }; + }; +}> { + const branchUpdated: string[] = []; + const logFile = path.resolve(getLoggingPath(), UPDATED_PROJECTS_LOG_NAME); + const res = { + fileName: logFile, + processedTargets: 0, + meta: { + projects: { + branchUpdated, + }, + }, + }; + + // ensure source is enabled for sync + const allowedSources = sources.filter((source) => + Object.values(SupportedIntegrationTypesUpdateProject).includes(source), + ); + if (!allowedSources.length) { + console.warn( + `The organization (${publicOrgId}) does not have any projects that are supported for sync. Currently supported projects are ${Object.values( + SupportedIntegrationTypesUpdateProject, + ).join(',')}`, + ); + return res; + } + + const requestManager = new requestsManager({ + userAgentPrefix: 'snyk-api-import', + period: 1000, + maxRetryCount: 3, + }); + + if (await getFeatureFlag(requestManager, 'customBranch', publicOrgId)) { + console.warn( + 'Detected custom branches are used in this organization. Skipping syncing organization ${publicOrgId}', + ); + return res; + } + + await pMap( + allowedSources, + async (source: SupportedIntegrationTypesUpdateProject) => { + const filters: TargetFilters = { + limit: 100, + origin: source, + excludeEmpty: true, + }; + debug(`Listing all targets for source ${source}`); + const { targets } = await listTargets( + requestManager, + publicOrgId, + filters, + ); + debug(`Syncing targets for source ${source}`); + const updated = await updateTargets( + requestManager, + publicOrgId, + targets, + dryRun, + ); + res.processedTargets += updated.processedTargets; + res.meta.projects.branchUpdated.push(...updated.meta.projects.branchUpdated); + debug(`Logging updated targets for source ${source}`); + // TODO: add a test to ensure a log was created & is the expected format + await logUpdatedProjects(publicOrgId, branchUpdated, logFile); + }, + { concurrency: 3 }, + ); + return res; +} + +export async function updateTargets( + requestManager: requestsManager, + orgId: string, + targets: SnykTarget[], + dryRun = false, +): Promise<{ + processedTargets: number; + meta: { + projects: { + branchUpdated: string[]; + }; + }; +}> { + let processedTargets = 0; + const updated: string[] = []; + + await pMap( + targets, + async (target: SnykTarget) => { + try { + const filters = { targetId: target.id }; + debug(`Listing projects for target ${target.attributes.displayName}`); + const { projects } = await listProjects(requestManager, orgId, filters); + debug(`Syncing projects for target ${target.attributes.displayName}`); + const { updatedProjects } = await syncAllProjects(requestManager, orgId, projects, dryRun); + updated.push(...updatedProjects); + processedTargets += 1; + } catch (e) { + debug(e); + console.warn(`Failed to sync target ${target.attributes.displayName}. ERROR: ${e.message}`) + } + }, + { concurrency: 10 }, + ); + return { + processedTargets, + // TODO: collect failed targets & log them with reason? + meta: { + projects: { + branchUpdated: updated, + }, + }, + }; +} + +async function syncAllProjects( + requestManager: requestsManager, + orgId: string, + projects: SnykProject[], + dryRun = false): Promise<{ updatedProjects: string[] }> { + const updatedProjects: string[] = []; + await pMap(projects, async (project) => { + const { updated } = await updateProjectForTarget( + requestManager, + orgId, + project, + dryRun, + ); + if (updated) { + updatedProjects.push(project.id); + } + }); + + return { updatedProjects }; +} diff --git a/src/scripts/sync/sync-projects-per-target.ts b/src/scripts/sync/sync-projects-per-target.ts new file mode 100644 index 00000000..98cb2deb --- /dev/null +++ b/src/scripts/sync/sync-projects-per-target.ts @@ -0,0 +1,56 @@ +import type { requestsManager } from 'snyk-request-manager'; +import * as debugLib from 'debug'; + +import { getGithubReposDefaultBranch } from '../../lib/source-handlers/github'; +import { compareAndUpdateBranches } from '../../lib/project/compare-branches'; +import type { + SnykProject, Target, +} from '../../lib/types'; +import { + SupportedIntegrationTypesUpdateProject, +} from '../../lib/types'; +import { targetGenerators } from '../generate-imported-targets-from-snyk'; +const debug = debugLib('snyk:sync-projects-per-target'); + +export function getBranchGenerator( + origin: SupportedIntegrationTypesUpdateProject, +): (target: Target, host?: string | undefined) => Promise { + const getDefaultBranchGenerators = { + [SupportedIntegrationTypesUpdateProject.GITHUB]: getGithubReposDefaultBranch, + }; + return getDefaultBranchGenerators[origin]; +} + +export async function updateProjectForTarget( + requestManager: requestsManager, + orgId: string, + project: SnykProject, + dryRun = false, // TODO: add a test for this function + this param +): Promise<{ updated: boolean }> { + let defaultBranch; + const origin = project.origin as SupportedIntegrationTypesUpdateProject; + + try { + const target = targetGenerators[origin](project); + defaultBranch = await getBranchGenerator(origin)(target); + } catch (e) { + debug(`Getting default branch failed with error: ${e}`); + } + + if (!defaultBranch) { + return { updated: false }; + } + + const { updated } = await compareAndUpdateBranches( + requestManager, + { + branch: project.branch!, + projectPublicId: project.id, + }, + defaultBranch, + orgId, + dryRun, + ); + + return { updated }; +} diff --git a/test/lib/org.test.ts b/test/lib/org.test.ts index f0808cf1..aebb81a0 100644 --- a/test/lib/org.test.ts +++ b/test/lib/org.test.ts @@ -97,7 +97,6 @@ describe('listTargets', () => { jest.restoreAllMocks(); }); - it('list the targets in a given Org without pagination - mock', async () => { jest.spyOn(requestManager, 'request').mockResolvedValue({ statusCode: 200, diff --git a/test/lib/project/compare-branches.test.ts b/test/lib/project/compare-branches.test.ts index 0fc34742..935b2bc4 100644 --- a/test/lib/project/compare-branches.test.ts +++ b/test/lib/project/compare-branches.test.ts @@ -11,10 +11,14 @@ describe('compareAndUpdateBranches', () => { process.env.SNYK_TOKEN = process.env.SNYK_TOKEN_TEST; }); afterAll(() => { + jest.clearAllMocks(); + process.env = { ...OLD_ENV }; + }, 1000); + afterEach(() => { jest.resetAllMocks(); process.env = { ...OLD_ENV }; }, 1000); - it('Update project branch', async () => { + it('updates project branch if the default branch changed', async () => { jest.spyOn(requestManager, 'request').mockResolvedValue({ data: { name: 'test', @@ -59,7 +63,27 @@ describe('compareAndUpdateBranches', () => { expect(res.updated).toBeTruthy(); }, 5000); - it('Return ProjectNeededUpdate false if branches are the same', async () => { + it('does not call the Projects API in dryRun mode', async () => { + const requestSpy = jest.spyOn(requestManager, 'request').mockResolvedValue({ + data: {}, + status: 200, + }); + + const res = await compareProject.compareAndUpdateBranches( + requestManager, + { + branch: 'main', + projectPublicId: 'af137b96-6966-46c1-826b-2e79ac49bbd9', + }, + 'newDefaultBranch', + 'af137b96-6966-46c1-826b-2e79ac49bbxx', + true, + ); + expect(requestSpy).not.toHaveBeenCalled(); + expect(res.updated).toBeTruthy(); + }, 5000); + + it('does not update the project if the branches are the same', async () => { const res = await compareProject.compareAndUpdateBranches( requestManager, { @@ -72,7 +96,7 @@ describe('compareAndUpdateBranches', () => { expect(res.updated).toBeFalsy(); }, 5000); - it('throw if the api requests fails', async () => { + it('throws if the api requests fails', async () => { jest .spyOn(requestManager, 'request') .mockResolvedValue({ statusCode: 500, data: {} }); diff --git a/test/lib/project/update-project-per-target.test.ts b/test/lib/project/update-project-per-target.test.ts new file mode 100644 index 00000000..2623ecae --- /dev/null +++ b/test/lib/project/update-project-per-target.test.ts @@ -0,0 +1,101 @@ +import { requestsManager } from 'snyk-request-manager'; +import { updateProjectForTarget } from '../../../src/scripts/sync/sync-projects-per-target'; +import * as github from '../../../src/lib/source-handlers/github'; +import * as projects from '../../../src/lib/api/project'; + +describe('UpdateProject (Github)', () => { + const requestManager = new requestsManager({ + userAgentPrefix: 'snyk-api-import:tests', + }); + let githubSpy: jest.SpyInstance; + let projectsSpy: jest.SpyInstance; + + beforeAll(() => { + githubSpy = jest.spyOn(github, 'getGithubReposDefaultBranch'); + projectsSpy = jest.spyOn(projects, 'updateProject'); + }); + + afterAll(async () => { + jest.restoreAllMocks(); + }, 1000); + + beforeEach(async () => { + jest.clearAllMocks(); + }, 1000); + + it('updates project when default branch changed', async () => { + const defaultBranch = 'newBranch'; + githubSpy.mockImplementation(() => Promise.resolve(defaultBranch)); + projectsSpy.mockImplementation(() => + Promise.resolve({ ...testProject, branch: defaultBranch }), + ); + const testProject = { + name: 'testProject', + id: 'af137b96-6966-46c1-826b-2e79ac49bbxx', + created: '2018-10-29T09:50:54.014Z', + origin: 'github', + type: 'npm', + branch: 'master', + }; + + const res = await updateProjectForTarget( + requestManager, + 'af137b96-6966-46c1-826b-2e79ac49bbxx', + testProject, + ); + expect(githubSpy).toHaveBeenCalledTimes(1); + expect(projectsSpy).toHaveBeenCalledTimes(1); + + expect(res.updated).toBeTruthy(); + }, 5000); + + it('fails to update project, if API call to get default branch fails', async () => { + const defaultBranch = 'newBranch'; + githubSpy.mockImplementation(() => + Promise.reject({ statusCode: 500, message: 'Error' }), + ); + projectsSpy.mockImplementation(() => + Promise.resolve({ ...testProject, branch: defaultBranch }), + ); + const testProject = { + name: 'testProject', + id: 'af137b96-6966-46c1-826b-2e79ac49bbxx', + created: '2018-10-29T09:50:54.014Z', + origin: 'github', + type: 'npm', + branch: 'master', + }; + expect( + updateProjectForTarget( + requestManager, + 'af137b96-6966-46c1-826b-2e79ac49bbxx', + testProject, + ), + ).rejects.toThrow(); + expect(githubSpy).toHaveBeenCalledTimes(1); + expect(projectsSpy).not.toHaveBeenCalled(); + }, 5000); + + it('does nothing if the default branch did no change', async () => { + jest + .spyOn(github, 'getGithubReposDefaultBranch') + .mockResolvedValue('master'); + + const testProject = { + name: 'testProject', + id: 'af137b96-6966-46c1-826b-2e79ac49bbxx', + created: '2018-10-29T09:50:54.014Z', + origin: 'github', + type: 'npm', + branch: 'master', + }; + + const res = await updateProjectForTarget( + requestManager, + 'af137b96-6966-46c1-826b-2e79ac49bbxx', + testProject, + ); + expect(res.updated).toBeFalsy(); + expect(projectsSpy).not.toHaveBeenCalled(); + }, 5000); +}); diff --git a/test/scripts/import-projects.test.ts b/test/scripts/import-projects.test.ts index 1ec37752..3e162a17 100644 --- a/test/scripts/import-projects.test.ts +++ b/test/scripts/import-projects.test.ts @@ -56,7 +56,8 @@ describe('Import skips previously imported', () => { process.env.INTEGRATION_ID = 'INTEGRATION_ID'; process.env.ORG_ID = 'ORG_ID'; - afterAll(async () => { + afterAll(() => { + jest.restoreAllMocks(); process.env = { ...OLD_ENV }; }, 1000); it('succeeds to import targets from file with import log', async () => { diff --git a/test/scripts/sync/sync-org-projects.test.ts b/test/scripts/sync/sync-org-projects.test.ts new file mode 100644 index 00000000..993ed192 --- /dev/null +++ b/test/scripts/sync/sync-org-projects.test.ts @@ -0,0 +1,539 @@ +import { requestsManager } from 'snyk-request-manager'; +import * as uuid from 'uuid'; +import { updateOrgTargets, updateTargets } from '../../../src/scripts/sync/sync-org-projects'; +import type { ProjectsResponse } from '../../../src/lib/api/org'; +import * as updateProjectForTarget from '../../../src/scripts/sync/sync-projects-per-target'; +import type { SnykProject, SnykTarget, SnykTargetRelationships } from '../../../src/lib/types'; +import { SupportedIntegrationTypesUpdateProject } from '../../../src/lib/types'; +import * as lib from '../../../src/lib'; +import * as projectApi from '../../../src/lib/api/project'; +import * as github from '../../../src/lib/source-handlers/github'; +import * as featureFlags from '../../../src/lib/api/feature-flags'; +import * as updateProjectsLog from '../../../src/loggers/log-updated-project'; + +describe('updateTargets', () => { + const requestManager = new requestsManager({ + userAgentPrefix: 'snyk-api-import:tests', + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('Github', () => { + + it('updates a projects branch if default branch changed', async () => { + const testTarget = [ + { + attributes: { + displayName: 'test', + isPrivate: false, + origin: 'github', + remoteUrl: null, + }, + id: 'af137b96-6966-46c1-826b-2e79ac49bbxx', + relationships: { + org: { + data: { + id: 'af137b96-6966-46c1-826b-2e79ac49bbxx', + type: 'org', + }, + links: {}, + meta: {}, + }, + }, + type: 'target', + }, + ]; + + const testProjects: ProjectsResponse = { + org: { + id: "af137b96-6966-46c1-826b-2e79ac49bbxx", + }, + projects: [{ + name: 'testProject', + id: 'af137b96-6966-46c1-826b-2e79ac49bbxx', + created: '2018-10-29T09:50:54.014Z', + origin: 'github', + type: 'npm', + branch: 'master', + }] + } + + const orgId = 'af137b96-6966-46c1-826b-2e79ac49bbxx'; + + jest.spyOn(lib, 'listProjects').mockImplementation(() => Promise.resolve(testProjects)); + jest.spyOn(updateProjectForTarget, 'updateProjectForTarget').mockImplementation(() => Promise.resolve({ updated: true })); + + const res = await updateTargets(requestManager, orgId, testTarget); + + expect(res.processedTargets).toEqual(1); + expect(res.meta.projects.branchUpdated).toEqual( + ['af137b96-6966-46c1-826b-2e79ac49bbxx',] + ); + }, 5000); + + it('did not need to update a projects branch', async () => { + const testTarget = [ + { + attributes: { + displayName: 'test', + isPrivate: false, + origin: 'github', + remoteUrl: null, + }, + id: 'af137b96-6966-46c1-826b-2e79ac49bbxx', + relationships: { + org: { + data: { + id: 'af137b96-6966-46c1-826b-2e79ac49bbxx', + type: 'org', + }, + links: {}, + meta: {}, + }, + }, + type: 'target', + }, + ]; + + const testProjects: ProjectsResponse = { + org: { + id: "af137b96-6966-46c1-826b-2e79ac49bbxx", + }, + projects: [{ + name: 'testProject', + id: 'af137b96-6966-46c1-826b-2e79ac49bbxx', + created: '2018-10-29T09:50:54.014Z', + origin: 'github', + type: 'npm', + branch: 'master', + }] + } + + const orgId = 'af137b96-6966-46c1-826b-2e79ac49bbxx'; + + jest.spyOn(lib, 'listProjects').mockImplementation(() => Promise.resolve(testProjects)); + jest.spyOn(updateProjectForTarget, 'updateProjectForTarget').mockImplementation(() => Promise.resolve({ updated: false })); + + const res = await updateTargets(requestManager, orgId, testTarget); + + expect(res.processedTargets).toEqual(1); + expect(res.meta.projects.branchUpdated).toEqual( + [] + ); + }, 5000); + + it('updates several projects from the same target 1 failed 1 success', async () => { + const testTargets = [ + { + attributes: { + displayName: 'test', + isPrivate: false, + origin: 'github', + remoteUrl: null, + }, + id: 'af137b96-6966-46c1-826b-2e79ac49bbxx', + relationships: { + org: { + data: { + id: 'af137b96-6966-46c1-826b-2e79ac49bbxx', + type: 'org', + }, + links: {}, + meta: {}, + }, + }, + type: 'target', + }, + ]; + + const testProjects: ProjectsResponse = { + org: { + id: "af137b96-6966-46c1-826b-2e79ac49bbxx", + }, + projects: [{ + name: 'testProject', + id: 'af137b96-6966-46c1-826b-2e79ac49bbxx', + created: '2018-10-29T09:50:54.014Z', + origin: 'github', + type: 'npm', + branch: 'master', + }, { + name: 'testProject2', + id: 'af137b96-6966-46c1-826b-2e79ac49aaxx', + created: '2018-10-29T09:50:54.014Z', + origin: 'github', + type: 'maven', + branch: 'master', + }] + } + + const orgId = 'af137b96-6966-46c1-826b-2e79ac49bbxx'; + + jest.spyOn(lib, 'listProjects').mockImplementationOnce(() => Promise.resolve(testProjects)) + jest.spyOn(updateProjectForTarget, 'updateProjectForTarget').mockImplementationOnce(() => Promise.resolve({ updated: true })).mockImplementationOnce(() => Promise.resolve({ updated: false })); + + const res = await updateTargets(requestManager, orgId, testTargets); + + expect(res.processedTargets).toEqual(1); + expect(res.meta.projects.branchUpdated).toEqual( + ['af137b96-6966-46c1-826b-2e79ac49bbxx',] + ); + }, 5000); + }); +}); +describe('updateOrgTargets', () => { + const OLD_ENV = process.env; + process.env.SNYK_LOG_PATH = './'; + process.env.SNYK_TOKEN = 'dummy' + + let featureFlagsSpy: jest.SpyInstance; + let listTargetsSpy: jest.SpyInstance; + let listProjectsSpy: jest.SpyInstance; + let logUpdatedProjectsSpy: jest.SpyInstance; + let githubSpy: jest.SpyInstance; + let updateProjectSpy: jest.SpyInstance; + + beforeAll(() => { + featureFlagsSpy = jest.spyOn(featureFlags, 'getFeatureFlag'); + listTargetsSpy = jest.spyOn(lib, 'listTargets'); + listProjectsSpy = jest.spyOn(lib, 'listProjects'); + logUpdatedProjectsSpy = jest.spyOn(updateProjectsLog, 'logUpdatedProjects'); + githubSpy = jest.spyOn(github, 'getGithubReposDefaultBranch'); + updateProjectSpy = jest.spyOn(projectApi, 'updateProject'); + }); + afterAll(() => { + jest.restoreAllMocks(); + }, 1000); + + afterEach(() => { + process.env = { ...OLD_ENV }; + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Github', () => { + it('skip unsupported origins', async () => { + const res = await updateOrgTargets('xxx', ['unsupported' as any]); + expect(res).toEqual({ + "fileName": expect.stringMatching("/updated-projects.log"), + "meta": { + "projects": { + "branchUpdated": [], + }, + }, + "processedTargets": 0 + }) + }); + it('skip an org that uses the customBranch FF', async () => { + featureFlagsSpy.mockResolvedValue(true); + logUpdatedProjectsSpy.mockResolvedValue(null); + const res = await updateOrgTargets('xxx', ['unsupported' as any]); + expect(res).toEqual({ + "fileName": expect.stringMatching("/updated-projects.log"), + "meta": { + "projects": { + "branchUpdated": [], + }, + }, + "processedTargets": 0 + }) + }); + + it('throws error if listTargets has API error', async () => { + featureFlagsSpy.mockResolvedValue(false) + listTargetsSpy.mockRejectedValue('Expected a 200 response, instead received:' + JSON.stringify({ statusCode: 500, message: 'Something went wrong' })) + expect(updateOrgTargets('xxx', [SupportedIntegrationTypesUpdateProject.GITHUB])).rejects.toThrowError('Expected a 200 response, instead received'); + }); + it('skips target if listingProjects has API error', async () => { + const targets: SnykTarget[] = [{ + attributes: { + displayName: 'foo/bar', + isPrivate: true, + origin: 'github', + remoteUrl: null, + }, + id: 'xxx', + relationships: {} as unknown as SnykTargetRelationships, + type: 'target', + }] + featureFlagsSpy.mockResolvedValue(false); + listTargetsSpy.mockResolvedValue({ targets }); + listProjectsSpy.mockRejectedValue('Expected a 200 response, instead received:' + JSON.stringify({ statusCode: 500, message: 'Something went wrong' })); + logUpdatedProjectsSpy.mockResolvedValue(null); + + const res = await updateOrgTargets('xxx', [SupportedIntegrationTypesUpdateProject.GITHUB]); + expect(res).toEqual({ + "fileName": expect.stringMatching("/updated-projects.log"), + "meta": { + "projects": { + "branchUpdated": [], + }, + }, + "processedTargets": 0, + }); + }); + it('skips target & projects error if getting default branch fails', async () => { + const targets: SnykTarget[] = [{ + attributes: { + displayName: 'foo/bar', + isPrivate: true, + origin: 'github', + remoteUrl: null, + }, + id: 'xxx', + relationships: {} as unknown as SnykTargetRelationships, + type: 'target', + }]; + const projects: SnykProject[] = [{ + name: 'example', + id: '123', + created: 'date', + origin: 'github', + type: 'npm', + branch: 'main' + }]; + featureFlagsSpy.mockResolvedValue(false); + listTargetsSpy.mockResolvedValue({ targets }); + listProjectsSpy.mockRejectedValue(projects); + logUpdatedProjectsSpy.mockResolvedValue(null); + + const res = await updateOrgTargets('xxx', [SupportedIntegrationTypesUpdateProject.GITHUB]); + expect(res).toEqual({ + "fileName": expect.stringMatching("/updated-projects.log"), + "meta": { + "projects": { + "branchUpdated": [], + }, + }, + "processedTargets": 0, + }); + }); + + // TODO: needs more work, updateProject spy is calling real function still. + it('Successfully updated several targets (1 supported, 1 unsupported)', async () => { + const targets: SnykTarget[] = [{ + attributes: { + displayName: 'snyk/bar', + isPrivate: true, + origin: 'github', + remoteUrl: null, + }, + id: uuid.v4(), + relationships: {} as unknown as SnykTargetRelationships, + type: 'target', + }, + { + attributes: { + displayName: 'snyk/cli', + isPrivate: true, + origin: 'github-enterprise', + remoteUrl: null, + }, + id: uuid.v4(), + relationships: {} as unknown as SnykTargetRelationships, + type: 'target', + }]; + const updatedProjectId = uuid.v4(); + const projects: SnykProject[] = [{ + name: 'example', + id: updatedProjectId, + created: 'date', + origin: 'github', + type: 'npm', + branch: 'main' + }]; + featureFlagsSpy.mockResolvedValue(false); + listTargetsSpy.mockResolvedValue({ targets }); + listProjectsSpy.mockResolvedValueOnce({ projects }); + listProjectsSpy.mockResolvedValue({ projects: [] }); + logUpdatedProjectsSpy.mockResolvedValue(null); + githubSpy.mockResolvedValue('develop'); + updateProjectSpy.mockResolvedValue(''); + + const res = await updateOrgTargets('xxx', [SupportedIntegrationTypesUpdateProject.GITHUB]); + expect(res).toEqual({ + "fileName": expect.stringMatching("/updated-projects.log"), + "meta": { + "projects": { + "branchUpdated": [updatedProjectId], + }, + }, + "processedTargets": 2, + }); + }); + it('Some projects fail to update in a target', async () => { + const targets: SnykTarget[] = [{ + attributes: { + displayName: 'snyk/bar', + isPrivate: true, + origin: 'github', + remoteUrl: null, + }, + id: uuid.v4(), + relationships: {} as unknown as SnykTargetRelationships, + type: 'target', + }]; + const updatedProjectId = uuid.v4(); + const projects: SnykProject[] = [{ + name: 'snyk/bar', + id: updatedProjectId, + created: 'date', + origin: 'github', + type: 'npm', + branch: 'main' + }, + { + name: 'snyk/foo', + id: uuid.v4(), + created: 'date', + origin: 'github', + type: 'yarn', + branch: 'develop' + }]; + featureFlagsSpy.mockResolvedValueOnce(false); + listTargetsSpy.mockResolvedValueOnce({ targets }); + listProjectsSpy.mockResolvedValueOnce({ projects }); + logUpdatedProjectsSpy.mockResolvedValueOnce(null); + githubSpy.mockResolvedValueOnce('develop'); + githubSpy.mockRejectedValueOnce('Failed to get default branch from Github'); + updateProjectSpy.mockResolvedValue(''); + + const res = await updateOrgTargets('xxx', [SupportedIntegrationTypesUpdateProject.GITHUB]); + expect(res).toEqual({ + "fileName": expect.stringMatching("/updated-projects.log"), + "meta": { + "projects": { + "branchUpdated": [updatedProjectId], + }, + }, + "processedTargets": 1, + }); + }); + it('Successfully updated several targets', async () => { + const targets: SnykTarget[] = [{ + attributes: { + displayName: 'snyk/bar', + isPrivate: true, + origin: 'github', + remoteUrl: null, + }, + id: uuid.v4(), + relationships: {} as unknown as SnykTargetRelationships, + type: 'target', + }, { + attributes: { + displayName: 'snyk/foo', + isPrivate: false, + origin: 'github', + remoteUrl: null, + }, + id: uuid.v4(), + relationships: {} as unknown as SnykTargetRelationships, + type: 'target', + }]; + const updatedProjectId1 = uuid.v4(); + const updatedProjectId2 = uuid.v4(); + const projectsTarget1: SnykProject[] = [{ + name: 'snyk/bar', + id: updatedProjectId1, + created: 'date', + origin: 'github', + type: 'npm', + branch: 'main' + }]; + const projectsTarget2: SnykProject[] = [ + { + name: 'snyk/foo', + id: updatedProjectId2, + created: 'date', + origin: 'github', + type: 'yarn', + branch: 'develop' + }]; + featureFlagsSpy.mockResolvedValueOnce(false); + listTargetsSpy.mockResolvedValueOnce({ targets }); + listProjectsSpy.mockResolvedValueOnce({ projects: projectsTarget1 }); + listProjectsSpy.mockResolvedValueOnce({ projects: projectsTarget2 }); + + logUpdatedProjectsSpy.mockResolvedValueOnce(null); + githubSpy.mockResolvedValue('new-branch'); + updateProjectSpy.mockResolvedValue(''); + + const res = await updateOrgTargets('xxx', [SupportedIntegrationTypesUpdateProject.GITHUB]); + expect(res).toEqual({ + "fileName": expect.stringMatching("/updated-projects.log"), + "meta": { + "projects": { + "branchUpdated": [updatedProjectId1, updatedProjectId2], + }, + }, + "processedTargets": 2, + }); + }); + it('Successfully updated several targets (dryRun mode)', async () => { + const targets: SnykTarget[] = [{ + attributes: { + displayName: 'snyk/bar', + isPrivate: true, + origin: 'github', + remoteUrl: null, + }, + id: uuid.v4(), + relationships: {} as unknown as SnykTargetRelationships, + type: 'target', + }, { + attributes: { + displayName: 'snyk/foo', + isPrivate: false, + origin: 'github', + remoteUrl: null, + }, + id: uuid.v4(), + relationships: {} as unknown as SnykTargetRelationships, + type: 'target', + }]; + const updatedProjectId1 = uuid.v4(); + const updatedProjectId2 = uuid.v4(); + const projectsTarget1: SnykProject[] = [{ + name: 'snyk/bar', + id: updatedProjectId1, + created: 'date', + origin: 'github', + type: 'npm', + branch: 'main' + }]; + const projectsTarget2: SnykProject[] = [ + { + name: 'snyk/foo', + id: updatedProjectId2, + created: 'date', + origin: 'github', + type: 'yarn', + branch: 'develop' + }]; + featureFlagsSpy.mockResolvedValueOnce(false); + listTargetsSpy.mockResolvedValueOnce({ targets }); + listProjectsSpy.mockResolvedValueOnce({ projects: projectsTarget1 }); + listProjectsSpy.mockResolvedValueOnce({ projects: projectsTarget2 }); + + logUpdatedProjectsSpy.mockResolvedValueOnce(null); + githubSpy.mockResolvedValue('new-branch'); + + const res = await updateOrgTargets('xxx', [SupportedIntegrationTypesUpdateProject.GITHUB], true); + expect(res).toEqual({ + "fileName": expect.stringMatching("/updated-projects.log"), + "meta": { + "projects": { + "branchUpdated": [updatedProjectId1, updatedProjectId2], + }, + }, + "processedTargets": 2, + }); + expect(updateProjectSpy).not.toHaveBeenCalled(); + }); + }); +}); From ec586ba8bc317e714394df232a3112f5635d8baa Mon Sep 17 00:00:00 2001 From: ghe Date: Thu, 20 Oct 2022 17:30:50 +0100 Subject: [PATCH 5/5] chore: refactor processing flag error --- src/lib/api/feature-flags/index.ts | 33 ++++++++------- test/lib/api/feature-flags/index.test.ts | 51 ++++++++++++++++++----- test/lib/project/compare-branches.test.ts | 2 +- 3 files changed, 58 insertions(+), 28 deletions(-) diff --git a/src/lib/api/feature-flags/index.ts b/src/lib/api/feature-flags/index.ts index 45a7f252..3f4d2cf9 100644 --- a/src/lib/api/feature-flags/index.ts +++ b/src/lib/api/feature-flags/index.ts @@ -24,22 +24,23 @@ export async function getFeatureFlag( const enabled: boolean = res.data['ok']; return enabled; - } catch (err) { - if (err instanceof Error) { - //Currently this is the only way to distinguish between an actual 403 and a 403 that is returned when an org hasn't got that FF enabled - const errorMessage = JSON.stringify(err); - if (errorMessage.includes('"ok":false')) { - debug( - `Feature flag ${featureFlagName} is not enabled for Org ${orgId}, please advise with your Snyk representative`, - ); - } else { - debug( - `Could not fetch the ${featureFlagName} feature flag for ${orgId}\n ${JSON.stringify( - err, - )}`, - ); - } + } catch (err: any) { + console.log(err.message) + const res = err.message?.response?.data; + const message = res?.userMessage || res?.message || err.message?.message; + + if (res && res.ok === false && res?.userMessage?.includes('feature enabled')) { + debug( + `Feature flag ${featureFlagName} is not enabled for Org ${orgId}`, + ); + return false; } - return false; + + debug( + `Could not fetch the ${featureFlagName} feature flag for ${orgId}\n ${JSON.stringify( + err, + )}`, + ); + throw new Error(message); } } diff --git a/test/lib/api/feature-flags/index.test.ts b/test/lib/api/feature-flags/index.test.ts index 4dee3e6a..b8cc9f18 100644 --- a/test/lib/api/feature-flags/index.test.ts +++ b/test/lib/api/feature-flags/index.test.ts @@ -3,17 +3,26 @@ import { getFeatureFlag } from '../../../../src/lib/api/feature-flags'; jest.unmock('snyk-request-manager'); jest.requireActual('snyk-request-manager'); -const orgId = process.env.TEST_ORG_ID as string; describe('getFeatureFlag', () => { const requestManager = new requestsManager({ userAgentPrefix: 'snyk-api-import:tests', maxRetryCount: 1, }); + const OLD_ENV = process.env; + const orgId = process.env.TEST_ORG_ID as string; + + beforeAll(() => { + process.env.SNYK_API = process.env.SNYK_API_TEST; + process.env.SNYK_TOKEN = process.env.SNYK_TOKEN_TEST; + }); afterAll(() => { jest.restoreAllMocks(); - }); - + process.env = { ...OLD_ENV }; + }, 1000); + afterEach(() => { + jest.resetAllMocks(); + }, 1000); it('get feature flag for org - mock', async () => { jest.spyOn(requestManager, 'request').mockResolvedValueOnce({ @@ -30,21 +39,41 @@ describe('getFeatureFlag', () => { expect(res).toBeTruthy(); }); - it('Error if the request fails with generic 403 - not a real Snyk org', async () => { - const res = await getFeatureFlag( - requestManager, - 'nonEnabledFeatureFlag', - '0000', - ); - expect(res).toBeFalsy(); + it('Error if the request fails with 404 - user has no access to an org', async () => { + const message = + 'Org test-org was not found or you may not have the correct permissions to access the org'; + const error = new Error('Request failed with status code 404'); + (error as any).message = { + response: { + data: { + ok: false, + message, + }, + }, + }; + jest.spyOn(requestManager, 'request').mockRejectedValue(error); + await expect( + getFeatureFlag(requestManager, 'nonEnabledFeatureFlag', '0000'), + ).rejects.toThrowError(); }, 20000); it('Error if the request fails with a 403 that indicates the FF is not enabled for a real org', async () => { + const error = new Error('Request failed with status code 403'); + (error as any).message = { + response: { + data: { + ok: false, + userMessage: + "Org test-comet doesn't have 'non-enabled-feature-flag' feature enabled", + }, + }, + }; + jest.spyOn(requestManager, 'request').mockRejectedValue(error); const res = await getFeatureFlag( requestManager, 'nonEnabledFeatureFlag', orgId, ); - expect(res).toBeFalsy(); + expect(res).toBe(false); }, 20000); }); diff --git a/test/lib/project/compare-branches.test.ts b/test/lib/project/compare-branches.test.ts index 935b2bc4..668e4a7a 100644 --- a/test/lib/project/compare-branches.test.ts +++ b/test/lib/project/compare-branches.test.ts @@ -11,7 +11,7 @@ describe('compareAndUpdateBranches', () => { process.env.SNYK_TOKEN = process.env.SNYK_TOKEN_TEST; }); afterAll(() => { - jest.clearAllMocks(); + jest.restoreAllMocks(); process.env = { ...OLD_ENV }; }, 1000); afterEach(() => {