From 14b72ebc0db5169f0dcf7ee13bb8c7fac0d86047 Mon Sep 17 00:00:00 2001 From: Matthias Rolke Date: Mon, 25 Feb 2019 11:33:57 +0100 Subject: [PATCH] feat: implement activating/deactivating critical updates --- package.json | 2 + .../critical-updates/activate-all.json | 14 ++ .../critical-updates/deactivate-all.json | 14 ++ .../critical-updates/index.e2e-spec.ts | 122 ++++++++++++++ .../critical-updates/index.test.ts | 153 ++++++++++++++++++ .../critical-updates/index.ts | 111 +++++++++++++ .../critical-updates/schema.json | 23 +++ src/plugins/company-settings/index.ts | 38 +++++ src/plugins/company-settings/schema.json | 11 ++ src/plugins/index.ts | 9 +- src/plugins/schema.json | 3 + yarn.lock | 17 +- 12 files changed, 515 insertions(+), 2 deletions(-) create mode 100644 src/plugins/company-settings/critical-updates/activate-all.json create mode 100644 src/plugins/company-settings/critical-updates/deactivate-all.json create mode 100644 src/plugins/company-settings/critical-updates/index.e2e-spec.ts create mode 100644 src/plugins/company-settings/critical-updates/index.test.ts create mode 100644 src/plugins/company-settings/critical-updates/index.ts create mode 100644 src/plugins/company-settings/critical-updates/schema.json create mode 100644 src/plugins/company-settings/index.ts create mode 100644 src/plugins/company-settings/schema.json diff --git a/package.json b/package.json index 155e6a5b..8a43e58c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@salesforce/command": "^1.2.0", "json-merge-patch": "^0.2.3", "p-retry": "^3.0.1", + "multimatch": "^3.0.0", "puppeteer": "^1.12.2", "tslib": "^1" }, @@ -27,6 +28,7 @@ "mocha": "^6", "nyc": "^13", "sfdx-cli": "^6.54.4", + "tmp": "^0.0.33", "ts-node": "^8", "typescript": "^3.3.3333" }, diff --git a/src/plugins/company-settings/critical-updates/activate-all.json b/src/plugins/company-settings/critical-updates/activate-all.json new file mode 100644 index 00000000..52dd9c50 --- /dev/null +++ b/src/plugins/company-settings/critical-updates/activate-all.json @@ -0,0 +1,14 @@ +{ + "$schema": "../../schema.json", + "settings": { + "companySettings": { + "criticalUpdates": [ + { + "name": "*", + "comment": "Activated by sfdx-browserforce-plugin", + "active": true + } + ] + } + } +} diff --git a/src/plugins/company-settings/critical-updates/deactivate-all.json b/src/plugins/company-settings/critical-updates/deactivate-all.json new file mode 100644 index 00000000..04016286 --- /dev/null +++ b/src/plugins/company-settings/critical-updates/deactivate-all.json @@ -0,0 +1,14 @@ +{ + "$schema": "../../schema.json", + "settings": { + "companySettings": { + "criticalUpdates": [ + { + "name": "*", + "comment": "Deactivated by sfdx-browserforce-plugin", + "active": false + } + ] + } + } +} diff --git a/src/plugins/company-settings/critical-updates/index.e2e-spec.ts b/src/plugins/company-settings/critical-updates/index.e2e-spec.ts new file mode 100644 index 00000000..5e62d63c --- /dev/null +++ b/src/plugins/company-settings/critical-updates/index.e2e-spec.ts @@ -0,0 +1,122 @@ +import { core } from '@salesforce/command'; +import * as assert from 'assert'; +import * as child from 'child_process'; +import { unlink } from 'fs'; +import * as path from 'path'; +import { file } from 'tmp'; +import { promisify } from 'util'; +import CriticalUpdates from '.'; +const tmpFilePromise = promisify(file); +const fsUnlinkPromise = promisify(unlink); + +describe(CriticalUpdates.name, () => { + let activatableCriticalUpdatesAvailable, + deactivatableCriticalUpdatesAvailable = false; + let stateFileActivation, stateFileDeactivation; + before(async () => { + stateFileActivation = await tmpFilePromise('state1.json'); + stateFileDeactivation = await tmpFilePromise('state2.json'); + }); + after(async () => { + await fsUnlinkPromise(stateFileActivation); + await fsUnlinkPromise(stateFileDeactivation); + }); + it('should list all activatable critical updates', async function() { + this.timeout(1000 * 90); + this.slow(1000 * 30); + const planActivateCmd = child.spawnSync(path.resolve('bin', 'run'), [ + 'browserforce:plan', + '-f', + path.resolve(path.join(__dirname, 'activate-all.json')), + '-s', + stateFileActivation + ]); + assert.deepEqual( + planActivateCmd.status, + 0, + planActivateCmd.output.toString() + ); + let state; + try { + state = await core.fs.readJson(stateFileActivation); + } catch (err) { + assert(false, err); + } + assert(state); + assert(state.settings); + assert(state.settings.companySettings); + assert(state.settings.companySettings.criticalUpdates); + if (state.settings.companySettings.criticalUpdates.length) { + activatableCriticalUpdatesAvailable = state.settings.companySettings.criticalUpdates.some( + item => item.active === false + ); + } + }); + it('should activate all available critical updates', function() { + if (!activatableCriticalUpdatesAvailable) { + this.skip(); + } + this.timeout(1000 * 90); + this.slow(1000 * 30); + const activateCmd = child.spawnSync(path.resolve('bin', 'run'), [ + 'browserforce:apply', + '-f', + path.resolve(path.join(__dirname, 'activate-all.json')) + ]); + assert.deepEqual(activateCmd.status, 0, activateCmd.output.toString()); + assert( + /changing 'criticalUpdates' to '\[/.test(activateCmd.output.toString()), + activateCmd.output.toString() + ); + }); + it('should list all deactivatable critical updates', async function() { + this.timeout(1000 * 90); + this.slow(1000 * 30); + const planDeactivateCmd = child.spawnSync(path.resolve('bin', 'run'), [ + 'browserforce:plan', + '-f', + path.resolve(path.join(__dirname, 'deactivate-all.json')), + '-s', + '/tmp/sfdx-browserforce-plugin.state.json' + ]); + assert.deepEqual( + planDeactivateCmd.status, + 0, + planDeactivateCmd.output.toString() + ); + let state; + try { + state = await core.fs.readJson( + '/tmp/sfdx-browserforce-plugin.state.json' + ); + } catch (err) { + assert(false, err); + } + assert(state); + assert(state.settings); + assert(state.settings.companySettings); + assert(state.settings.companySettings.criticalUpdates); + if (state.settings.companySettings.criticalUpdates.length) { + deactivatableCriticalUpdatesAvailable = state.settings.companySettings.criticalUpdates.some( + item => item.active === true + ); + } + }); + it('should deactivate all deactivatable critical updates', function() { + if (!deactivatableCriticalUpdatesAvailable) { + this.skip(); + } + this.timeout(1000 * 90); + this.slow(1000 * 30); + const deactivateCmd = child.spawnSync(path.resolve('bin', 'run'), [ + 'browserforce:apply', + '-f', + path.resolve(path.join(__dirname, 'deactivate-all.json')) + ]); + assert.deepEqual(deactivateCmd.status, 0, deactivateCmd.output.toString()); + assert( + /changing 'criticalUpdates' to '\[/.test(deactivateCmd.output.toString()), + deactivateCmd.output.toString() + ); + }); +}); diff --git a/src/plugins/company-settings/critical-updates/index.test.ts b/src/plugins/company-settings/critical-updates/index.test.ts new file mode 100644 index 00000000..43192835 --- /dev/null +++ b/src/plugins/company-settings/critical-updates/index.test.ts @@ -0,0 +1,153 @@ +import * as assert from 'assert'; +import CriticalUpdates from '.'; + +interface TestData { + name: string | string[]; + active: boolean; + comment?: string; +} +interface Test { + description: string; + source: TestData[]; + target: TestData[]; + expected: TestData[]; + skip?: boolean; +} + +const tests: Test[] = [ + { + description: 'should return no change', + source: [ + { + name: 'Update 1', + active: true + }, + { + name: 'Update 2', + active: true + }, + { + name: 'Update 3', + active: true + } + ], + target: [ + { + name: 'Update 2', + active: true, + comment: 'This is a comment' + } + ], + expected: [] + }, + { + description: 'should match a specific item', + source: [ + { + name: 'Update 1', + active: false + }, + { + name: 'Update 2', + active: false + }, + { + name: 'Update 3', + active: false + } + ], + target: [ + { + name: 'Update 2', + active: true, + comment: 'This is a comment' + } + ], + expected: [ + { + name: 'Update 2', + active: true, + comment: 'This is a comment' + } + ] + }, + { + description: 'should match all inactive items', + source: [ + { + name: 'Update 1', + active: true + }, + { + name: 'Update 2', + active: false + }, + { + name: 'Update 3', + active: true + } + ], + target: [ + { + name: '*', + active: true, + comment: 'This is a comment' + } + ], + expected: [ + { + name: 'Update 2', + active: true, + comment: 'This is a comment' + } + ] + }, + { + description: 'should handle multiple patterns', + source: [ + { + name: 'Update 1', + active: false + }, + { + name: 'Update 2', + active: false + }, + { + name: 'Update 3', + active: false + } + ], + target: [ + { + name: ['*', '!Update 2'], + active: true, + comment: 'This is a comment' + } + ], + expected: [ + { + name: 'Update 1', + active: true, + comment: 'This is a comment' + }, + { + name: 'Update 3', + active: true, + comment: 'This is a comment' + } + ] + } +]; + +describe('CriticalUpdates', () => { + describe('diff()', () => { + const p = new CriticalUpdates(null, null); + for (const t of tests) { + (t.skip ? it.skip : it)(t.description, () => { + const actual = p.diff(t.source, t.target); + assert.deepEqual(actual, t.expected); + }); + } + }); +}); diff --git a/src/plugins/company-settings/critical-updates/index.ts b/src/plugins/company-settings/critical-updates/index.ts new file mode 100644 index 00000000..d456d35c --- /dev/null +++ b/src/plugins/company-settings/critical-updates/index.ts @@ -0,0 +1,111 @@ +import * as jsonMergePatch from 'json-merge-patch'; +import * as multimatch from 'multimatch'; +import { BrowserforcePlugin } from '../../../plugin'; + +const PATHS = { + BASE: 'ruac/ruacPage.apexp', + REVIEW: '/ruac/CriticalUpdateDetail.apexp?name=', + ACTIVATE: '/ruac/CriticalUpdateActivate.apexp?name=', + DEACTIVATE: '/ruac/CriticalUpdateDeactivate.apexp?name=' +}; +const SELECTORS = { + TABLE_BODY: 'tbody[id$=":featuresTable:tb"]', + TABLE_ROWS: 'tbody[id$=":featuresTable:tb"] > tr', + ROW_NAME_COLUMN: 'td:nth-child(2)', + ROW_ACTIVATE_ACTION: `td:first-child a[href*="${PATHS.ACTIVATE}"]`, + ROW_DEACTIVATE_ACTION: `td:first-child a[href*="${PATHS.DEACTIVATE}"]`, + FORM_COMMENT: 'textarea[id$=":comment"]', + FORM_ACTIVATE_BUTTON: 'input[id$=":activate"]', + FORM_DEACTIVATE_BUTTON: 'input[id$=":deactivate"]' +}; + +export default class CriticalUpdates extends BrowserforcePlugin { + public async retrieve(definition?) { + const page = await this.browserforce.openPage(PATHS.BASE); + await page.waitFor(SELECTORS.TABLE_BODY); + const response = []; + const rowHandles = await page.$$(SELECTORS.TABLE_ROWS); + for (const rowHandle of rowHandles) { + const name = await rowHandle.$eval( + SELECTORS.ROW_NAME_COLUMN, + (td: HTMLTableDataCellElement) => td.textContent + ); + const rowDeactivateActionHandle = await rowHandle.$( + SELECTORS.ROW_DEACTIVATE_ACTION + ); + const rowActivateActionHandle = await rowHandle.$( + SELECTORS.ROW_ACTIVATE_ACTION + ); + // return only actionable items + if (rowDeactivateActionHandle || rowActivateActionHandle) { + response.push({ + name, + active: + rowDeactivateActionHandle !== null || + rowActivateActionHandle === null + }); + } + } + return response; + } + + public diff(state, definition) { + const response = []; + for (const stateItem of state) { + const targetMatch = definition.find( + item => + multimatch( + [stateItem.name], + Array.isArray(item.name) ? item.name : [item.name] + ).length > 0 + ); + if (targetMatch) { + const newDefinition = Object.assign({}, targetMatch); + // replace the pattern by the real name + newDefinition.name = stateItem.name; + // copy comment to state for diffing + stateItem['comment'] = newDefinition.comment; + const diff = jsonMergePatch.generate(stateItem, newDefinition); + if (diff) { + response.push(newDefinition); + } + } + } + return response; + } + + public async apply(config) { + for (const update of config) { + const url = `${ + update.active ? PATHS.ACTIVATE : PATHS.DEACTIVATE + }${encodeURI(update.name)}`; + const page = await this.browserforce.openPage(url); + const buttonSelector = update.active + ? SELECTORS.FORM_ACTIVATE_BUTTON + : SELECTORS.FORM_DEACTIVATE_BUTTON; + await page.waitFor(buttonSelector); + const isDisabled = await page.$eval( + buttonSelector, + button => button.disabled + ); + if (isDisabled) { + // TODO: use this.logger.warn once plugins have loggers + console.warn( + `Warning: Critical Update '${update.name}' cannot be set to ${ + update.active + }` + ); + continue; + } else { + await page.waitFor(SELECTORS.FORM_COMMENT); + if (update.comment) { + await page.type(SELECTORS.FORM_COMMENT, update.comment); + } + await Promise.all([ + page.waitFor(SELECTORS.TABLE_BODY), + page.click(buttonSelector) + ]); + } + } + } +} diff --git a/src/plugins/company-settings/critical-updates/schema.json b/src/plugins/company-settings/critical-updates/schema.json new file mode 100644 index 00000000..27ebf095 --- /dev/null +++ b/src/plugins/company-settings/critical-updates/schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://github.com/amtrack/sfdx-browserforce-plugin/src/plugins/company-settings/critical-updates/schema.json", + "title": "Critical Updates", + "type": "array", + "items": { "$ref": "#/definitions/criticalUpdate" }, + "default": [], + "description": "Examples:\n - activate all but 'Update 2': [{\"name\": [\"*\", \"!Update 2\"], \"active\": true]\n - activate only 'Update 2': [{\"name\": \"!Update 2\", \"active\": true]", + "definitions": { + "criticalUpdate": { + "type": "object", + "properties": { + "name": { + "type": ["string", "array"], + "$comment": "Name or Array of Glob Patterns. Note: This is in the language of the user." + }, + "active": { "type": "boolean" }, + "comment": { "type": "string" } + }, + "required": ["name", "active"] + } + } +} diff --git a/src/plugins/company-settings/index.ts b/src/plugins/company-settings/index.ts new file mode 100644 index 00000000..c9230b2f --- /dev/null +++ b/src/plugins/company-settings/index.ts @@ -0,0 +1,38 @@ +import { BrowserforcePlugin } from '../../plugin'; +import { removeEmptyValues } from '../utils'; +import CriticalUpdates from './critical-updates'; + +export default class CompanySettings extends BrowserforcePlugin { + public async retrieve(definition?) { + const response = { + criticalUpdates: {} + }; + if (definition) { + if (definition.criticalUpdates) { + const pluginCU = new CriticalUpdates(this.browserforce, this.org); + response.criticalUpdates = await pluginCU.retrieve( + definition.criticalUpdates + ); + } + } + return response; + } + + public diff(state, definition) { + const pluginCU = new CriticalUpdates(null, null); + const response = { + criticalUpdates: pluginCU.diff( + state.criticalUpdates, + definition.criticalUpdates + ) + }; + return removeEmptyValues(response); + } + + public async apply(plan) { + if (plan.criticalUpdates) { + const pluginCU = new CriticalUpdates(this.browserforce, this.org); + await pluginCU.apply(plan.criticalUpdates); + } + } +} diff --git a/src/plugins/company-settings/schema.json b/src/plugins/company-settings/schema.json new file mode 100644 index 00000000..b8fd6251 --- /dev/null +++ b/src/plugins/company-settings/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/amtrack/sfdx-browserforce-plugin/src/plugins/schema.json", + "title": "Company Settings", + "type": "object", + "properties": { + "criticalUpdates": { + "$ref": "./critical-updates/schema.json" + } + } +} diff --git a/src/plugins/index.ts b/src/plugins/index.ts index a9e0e0fb..9b9ce5b3 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -1,5 +1,12 @@ +import * as companySettings from './company-settings'; import * as customerPortal from './customer-portal'; import * as homePageLayouts from './home-page-layouts'; import * as salesforceToSalesforce from './salesforce-to-salesforce'; import * as security from './security'; -export { customerPortal, homePageLayouts, salesforceToSalesforce, security }; +export { + companySettings, + customerPortal, + homePageLayouts, + salesforceToSalesforce, + security +}; diff --git a/src/plugins/schema.json b/src/plugins/schema.json index e95702b2..cb87bb00 100644 --- a/src/plugins/schema.json +++ b/src/plugins/schema.json @@ -7,6 +7,9 @@ "properties": { "settings": { "properties": { + "companySettings": { + "$ref": "./company-settings/schema.json" + }, "customerPortal": { "$ref": "./customer-portal/schema.json" }, diff --git a/yarn.lock b/yarn.lock index 5f36b0b4..196896df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -857,6 +857,11 @@ array-differ@^1.0.0: resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" integrity sha1-7/UuN1gknTO+QCuLuOVkuytdQDE= +array-differ@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-2.0.3.tgz#0195bb00ccccf271106efee4a4786488b7180712" + integrity sha1-AZW7AMzM8nEQbv7kpHhkiLcYBxI= + array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" @@ -872,7 +877,7 @@ array-from@^2.1.1: resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= -array-union@^1.0.1: +array-union@^1.0.1, array-union@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= @@ -5341,6 +5346,16 @@ multimatch@^2.0.0: arrify "^1.0.0" minimatch "^3.0.0" +multimatch@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-3.0.0.tgz#0e2534cc6bc238d9ab67e1b9cd5fcd85a6dbf70b" + integrity sha512-22foS/gqQfANZ3o+W7ST2x25ueHDVNWl/b9OlGcLpy/iKxjCpvcNCM51YCenUi7Mt/jAjjqv8JwZRs8YP5sRjA== + dependencies: + array-differ "^2.0.3" + array-union "^1.0.2" + arrify "^1.0.1" + minimatch "^3.0.4" + multistream@^2.0.5: version "2.1.1" resolved "https://registry.yarnpkg.com/multistream/-/multistream-2.1.1.tgz#629d3a29bd76623489980d04519a2c365948148c"