From 1894bc2b2a34d1d5c87109f5f3e88466715fbe3c Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Fri, 24 Mar 2023 13:58:56 +0100 Subject: [PATCH] Add tests Added tests for `deployBlocker` workflow See: https://github.com/Expensify/App/issues/13604 --- .github/workflows/deployBlocker.yml | 6 +- .../assertions/deployBlockerAssertions.js | 105 +++++++++++ workflow_tests/createNewVersion.test.js | 2 +- workflow_tests/deployBlocker.test.js | 163 ++++++++++++++++++ workflow_tests/mocks/deployBlockerMocks.js | 71 ++++++++ workflow_tests/utils/preGenerateTest.js | 48 +++--- workflow_tests/utils/utils.js | 46 ++++- 7 files changed, 408 insertions(+), 33 deletions(-) create mode 100644 workflow_tests/assertions/deployBlockerAssertions.js create mode 100644 workflow_tests/deployBlocker.test.js create mode 100644 workflow_tests/mocks/deployBlockerMocks.js diff --git a/.github/workflows/deployBlocker.yml b/.github/workflows/deployBlocker.yml index 30e937722596..b001e0f3f643 100644 --- a/.github/workflows/deployBlocker.yml +++ b/.github/workflows/deployBlocker.yml @@ -11,7 +11,8 @@ jobs: if: ${{ github.event.label.name == 'DeployBlockerCash' }} steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + - name: Checkout + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} @@ -64,7 +65,8 @@ jobs: 2. Find someone who can quickly fix the issue. 3. Fix the issue yourself. - - if: ${{ failure() }} + - name: Announce failed workflow in Slack + if: ${{ failure() }} uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/workflow_tests/assertions/deployBlockerAssertions.js b/workflow_tests/assertions/deployBlockerAssertions.js new file mode 100644 index 000000000000..dbc1f18b737e --- /dev/null +++ b/workflow_tests/assertions/deployBlockerAssertions.js @@ -0,0 +1,105 @@ +const utils = require('../utils/utils'); + +const assertDeployBlockerJobExecuted = (workflowResult, issueTitle, issueNumber, didExecute = true, isSuccessful = true) => { + const steps = [ + utils.getStepAssertion( + 'Checkout', + true, + null, + 'DEPLOYBLOCKER', + 'Checkout', + [{key: 'fetch-depth', value: '0'}, {key: 'token', value: '***'}], + [], + ), + utils.getStepAssertion( + 'Get URL, title, & number of new deploy blocker (issue)', + true, + null, + 'DEPLOYBLOCKER', + 'Get URL, title and number of new deploy blocker - issue', + [], + [{key: 'TITLE', value: issueTitle}], + ), + utils.getStepAssertion( + 'Update StagingDeployCash with new deploy blocker', + true, + null, + 'DEPLOYBLOCKER', + 'Update StagingDeployCash with new deploy blocker', + [{key: 'GITHUB_TOKEN', value: '***'}], + [], + ), + utils.getStepAssertion( + 'Give the issue/PR the Hourly, Engineering labels', + true, + null, + 'DEPLOYBLOCKER', + 'Give the issue/PR the Hourly, Engineering labels', + [{key: 'add-labels', value: 'Hourly, Engineering'}, {key: 'remove-labels', value: 'Daily, Weekly, Monthly'}], + [], + ), + utils.getStepAssertion( + 'Comment on deferred PR', + true, + null, + 'DEPLOYBLOCKER', + 'Comment on deferred PR', + [{key: 'github_token', value: '***'}, {key: 'number', value: issueNumber}], + [], + ), + ]; + + for (const expectedStep of steps) { + if (didExecute) { + if (isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); + } + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); + } + } + + const successSteps = [ + utils.getStepAssertion( + 'Post the issue in the #expensify-open-source slack room', + true, + null, + 'DEPLOYBLOCKER', + 'Post the issue in the expensify-open-source slack room', + [{key: 'status', value: 'custom'}], + [{key: 'GITHUB_TOKEN', value: '***'}, {key: 'SLACK_WEBHOOK_URL', value: '***'}], + ), + ]; + + for (const step of successSteps) { + if (didExecute && isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([step])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([step])); + } + } + + const failedSteps = [ + utils.getStepAssertion( + 'Announce failed workflow in Slack', + true, + null, + 'DEPLOYBLOCKER', + 'Announce failed workflow in Slack', + [{key: 'SLACK_WEBHOOK', value: '***'}], + [], + ), + ]; + + for (const step of failedSteps) { + if (didExecute && !isSuccessful) { + expect(workflowResult).toEqual(expect.arrayContaining([step])); + } else { + expect(workflowResult).not.toEqual(expect.arrayContaining([step])); + } + } +}; + +module.exports = { + assertDeployBlockerJobExecuted, +}; diff --git a/workflow_tests/createNewVersion.test.js b/workflow_tests/createNewVersion.test.js index e798926ed2c7..e663eccfed7f 100644 --- a/workflow_tests/createNewVersion.test.js +++ b/workflow_tests/createNewVersion.test.js @@ -166,7 +166,7 @@ describe('test workflow createNewVersion', () => { act = utils.setJobRunners(act, {createNewVersion: 'ubuntu-latest'}, workflowPath); const testMockSteps = { validateActor: mocks.CREATENEWVERSION__VALIDATEACTOR__ADMIN__STEP_MOCKS, - createNewVersion: JSON.parse(JSON.stringify(mocks.CREATENEWVERSION__CREATENEWVERSION__STEP_MOCKS)), + createNewVersion: utils.deepCopy(mocks.CREATENEWVERSION__CREATENEWVERSION__STEP_MOCKS), }; testMockSteps.createNewVersion[5] = utils.getMockStep( 'Commit new version', diff --git a/workflow_tests/deployBlocker.test.js b/workflow_tests/deployBlocker.test.js new file mode 100644 index 000000000000..8a4fe393bfe0 --- /dev/null +++ b/workflow_tests/deployBlocker.test.js @@ -0,0 +1,163 @@ +const path = require('path'); +const kieMockGithub = require('@kie/mock-github'); +const utils = require('./utils/utils'); +const assertions = require('./assertions/deployBlockerAssertions'); +const mocks = require('./mocks/deployBlockerMocks'); +const eAct = require('./utils/ExtendedAct'); + +jest.setTimeout(60 * 1000); +let mockGithub; +const FILES_TO_COPY_INTO_TEST_REPO = [ + { + src: path.resolve(__dirname, '..', '.github', 'actions'), + dest: '.github/actions', + }, + { + src: path.resolve(__dirname, '..', '.github', 'libs'), + dest: '.github/libs', + }, + { + src: path.resolve(__dirname, '..', '.github', 'scripts'), + dest: '.github/scripts', + }, + { + src: path.resolve(__dirname, '..', '.github', 'workflows', 'deployBlocker.yml'), + dest: '.github/workflows/deployBlocker.yml', + }, +]; + +describe('test workflow deployBlocker', () => { + const githubToken = 'dummy_github_token'; + const actor = 'Dummy Author'; + const secrets = { + OS_BOTIFY_TOKEN: 'dummy_osbotify_token', + SLACK_WEBHOOK: 'dummy_slack_webhook', + }; + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + testDeployBlockerWorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + + // if any branches besides main are need add: pushedBranches: ['staging', 'production'], + }, + }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); + }); + describe('issue labeled', () => { + const event = 'issues'; + const eventOptions = { + action: 'labeled', + label: { + name: 'DeployBlockerCash', + }, + issue: { + title: 'Labeled issue title', + number: '1234', + html_url: 'http://issue.html.url', + }, + }; + describe('label is DeployBlockerCash', () => { + const testEventOptions = utils.deepCopy(eventOptions); + testEventOptions.label = {name: 'DeployBlockerCash'}; + it('runs the workflow and announces success on Slack', async () => { + const repoPath = mockGithub.repo.getPath('testDeployBlockerWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'deployBlocker.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + event, + testEventOptions, + secrets, + githubToken, + {}, + {}, + ); + const testMockSteps = { + deployBlocker: mocks.DEPLOYBLOCKER__DEPLOYBLOCKER__STEP_MOCKS, + }; + const result = await act + .runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows'), + mockSteps: testMockSteps, + actor, + }); + + assertions.assertDeployBlockerJobExecuted(result, 'Labeled issue title', '1234'); + }); + describe('one step fails', () => { + it('announces failure on Slack', async () => { + const repoPath = mockGithub.repo.getPath('testDeployBlockerWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'deployBlocker.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + event, + testEventOptions, + secrets, + githubToken, + {}, + {}, + ); + const testMockSteps = { + deployBlocker: utils.deepCopy(mocks.DEPLOYBLOCKER__DEPLOYBLOCKER__STEP_MOCKS), + }; + testMockSteps.deployBlocker[2] = utils.getMockStep( + 'Update StagingDeployCash with new deploy blocker', + 'Update StagingDeployCash with new deploy blocker', + 'DEPLOYBLOCKER', + ['GITHUB_TOKEN'], + [], + {}, + {}, + false, + ); + const result = await act + .runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows'), + mockSteps: testMockSteps, + actor, + }); + + assertions.assertDeployBlockerJobExecuted(result, 'Labeled issue title', '1234', true, false); + }); + }); + }); + describe('label is different', () => { + const testEventOptions = utils.deepCopy(eventOptions); + testEventOptions.label = {name: 'Different Label'}; + it('does not run workflow', async () => { + const repoPath = mockGithub.repo.getPath('testDeployBlockerWorkflowRepo') || ''; + const workflowPath = path.join(repoPath, '.github', 'workflows', 'deployBlocker.yml'); + let act = new eAct.ExtendedAct(repoPath, workflowPath); + act = utils.setUpActParams( + act, + event, + testEventOptions, + secrets, + githubToken, + {}, + {}, + ); + const testMockSteps = { + deployBlocker: mocks.DEPLOYBLOCKER__DEPLOYBLOCKER__STEP_MOCKS, + }; + const result = await act + .runEvent(event, { + workflowFile: path.join(repoPath, '.github', 'workflows'), + mockSteps: testMockSteps, + actor, + }); + + assertions.assertDeployBlockerJobExecuted(result, '', '', false); + }); + }); + }); +}); diff --git a/workflow_tests/mocks/deployBlockerMocks.js b/workflow_tests/mocks/deployBlockerMocks.js new file mode 100644 index 000000000000..4c8410b70c4d --- /dev/null +++ b/workflow_tests/mocks/deployBlockerMocks.js @@ -0,0 +1,71 @@ +const utils = require('../utils/utils'); + +// deployblocker +const DEPLOYBLOCKER__DEPLOYBLOCKER__CHECKOUT__STEP_MOCK = utils.getMockStep( + 'Checkout', + 'Checkout', + 'DEPLOYBLOCKER', + ['fetch-depth', 'token'], + [], +); +const DEPLOYBLOCKER__DEPLOYBLOCKER__GET_URL_TITLE_AND_NUMBER_OF_NEW_DEPLOY_BLOCKER_ISSUE__STEP_MOCK = utils.getMockStep( + 'Get URL, title, & number of new deploy blocker (issue)', + 'Get URL, title and number of new deploy blocker - issue', + 'DEPLOYBLOCKER', + [], + ['TITLE'], + {}, + { + DEPLOY_BLOCKER_URL: '${{ github.event.issue.html_url }}', + DEPLOY_BLOCKER_NUMBER: '${{ github.event.issue.number }}', + DEPLOY_BLOCKER_TITLE: '${{ github.event.issue.title }}', + }, +); +const DEPLOYBLOCKER__DEPLOYBLOCKER__UPDATE_STAGINGDEPLOYCASH_WITH_NEW_DEPLOY_BLOCKER__STEP_MOCK = utils.getMockStep( + 'Update StagingDeployCash with new deploy blocker', + 'Update StagingDeployCash with new deploy blocker', + 'DEPLOYBLOCKER', + ['GITHUB_TOKEN'], + [], +); +const DEPLOYBLOCKER__DEPLOYBLOCKER__GIVE_THE_ISSUE_OR_PR_THE_HOURLY_ENGINEERING_LABELS__STEP_MOCK = utils.getMockStep( + 'Give the issue/PR the Hourly, Engineering labels', + 'Give the issue/PR the Hourly, Engineering labels', + 'DEPLOYBLOCKER', + ['add-labels', 'remove-labels'], + [], +); +const DEPLOYBLOCKER__DEPLOYBLOCKER__POST_THE_ISSUE_IN_THE_EXPENSIFY_OPEN_SOURCE_SLACK_ROOM__STEP_MOCK = utils.getMockStep( + 'Post the issue in the #expensify-open-source slack room', + 'Post the issue in the expensify-open-source slack room', + 'DEPLOYBLOCKER', + ['status'], + ['GITHUB_TOKEN', 'SLACK_WEBHOOK_URL'], +); +const DEPLOYBLOCKER__DEPLOYBLOCKER__COMMENT_ON_DEFERRED_PR__STEP_MOCK = utils.getMockStep( + 'Comment on deferred PR', + 'Comment on deferred PR', + 'DEPLOYBLOCKER', + ['github_token', 'number'], + [], +); +const DEPLOYBLOCKER__DEPLOYBLOCKER__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK = utils.getMockStep( + 'Announce failed workflow in Slack', + 'Announce failed workflow in Slack', + 'DEPLOYBLOCKER', + ['SLACK_WEBHOOK'], + [], +); +const DEPLOYBLOCKER__DEPLOYBLOCKER__STEP_MOCKS = [ + DEPLOYBLOCKER__DEPLOYBLOCKER__CHECKOUT__STEP_MOCK, + DEPLOYBLOCKER__DEPLOYBLOCKER__GET_URL_TITLE_AND_NUMBER_OF_NEW_DEPLOY_BLOCKER_ISSUE__STEP_MOCK, + DEPLOYBLOCKER__DEPLOYBLOCKER__UPDATE_STAGINGDEPLOYCASH_WITH_NEW_DEPLOY_BLOCKER__STEP_MOCK, + DEPLOYBLOCKER__DEPLOYBLOCKER__GIVE_THE_ISSUE_OR_PR_THE_HOURLY_ENGINEERING_LABELS__STEP_MOCK, + DEPLOYBLOCKER__DEPLOYBLOCKER__POST_THE_ISSUE_IN_THE_EXPENSIFY_OPEN_SOURCE_SLACK_ROOM__STEP_MOCK, + DEPLOYBLOCKER__DEPLOYBLOCKER__COMMENT_ON_DEFERRED_PR__STEP_MOCK, + DEPLOYBLOCKER__DEPLOYBLOCKER__ANNOUNCE_FAILED_WORKFLOW_IN_SLACK__STEP_MOCK, +]; + +module.exports = { + DEPLOYBLOCKER__DEPLOYBLOCKER__STEP_MOCKS, +}; diff --git a/workflow_tests/utils/preGenerateTest.js b/workflow_tests/utils/preGenerateTest.js index 19d65dec4c4b..4587705f7c54 100644 --- a/workflow_tests/utils/preGenerateTest.js +++ b/workflow_tests/utils/preGenerateTest.js @@ -26,6 +26,7 @@ const assertions = require('./assertions/${workflowName}Assertions'); const mocks = require('./mocks/${workflowName}Mocks'); const eAct = require('./utils/ExtendedAct'); +jest.setTimeout(60 * 1000); let mockGithub; const FILES_TO_COPY_INTO_TEST_REPO = [ { @@ -46,34 +47,37 @@ const FILES_TO_COPY_INTO_TEST_REPO = [ }, ]; -beforeEach(async () => { - // create a local repository and copy required files - mockGithub = new kieMockGithub.MockGithub({ - repo: { - test${capitalize(workflowName)}WorkflowRepo: { - files: FILES_TO_COPY_INTO_TEST_REPO, - - // if any branches besides main are need add: pushedBranches: ['staging', 'production'], +describe('test workflow ${workflowName}', () => { + const githubToken = 'dummy_github_token', + const actor = 'Dummy Actor'; + beforeEach(async () => { + // create a local repository and copy required files + mockGithub = new kieMockGithub.MockGithub({ + repo: { + test${capitalize(workflowName)}WorkflowRepo: { + files: FILES_TO_COPY_INTO_TEST_REPO, + + // if any branches besides main are need add: pushedBranches: ['staging', 'production'], + }, }, - }, + }); + + await mockGithub.setup(); + }); + + afterEach(async () => { + await mockGithub.teardown(); }); - - await mockGithub.setup(); -}); - -afterEach(async () => { - await mockGithub.teardown(); -}); - -describe('test workflow ${workflowName}', () => { test('test stub', async () => { const repoPath = mockGithub.repo.getPath('test${capitalize(workflowName)}WorkflowRepo') || ''; const workflowPath = path.join(repoPath, '.github', 'workflows', '${workflowName}.yml'); let act = new eAct.ExtendedAct(repoPath, workflowPath); act = utils.setUpActParams( act, - - // set up params if needed + '[EVENT]', + {}, + {}, + githubToken, ); const testMockSteps = { // mock steps with imported mocks @@ -82,11 +86,11 @@ describe('test workflow ${workflowName}', () => { .runEvent('[EVENT]', { workflowFile: path.join(repoPath, '.github', 'workflows'), mockSteps: testMockSteps, - actor: 'Dummy Author', + actor, }); // assert execution with imported assertions - }, 60000); + }); }); `; const mockStepTemplate = (stepMockName, step, jobId) => ` diff --git a/workflow_tests/utils/utils.js b/workflow_tests/utils/utils.js index b6a7bb34e0e6..e3b3d74f611f 100644 --- a/workflow_tests/utils/utils.js +++ b/workflow_tests/utils/utils.js @@ -1,7 +1,15 @@ const yaml = require('yaml'); const fs = require('fs'); -const setUpActParams = (act, event = null, eventOptions = null, secrets = null, githubToken = null, envVars = null, inputs = null) => { +function setUpActParams( + act, + event = null, + eventOptions = null, + secrets = null, + githubToken = null, + envVars = null, + inputs = null, +) { let updated_act = act; if (event && eventOptions) { @@ -38,9 +46,18 @@ const setUpActParams = (act, event = null, eventOptions = null, secrets = null, } return updated_act; -}; +} -const getMockStep = (name, message, job_id = null, inputs = null, in_envs = null, outputs = null, out_envs = null, isSuccessful = true) => { +function getMockStep( + name, + message, + job_id = null, + inputs = null, + in_envs = null, + outputs = null, + out_envs = null, + isSuccessful = true, +) { const mockStepName = name; let mockWithCommand = 'echo [MOCK]'; if (job_id) { @@ -74,9 +91,17 @@ const getMockStep = (name, message, job_id = null, inputs = null, in_envs = null name: mockStepName, mockWith: mockWithCommand, }; -}; +} -const getStepAssertion = (name, isSuccessful = true, expectedOutput = null, jobId = null, message = null, inputs = null, envs = null) => { +function getStepAssertion( + name, + isSuccessful = true, + expectedOutput = null, + jobId = null, + message = null, + inputs = null, + envs = null, +) { const stepName = `Main ${name}`; const stepStatus = isSuccessful ? 0 : 1; let stepOutput; @@ -106,9 +131,9 @@ const getStepAssertion = (name, isSuccessful = true, expectedOutput = null, jobI status: stepStatus, output: stepOutput, }; -}; +} -const setJobRunners = (act, jobs, workflowPath) => { +function setJobRunners(act, jobs, workflowPath) { if (!act || !jobs || !workflowPath) { return act; } @@ -120,11 +145,16 @@ const setJobRunners = (act, jobs, workflowPath) => { } fs.writeFileSync(workflowPath, yaml.stringify(workflow), 'utf8'); return act; -}; +} + +function deepCopy(originalObject) { + return JSON.parse(JSON.stringify(originalObject)); +} module.exports = { setUpActParams, getMockStep, getStepAssertion, setJobRunners, + deepCopy, };