diff --git a/README.md b/README.md index 1c8bb8e..30855ae 100644 --- a/README.md +++ b/README.md @@ -460,10 +460,24 @@ The object consisting of PR comment ID, create time, and username. No parameters are required. #### Expected Outcome -The array of scm context name (e.g. [github:github.com, gitlab:my-gitlab]) +The array of scm context names (e.g. [github:github.com, gitlab:my-gitlab]) #### Expected Response -1. The array of scm context name +1. The array of scm context names + +### getScmContext +The parameters required are: + +| Parameter | Type | Required | Description | +| :------------- | :---- | :------- | :-------------| +| config | Object | Yes | Configuration Object | +| config.hostname | String | Yes | The scm host name (ex: `github.com`) | + +#### Expected Outcome +The matching scm context name string (e.g. github:github.com) + +#### Expected Response +1. The matching scm context name ### canHandleWebhook The parameters required are: @@ -508,6 +522,19 @@ The display name of scm context #### Expected Response 1. The display name of scm context +### getReadOnlyInfo (overriding needs only the case of `scm-router`) +The parameters required are: + +| Parameter | Type | Description | +| :------------- | :---- | :-------------| +| scmContext | String | The name of scm context | + +#### Expected Outcome +Read-only SCM config + +#### Expected Response +1. Read-only SCM config + ### openPr | Parameter | Type | Required | Description | | :------------- | :---- | :------- | :-------------| @@ -546,11 +573,14 @@ To make use of the validation functions, the functions to override are: 1. `_getBellConfiguration` 1. `_getPrInfo` 1. `stats`  -1. `_getScmContexts`  +1. `_getScmContexts` +1. `_getScmContext` 1. `_canHandleWebhook`  1. `_getBranchList` 1. `_openPr` -1. `getDisplayName` (overriding needs only the case of `scm-router`)  +1. `getDisplayName` (overriding needs only the case of `scm-router`) +1. `getReadOnlyInfo` (overriding needs only the case of `scm-router`)  + ```js class MyScm extends ScmBase { diff --git a/index.js b/index.js index 102be10..52f42ba 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ 'use strict'; /* eslint-disable no-underscore-dangle */ +const Hoek = require('@hapi/hoek'); const Joi = require('joi'); const dataSchema = require('screwdriver-data-schema'); const { getAnnotations } = require('./lib/helper'); @@ -40,6 +41,24 @@ class ScmBase { this.config = config; } + /** + * Set token correctly if is read-only SCM + * @param {Object} config + * @return {Object} Config with proper token + */ + getConfig(config) { + const newConfig = config; + const { accessToken, enabled } = Hoek.reach(this.config, 'readOnly', { default: {} }); + + if (newConfig && enabled && accessToken) { + newConfig.token = accessToken; + + return newConfig; + } + + return newConfig; + } + /** * Adds the Screwdriver webhook to the SCM repository * @@ -55,7 +74,7 @@ class ScmBase { */ addWebhook(config) { return validate(config, dataSchema.plugins.scm.addWebhook) - .then(() => this._addWebhook(config)); + .then(() => this._addWebhook(this.getConfig(config))); } _addWebhook() { @@ -99,7 +118,7 @@ class ScmBase { */ addDeployKey(config) { return validate(config, dataSchema.plugins.scm.addDeployKey) - .then(() => this._addDeployKey(config)); + .then(() => this._addDeployKey(this.getConfig(config))); } _addDeployKey() { @@ -118,7 +137,7 @@ class ScmBase { */ parseUrl(config) { return validate(config, dataSchema.plugins.scm.parseUrl) - .then(validUrl => this._parseUrl(validUrl)) + .then(validUrl => this._parseUrl(this.getConfig(validUrl))) .then(uri => validate(uri, dataSchema.models.pipeline.base.extract('scmUri'))); } @@ -154,7 +173,7 @@ class ScmBase { */ getChangedFiles(config) { return validate(config, dataSchema.plugins.scm.getChangedFilesInput) - .then(validInput => this._getChangedFiles(validInput)) + .then(validInput => this._getChangedFiles(this.getConfig(validInput))) .then(changedFiles => validate(changedFiles, dataSchema.plugins.scm.getChangedFilesOutput)); } @@ -235,6 +254,7 @@ class ScmBase { checkoutConfig.commitBranch = o.build.baseBranch; } + // Set parentConfig info if (o.configPipeline) { const parentConfig = { sha: o.configPipelineSha }; @@ -264,7 +284,7 @@ class ScmBase { */ decorateUrl(config) { return validate(config, dataSchema.plugins.scm.decorateUrl) - .then(validUrl => this._decorateUrl(validUrl)) + .then(validUrl => this._decorateUrl(this.getConfig(validUrl))) .then(decoratedUrl => validate(decoratedUrl, dataSchema.core.scm.repo)); } @@ -284,7 +304,7 @@ class ScmBase { */ decorateCommit(config) { return validate(config, dataSchema.plugins.scm.decorateCommit) - .then(validCommit => this._decorateCommit(validCommit)) + .then(validCommit => this._decorateCommit(this.getConfig(validCommit))) .then(decoratedCommit => validate(decoratedCommit, dataSchema.core.scm.commit)); } @@ -303,7 +323,7 @@ class ScmBase { */ decorateAuthor(config) { return validate(config, dataSchema.plugins.scm.decorateAuthor) - .then(validAuthor => this._decorateAuthor(validAuthor)) + .then(validAuthor => this._decorateAuthor(this.getConfig(validAuthor))) .then(decoratedAuthor => validate(decoratedAuthor, dataSchema.core.scm.user)); } @@ -322,7 +342,17 @@ class ScmBase { */ getPermissions(config) { return validate(config, dataSchema.plugins.scm.getPermissions) - .then(validConfig => this._getPermissions(validConfig)); + .then((validConfig) => { + if (Hoek.reach(this.config, 'readOnly.enabled')) { + return Promise.resolve({ + admin: true, + push: true, + pull: true + }); + } + + return this._getPermissions(validConfig); + }); } _getPermissions() { @@ -341,7 +371,7 @@ class ScmBase { */ getOrgPermissions(config) { return validate(config, dataSchema.plugins.scm.getOrgPermissions) - .then(validConfig => this._getOrgPermissions(validConfig)); + .then(validConfig => this._getOrgPermissions(this.getConfig(validConfig))); } _getOrgPermissions() { @@ -360,7 +390,7 @@ class ScmBase { */ getCommitSha(config) { return validate(config, dataSchema.plugins.scm.getCommitSha) - .then(validConfig => this._getCommitSha(validConfig)); + .then(validConfig => this._getCommitSha(this.getConfig(validConfig))); } _getCommitSha() { @@ -380,7 +410,7 @@ class ScmBase { */ getCommitRefSha(config) { return validate(config, dataSchema.plugins.scm.getCommitRefSha) - .then(validConfig => this._getCommitRefSha(validConfig)); + .then(validConfig => this._getCommitRefSha(this.getConfig(validConfig))); } _getCommitRefSha() { @@ -405,7 +435,7 @@ class ScmBase { */ updateCommitStatus(config) { return validate(config, dataSchema.plugins.scm.updateCommitStatus) - .then(validConfig => this._updateCommitStatus(validConfig)); + .then(validConfig => this._updateCommitStatus(this.getConfig(validConfig))); } _updateCommitStatus() { @@ -424,7 +454,7 @@ class ScmBase { */ getFile(config) { return validate(config, dataSchema.plugins.scm.getFile) - .then(validConfig => this._getFile(validConfig)); + .then(validConfig => this._getFile(this.getConfig(validConfig))); } _getFile() { @@ -442,7 +472,7 @@ class ScmBase { */ getOpenedPRs(config) { return validate(config, dataSchema.plugins.scm.getCommitSha) // includes scmUri, token and scmContext - .then(validConfig => this._getOpenedPRs(validConfig)) + .then(validConfig => this._getOpenedPRs(this.getConfig(validConfig))) .then(jobList => validate(jobList, Joi.array().items( Joi.object().keys({ @@ -487,7 +517,7 @@ class ScmBase { */ getPrInfo(config) { return validate(config, dataSchema.plugins.scm.getCommitSha) // includes scmUri, token and scmContext - .then(validConfig => this._getPrInfo(validConfig)) + .then(validConfig => this._getPrInfo(this.getConfig(validConfig))) .then(pr => validate(pr, Joi.object().keys({ name: dataSchema.models.job.base.extract('name').required(), sha: dataSchema.models.build.base.extract('sha').required(), @@ -521,7 +551,7 @@ class ScmBase { */ addPrComment(config) { return validate(config, dataSchema.plugins.scm.addPrComment) // includes scmUri, token and scmContext - .then(validConfig => this._addPrComment(validConfig)) + .then(validConfig => this._addPrComment(this.getConfig(validConfig))) .then(prComment => validate(prComment, Joi.alternatives().try( Joi.object().keys({ commentId: dataSchema.models.job.base.extract('id').required(), @@ -565,6 +595,26 @@ class ScmBase { throw new Error('Not implemented'); } + /** + * Get a scm context matching given hostname + * @method getScmContext + * @param {Object} config + * @param {String} config.hostname Scm hostname (e.g. github.com or GHE.com) + * @return {String} Returns scm context (e.g. github:github.com + * or github:GHE.com) + */ + getScmContext(config) { + const result = this._getScmContext(config); + const schema = dataSchema.models.pipeline.base.extract('scmContext').required(); + const validateResult = schema.validate(result); + + return validateResult.error || result; + } + + _getScmContext() { + throw new Error('Not implemented'); + } + /** * Determine a scm module can handle the received webhook * @method canHandleWebhook @@ -589,6 +639,15 @@ class ScmBase { return this.config.displayName || ''; } + /** + * Get readOnly object + * @method getReadOnlyInfo + * @return {Object} + */ + getReadOnlyInfo() { + return this.config.readOnly || {}; + } + /** * Get the branch list related to the repository * @method getBranchList @@ -600,7 +659,7 @@ class ScmBase { */ getBranchList(config) { return validate(config, dataSchema.plugins.scm.getBranchList) - .then(() => this._getBranchList(config)); + .then(() => this._getBranchList(this.getConfig(config))); } _getBranchList() { @@ -622,7 +681,7 @@ class ScmBase { */ openPr(config) { return validate(config, dataSchema.plugins.scm.openPr) - .then(() => this._openPr(config)); + .then(() => this._openPr(this.getConfig(config))); } _openPr() { diff --git a/package.json b/package.json index 71aa459..c5b0794 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "nyc": "^15.0.0" }, "dependencies": { + "@hapi/hoek": "^9.1.0", "joi": "^17.2.0", - "screwdriver-data-schema": "^21.0.0" + "screwdriver-data-schema": "^21.3.1" } } diff --git a/test/index.test.js b/test/index.test.js index dcb7cdd..95e2485 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -4,6 +4,11 @@ const assert = require('chai').assert; const token = 'token'; const testParseHook = require('./data/parseHookOutput.json'); +const readOnlyConfig = { + enabled: true, + username: 'headlessbot', + accessToken: 'sometoken' +}; describe('index test', () => { let instance; @@ -14,7 +19,7 @@ describe('index test', () => { ScmBase = require('../index'); /* eslint-enable global-require */ - instance = new ScmBase({ foo: 'bar' }); + instance = new ScmBase({ foo: 'bar', readOnly: readOnlyConfig }); }); afterEach(() => { @@ -79,6 +84,18 @@ describe('index test', () => { describe('addDeployKey', () => { const privKey = 'fakePrivateKey'; const checkoutUrl = 'git@github.com:screwdriver-cd/data-model.git#master'; + const config = { token, checkoutUrl }; + + it('returns error when invalid config object', () => + instance.addDeployKey({}) + .then(() => { + assert.fail('you will never get dis'); + }) + .catch((err) => { + assert.instanceOf(err, Error); + assert.equal(err.name, 'ValidationError'); + }) + ); it('returns data from underlying method', () => { instance._addDeployKey = () => Promise.resolve(privKey); @@ -90,10 +107,11 @@ describe('index test', () => { }); it('returns not implemented', () => - instance.addDeployKey({ token, checkoutUrl }) + instance.addDeployKey(config) .then(() => { - assert.fail('This should not fail the test'); - }, (err) => { + assert.fail('you will never get dis'); + }) + .catch((err) => { assert.equal(err.message, 'Not implemented'); }) ); @@ -402,16 +420,16 @@ describe('index test', () => { org: 'screwdriver-cd', repo: 'guide', sha: '12345', + scmContext: 'github:github.com', + prRef: 'prRef', prSource: 'branch', prBranchName: 'prBranchName', - prRef: 'prRef', - scmContext: 'github:github.com', parentConfig: { - branch: 'master', + sha: '54321', host: 'github.com', + branch: 'master', org: 'screwdriver-cd', - repo: 'parent-to-guide', - sha: '54321' + repo: 'parent-to-guide' } }); @@ -565,11 +583,18 @@ describe('index test', () => { }); describe('getPermissons', () => { - const config = { + const permConfig = { scmUri: 'github.com:repoId:branch', token, scmContext: 'github:github.com' }; + const config = { + readOnly: readOnlyConfig + }; + + beforeEach(() => { + instance.configure(config); + }); it('returns error when invalid config object', () => instance.getPermissions({}) @@ -587,7 +612,7 @@ describe('index test', () => { invalid: 'object' }); - return instance.getPermissions(config) + return instance.getPermissions(permConfig) .then(() => { assert.fail('you will never get dis'); }) @@ -597,15 +622,28 @@ describe('index test', () => { }); }); - it('returns not implemented', () => - instance.getPermissions(config) + it('returns true permissions for read-only SCM', () => + instance.getPermissions(permConfig) + .then((result) => { + assert.deepEqual(result, { + admin: true, + push: true, + pull: true + }); + }) + ); + + it('returns not implemented', () => { + instance.configure({}); + + return instance.getPermissions(permConfig) .then(() => { assert.fail('you will never get dis'); }) .catch((err) => { assert.equal(err.message, 'Not implemented'); - }) - ); + }); + }); }); describe('getOrgPermissions', () => { @@ -1060,6 +1098,25 @@ describe('index test', () => { }); }); + describe('getScmContext', () => { + it('returns error when invalid output', () => { + instance._getScmContext = () => 'invalid'; + + const result = instance.getScmContext({ hostname: 'github.com' }); + + assert.instanceOf(result, Error); + assert.equal(result.name, 'ValidationError'); + }); + + it('returns not implemented', () => { + try { + instance.getScmContext({ hostname: 'github.com' }); + } catch (err) { + assert.equal(err.message, 'Not implemented'); + } + }); + }); + describe('canHandleWebhook', () => { const headers = { stuff: 'foo' @@ -1111,6 +1168,25 @@ describe('index test', () => { }); }); + describe('getReadOnlyInfo', () => { + const config = { + readOnly: readOnlyConfig + }; + + beforeEach(() => { + instance.configure(config); + }); + + it('returns false if no configuration', () => { + instance.configure({}); + assert.deepEqual(instance.getReadOnlyInfo(), {}); + }); + + it('returns actual boolean when set', () => { + assert.deepEqual(instance.getReadOnlyInfo(), config.readOnly); + }); + }); + describe('autoDeployKeyGenerationEnabled', () => { const config = { autoDeployKeyGeneration: true @@ -1131,9 +1207,23 @@ describe('index test', () => { }); describe('getWebhookEventsMapping', () => { + const webhookEventsMapping = { + pr: 'merge_requests_events', + commit: 'push_events' + }; + + it('returns data from underlying method', () => { + instance._getWebhookEventsMapping = () => Promise.resolve(webhookEventsMapping); + + return instance.getWebhookEventsMapping() + .then((output) => { + assert.deepEqual(output, webhookEventsMapping); + }); + }); + it('returns not implemented', () => { try { - instance.getScmContexts(); + instance.getWebhookEventsMapping({ scmContext: 'gitlab:gitlab.com' }); } catch (err) { assert.equal(err.message, 'Not implemented'); }