diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..8004650 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,281 @@ +module.exports = { + "env": { + "es6": true, + "node": true + }, + "globals": { + "after": true, + "afterEach": true, + "before": true, + "beforeEach": true, + "describe": true, + "it": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "accessor-pairs": "error", + "array-bracket-newline": "off", + "array-bracket-spacing": [ + "error", + "never" + ], + "array-callback-return": "error", + "array-element-newline": "off", + "arrow-body-style": "error", + "arrow-parens": "error", + "arrow-spacing": "error", + "block-scoped-var": "error", + "block-spacing": "error", + "brace-style": "error", + "callback-return": "error", + "camelcase": [ + "error", { + "properties": "never" + } + ], + "capitalized-comments": [ + "error", + "always", { + "ignorePattern": "arrange|act|assert" + } + ], + "class-methods-use-this": "error", + "comma-dangle": "off", + "comma-spacing": [ + "error", { + "after": true, + "before": false + } + ], + "comma-style": [ + "error", + "last" + ], + "complexity": "error", + "computed-property-spacing": [ + "error", + "never" + ], + "consistent-return": "error", + "consistent-this": "error", + "curly": "error", + "default-case": "error", + "dot-location": ["error", "property"], + "dot-notation": [ + "error", { + "allowKeywords": true + } + ], + "eol-last": [ + "error", + "never" + ], + "eqeqeq": "error", + "for-direction": "error", + "func-call-spacing": "error", + "func-name-matching": "error", + "func-names": [ + "error", + "never" + ], + "function-paren-newline": "error", + "generator-star-spacing": "error", + "getter-return": "error", + "global-require": "off", + "guard-for-in": "error", + "handle-callback-err": "error", + "id-blacklist": "error", + "id-length": [ + "error", { + "properties": "never" + } + ], + "id-match": "error", + "implicit-arrow-linebreak": "error", + "indent": "error", + "indent-legacy": "error", + "init-declarations": "off", + "jsx-quotes": "error", + "key-spacing": "error", + "keyword-spacing": "error", + "line-comment-position": "error", + "lines-around-comment": "error", + "lines-around-directive": "error", + "lines-between-class-members": "error", + "max-depth": "error", + "max-len": "off", + "max-lines": "off", + "max-nested-callbacks": "error", + "max-params": ["error", 5], + "max-statements": "off", + "max-statements-per-line": "error", + "multiline-comment-style": "error", + "multiline-ternary": "error", + "new-cap": "error", + "new-parens": "error", + "newline-after-var": "off", + "newline-before-return": "error", + "newline-per-chained-call": "error", + "no-alert": "error", + "no-array-constructor": "error", + "no-await-in-loop": "error", + "no-bitwise": "error", + "no-buffer-constructor": "error", + "no-caller": "error", + "no-catch-shadow": "error", + "no-confusing-arrow": "error", + "no-continue": "error", + "no-div-regex": "error", + "no-duplicate-imports": "error", + "no-else-return": "error", + "no-empty-function": "error", + "no-eq-null": "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-extra-label": "error", + "no-extra-parens": "error", + "no-floating-decimal": "error", + "no-implicit-coercion": "error", + "no-implicit-globals": "error", + "no-implied-eval": "error", + "no-inline-comments": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-lonely-if": "error", + "no-loop-func": "error", + "no-magic-numbers": "off", + "no-mixed-operators": "error", + "no-mixed-requires": "off", + "no-multi-assign": "error", + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-multiple-empty-lines": "error", + "no-native-reassign": "error", + "no-negated-condition": "error", + "no-negated-in-lhs": "error", + "no-nested-ternary": "error", + "no-new": "off", + "no-new-func": "error", + "no-new-object": "error", + "no-new-require": "error", + "no-new-wrappers": "error", + "no-octal-escape": "error", + "no-param-reassign": "error", + "no-path-concat": "error", + "no-plusplus": "error", + "no-process-env": "error", + "no-process-exit": "error", + "no-proto": "error", + "no-prototype-builtins": "error", + "no-restricted-globals": "error", + "no-restricted-imports": "error", + "no-restricted-modules": "error", + "no-restricted-properties": "error", + "no-restricted-syntax": "error", + "no-return-assign": "error", + "no-return-await": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow": "error", + "no-shadow-restricted-names": "error", + "no-spaced-func": "error", + "no-sync": "error", + "no-tabs": "error", + "no-template-curly-in-string": "error", + "no-ternary": "error", + "no-throw-literal": "error", + "no-trailing-spaces": "error", + "no-undef-init": "error", + "no-undefined": "off", + "no-underscore-dangle": "off", + "no-unmodified-loop-condition": "error", + "no-unneeded-ternary": "error", + "no-unused-expressions": "off", + "no-use-before-define": ["error", { + "functions": false, + "classes": true, + "variables": true + }], + "no-useless-call": "error", + "no-useless-computed-key": "error", + "no-useless-concat": "error", + "no-useless-constructor": "error", + "no-useless-rename": "error", + "no-useless-return": "error", + "no-var": "error", + "no-void": "error", + "no-whitespace-before-property": "error", + "no-with": "error", + "nonblock-statement-body-position": "error", + "object-curly-newline": "off", + "object-curly-spacing": "error", + "object-property-newline": "error", + "object-shorthand": "off", + "one-var": "off", + "one-var-declaration-per-line": "error", + "operator-assignment": "error", + "operator-linebreak": "error", + "padded-blocks": "off", + "padding-line-between-statements": "error", + "prefer-arrow-callback": "off", + "prefer-const": "off", + "prefer-destructuring": "off", + "prefer-numeric-literals": "error", + "prefer-promise-reject-errors": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "quote-props": "off", + "quotes": "off", + "radix": "error", + "require-await": "error", + "require-jsdoc": "error", + "rest-spread-spacing": "error", + "semi": "error", + "semi-spacing": "error", + "semi-style": [ + "error", + "last" + ], + "sort-imports": "error", + "sort-keys": "off", + "sort-vars": "off", + "space-before-blocks": "error", + "space-before-function-paren": "off", + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": "error", + "space-unary-ops": "error", + "spaced-comment": [ + "error", + "always" + ], + "strict": "error", + "switch-colon-spacing": "error", + "symbol-description": "error", + "template-curly-spacing": [ + "error", + "never" + ], + "template-tag-spacing": "error", + "unicode-bom": [ + "error", + "never" + ], + "valid-jsdoc": "error", + "vars-on-top": "error", + "wrap-iife": "error", + "wrap-regex": "error", + "yield-star-spacing": "error", + "yoda": "error" + } +}; \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index c1677b3..a68f62c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,10 @@ language: node_js node_js: - node - lts/* + +before_script: + - npm run lint + notifications: email: - mr.v.radev@gmail.com @@ -10,4 +14,5 @@ notifications: on_success: change # options: [always|never|change] default: always on_failure: always # options: [always|never|change] default: always on_start: never # options: [always|never|change] default: always + sudo: false \ No newline at end of file diff --git a/lib/Bitbucket.js b/lib/Bitbucket.js index 8db1379..a4a61cc 100644 --- a/lib/Bitbucket.js +++ b/lib/Bitbucket.js @@ -68,19 +68,19 @@ class Bitbucket { }); return request(body) - .then(data => { + .then((data) => { try { let tokens = JSON.parse(data); // TODO: Write new tokens in config file - this.accessToken = tokens['access_token']; - this.refreshToken = tokens['refresh_token']; - } catch (e) { - throw new Error('Cannot parse tokens. Stack trace: ' + e); + this.accessToken = tokens.access_token; + this.refreshToken = tokens.refresh_token; + } catch (err) { + throw new Error(`Cannot parse tokens. Stack trace: ${err}`); } }) - .catch(err => { - throw new Error('Can not fetch client tokens. Stack trace: ' + err); + .catch((err) => { + throw new Error(`Can not fetch client tokens. Stack trace: ${err}`); }); } @@ -94,29 +94,37 @@ class Bitbucket { }); return request(body) - .then(data => { + .then((data) => { try { let tokens = JSON.parse(data); // TODO: Write new tokens in config file - this.accessToken = tokens['access_token']; - } catch (e) { - throw new Error('Cannot parse tokens. Stack trace: ' + e); + this.accessToken = tokens.access_token; + } catch (err) { + throw new Error(`Cannot parse tokens. Stack trace: ${err}`); } }) - .catch(err => { - throw new Error('Can not refresh access token by given refresh: ' + this.refreshToken + '. Stack trace: ' + err); + .catch((err) => { + throw new Error(`Can not refresh access token by given refresh: ${this.refreshToken}. Stack trace: ${err}`); }); } - // Append features to client object. - // Intention behind this idea is to make different calls with single instance of the client. - // e.g. request pull requests from different repositories with single client instance + /* + * Append features to client object. + * Intention behind this idea is to make different calls with single instance of the client. + * E.g. request pull requests from different repositories with single client instance + */ pullRequests(username, repoSlug, options) { - return (new PullRequests(this, username, repoSlug, options)); + return new PullRequests(this, username, repoSlug, options); } } +/** + * Builds request body for bitbucket by given configuration + * + * @param {Object} configuration - form body configuration + * @returns {String} options - Request body + */ function buildRequestBody(configuration) { let options = { method: 'POST', diff --git a/lib/Gmail.js b/lib/Gmail.js index 9120051..d60ad41 100644 --- a/lib/Gmail.js +++ b/lib/Gmail.js @@ -1,9 +1,11 @@ const nodemailer = require('nodemailer'); +/* + * Gmail requires browser authorisation and client must verify requested access rights to his account by the application. + * This means there is no way to obtain tokens with calls. + * Therefore you must obtain your refresh token via browser, postman, google playground or another preferable by you way. + */ class Gmail { - // Gmail requires browser authorisation and client must verify requested access rights to his account by the application. - // This means there is no way to obtain tokens with calls. - // Therefore you must obtain your refresh token via browser, postman, google playground or another preferable by you way. constructor(user, clientId, clientSecret, accessToken, refreshToken) { this.user = user; this.id = clientId; @@ -124,10 +126,10 @@ class Gmail { // Send email with options from above. If something is missing error will be thrown and email won't send transporter.sendMail(mailOptions, (error, info) => { if (error) { - throw new Error('Can not send email. Stack trace: ' + error); + throw new Error(`Can not send email. Stack trace: ${error}`); } - console.log('Message sent: %s', info.messageId); + console.log('Message sent: %s', info.messageId); // eslint-disable-line no-console }); } } diff --git a/lib/Jira.js b/lib/Jira.js index 674a792..f3f5269 100644 --- a/lib/Jira.js +++ b/lib/Jira.js @@ -1,7 +1,7 @@ const request = require('request-promise'); -const browseUrl = 'https://{domain}.atlassian.net/browse/{ticketId}'; -const transitionUrl = 'https://{domain}.atlassian.net/rest/api/2/issue/{issueIdOrKey}/transitions'; +const browseUrlPattern = 'https://{domain}.atlassian.net/browse/{ticketId}'; +const transitionUrlPattern = 'https://{domain}.atlassian.net/rest/api/2/issue/{issueIdOrKey}/transitions'; class Jira { constructor(domain, username, authorisationToken) { @@ -10,8 +10,8 @@ class Jira { this.authorisationToken = authorisationToken; if (this.domain) { - this.browseUrl = browseUrl.replace('{domain}', this.domain); - this.transitionUrl = transitionUrl.replace('{domain}', this.domain); + this.browseUrl = browseUrlPattern.replace('{domain}', this.domain); + this.transitionUrl = transitionUrlPattern.replace('{domain}', this.domain); } } @@ -97,8 +97,8 @@ class Jira { // Reponse is 204 No content therefore no need of then statement return request(body) - .catch(err => { - throw new Error('Can not transition issue ' + issueId + ' to ' + options.transition.id + '. Stack trace: ' + err); + .catch((err) => { + throw new Error(`Can not transition issue ${issueId} to ${options.transition.id}. Stack trace: ${err}`); }); } } diff --git a/lib/PullRequests.js b/lib/PullRequests.js index 8f5f15e..2bb0cfc 100644 --- a/lib/PullRequests.js +++ b/lib/PullRequests.js @@ -40,12 +40,19 @@ class PullRequests { } set options(options) { - options = options || {}; - options.regExp = options.regExp || /[a-zA-Z]+-[0-9]+/; - options.addJiraLinks = options.addJiraLinks || false; - options.jira = options.jira || undefined; + let opts = {}; - this._options = options; + if (options) { + opts.regExp = options.regExp || /[a-zA-Z]+-[0-9]+/; + opts.addJiraLinks = options.addJiraLinks || false; + opts.jira = options.jira || undefined; + } else { + opts.regExp = /[a-zA-Z]+-[0-9]+/; + opts.addJiraLinks = false; + opts.jira = undefined; + } + + this._options = opts; } /* @@ -65,7 +72,8 @@ class PullRequests { params.q = options.q || buildSearchQuery(options); params.page = options.page || 1; - let pullRequestsObj = pullRequestsList || {}; // Checks if there are pull requests from previous recursion. If there are takes them if not - creates new empty object + // Checks if there are pull requests from previous recursion. If there are takes them if not - creates new empty object + let pullRequestsObj = pullRequestsList || {}; let body = { method: 'GET', uri: this.pullRequestsUrl, @@ -79,57 +87,60 @@ class PullRequests { }; return request(body) - .then(data => { + .then((data) => { try { - let pullRequests = JSON.parse(data); // Parses pull requests from API response + // Parses pull requests from API response + let pullRequests = JSON.parse(data); let serializedPullRequests = serializePullRequestsData.call(this, pullRequests.values, pullRequestsObj); // If there is next page make a recursion if (pullRequests.next) { - options.page = ++params.page; + options.page = params.page + 1; return this.getPullRequests(options, serializedPullRequests); } return serializedPullRequests; - } catch (e) { - // Set retries to one as it will bubble up to promise catch statement below. - // There it will retry once again which is not necessary as the problem is with parsing not with access token + } catch (err) { + + /* + * Set retries to one as it will bubble up to promise catch statement below. + * There it will retry once again which is not necessary as the problem is with parsing not with access token + */ this.retries = 1; - throw new Error('Can not parse pull requests. Stack trace: ' + e); + throw new Error(`Can not parse pull requests. Stack trace: ${err}`); } }) - .catch(err => { + .catch((err) => { if (this.retries !== 0) { this.retries = 0; - throw new Error('Maximum number of refresh token retries exceeded. Stack trace: ' + err); + throw new Error(`Maximum number of refresh token retries exceeded. Stack trace: ${err}`); } - this.retries++; + this.retries = this.retries + 1; - // requires a return because it is a promise + // Requires a return because it is a promise return this.bitbucket.refreshTokens() - .then(() => { - return this.getPullRequests(options, pullRequestsObj); - }) - .catch(err => { - throw new Error('PullRequests: Can not refresh access token. Stack trace: ' + err); + .then(() => this.getPullRequests(options, pullRequestsObj)) + .catch((responseError) => { + throw new Error(`PullRequests: Can not refresh access token. Stack trace: ${responseError}`); }); }); } } -/* +/** * Serialzies pull request data in required format for future use * - * @params {Object} pullRequests - Pull requests from Bitbucket response - * @params {Object} targetPullRequests - Previously serialzied pull requests + * @param {Object} pullRequests - Pull requests from Bitbucket response + * @param {Object} targetPullRequests - Previously serialzied pull requests * @returns {Object} result - Serialized pull requests */ function serializePullRequestsData(pullRequests, targetPullRequests) { - let result = Object.assign({}, targetPullRequests); // Cloning target object so we don't change it by reference + // Cloning target object so we don't change it by reference + let result = Object.assign({}, targetPullRequests); - for (let item in pullRequests) { + for (let item in pullRequests) { // eslint-disable-line guard-for-in let pullRequest = pullRequests[item]; let destinationBranch = pullRequest.destination.branch.name; @@ -166,32 +177,32 @@ function serializePullRequestsData(pullRequests, targetPullRequests) { return result; } -/* +/** * Builds request search query * - * @params {String} params - Parameter for search query + * @param {String} params - Parameter for search query * @returns {String} queryString - Generated search query */ function buildSearchQuery(params) { let queryString = ''; if (params.state) { - queryString += 'state="' + params.state + '"'; + queryString += `state="${params.state}"`; } if (params.updatedOn) { if (queryString) { - queryString += ' AND updated_on >= ' + params.updatedOn; + queryString += ` AND updated_on >= ${params.updatedOn}`; } else { - queryString += 'updated_on >= ' + params.updatedOn; + queryString += `updated_on >= ${params.updatedOn}`; } } if (params.destinationBranch) { if (queryString) { - queryString += ' AND destination.branch.name="' + params.destinationBranch + '"'; + queryString += ` AND destination.branch.name="${params.destinationBranch}"`; } else { - queryString += 'destination.branch.name="' + params.destinationBranch + '"'; + queryString += `destination.branch.name="${params.destinationBranch}"`; } } diff --git a/package.json b/package.json index 937b0a1..b8a2400 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "bitbucket-notifications", - "version": "1.2.1", + "version": "1.2.2", "description": "Node.js application which can send an email with links to all PRs that have been merged in last 24 hours. It connects to Bitbucket, Gmail and Jira with OAuth2 for higher security by simply adding your credentials in configuration file.", "main": "index.js", "scripts": { "test": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", + "lint": "eslint ./lib ./test", "devCoverage": "istanbul cover ./node_modules/mocha/bin/_mocha" }, "repository": { @@ -27,19 +28,20 @@ "license": "MIT", "dependencies": { "handlebars": "^4.0.11", - "moment": "^2.20.1", - "nodemailer": "^4.4.1", - "request": "^2.83.0", + "moment": "^2.22.2", + "nodemailer": "^4.6.7", + "request": "^2.87.0", "request-promise": "^4.2.2" }, "devDependencies": { "chai": "^4.1.2", "chai-as-promised": "^7.1.1", - "coveralls": "^3.0.0", + "coveralls": "^3.0.1", + "eslint": "^4.19.1", "istanbul": "^0.4.5", - "mocha": "^5.0.0", + "mocha": "^5.2.0", "mocha-lcov-reporter": "^1.3.0", - "proxyquire": "^1.8.0", + "proxyquire": "^2.0.1", "sinon": "^4.2.2", "sinon-stub-promise": "^4.0.0" } diff --git a/test/Bitbucket.js b/test/Bitbucket.js index 4c12b83..f7a4503 100644 --- a/test/Bitbucket.js +++ b/test/Bitbucket.js @@ -135,7 +135,7 @@ describe('Bitbucket', function() { // act let client = new Bitbucket(clientId, clientSecret, accessToken, refreshToken); - let result = client.obtainTokens(); + client.obtainTokens(); // assert expect(client.accessToken).to.equal(response.access_token); @@ -188,7 +188,7 @@ describe('Bitbucket', function() { // act let client = new Bitbucket(clientId, clientSecret, accessToken, refreshToken); - let result = client.refreshTokens(); + client.refreshTokens(); // assert expect(client.accessToken).to.equal(response.access_token); @@ -213,7 +213,7 @@ describe('Bitbucket', function() { // act let client = new Bitbucket(clientId, clientSecret, accessToken, refreshToken); - let result = client.pullRequests(username, repoSlug); + client.pullRequests(username, repoSlug); // assert expect(PullRequests.callCount).to.equal(1); diff --git a/test/Gmail.js b/test/Gmail.js index 223a184..d37ef71 100644 --- a/test/Gmail.js +++ b/test/Gmail.js @@ -106,7 +106,6 @@ describe('Gmail', function() { let nodemailer = {}, transporter = {}, sender, - recipientsObject, subject, content, gmail; diff --git a/test/Jira.js b/test/Jira.js index cb12aaa..97f174d 100644 --- a/test/Jira.js +++ b/test/Jira.js @@ -245,7 +245,7 @@ describe('Jira', function() { // act let jira = new Jira(domain, username, authorisationToken); - let result = jira.transitionIssue(issueId, options); + jira.transitionIssue(issueId, options); // assert expect(promise.callCount).to.equal(1); diff --git a/test/PullRequests.js b/test/PullRequests.js index 38487bb..b5f6aca 100644 --- a/test/PullRequests.js +++ b/test/PullRequests.js @@ -64,6 +64,23 @@ describe('PullRequests', function() { expect(pullPequests.options).to.eql(expected); }); + it('should set options if not all are passed', function() { + // arrange + let expected = { + regExp: /[a-zA-Z]{2-5}-[0-9]{2-5}/, + addJiraLinks: false, + jira: undefined + }; + + // act + let pullPequests = new PullRequests(bitbucket, username, repoSlug, { + regExp: /[a-zA-Z]{2-5}-[0-9]{2-5}/ + }); + + // assert + expect(pullPequests.options).to.eql(expected); + }); + it('should set options if all are passed', function() { // arrange let expected = { @@ -161,7 +178,7 @@ describe('PullRequests', function() { // act let pullRequests = new PullRequests(bitbucket, username, repoSlug); - let pullRequestsData = pullRequests.getPullRequests({ + pullRequests.getPullRequests({ q: 'state="MERGED"', state: 'OPEN' }); @@ -178,7 +195,7 @@ describe('PullRequests', function() { // act let pullRequests = new PullRequests(bitbucket, username, repoSlug); - let pullRequestsData = pullRequests.getPullRequests({ + pullRequests.getPullRequests({ state: 'OPEN' }); @@ -194,7 +211,7 @@ describe('PullRequests', function() { // act let pullRequests = new PullRequests(bitbucket, username, repoSlug); - let pullRequestsData = pullRequests.getPullRequests({ + pullRequests.getPullRequests({ updatedOn: '6-06-6006' }); @@ -210,7 +227,7 @@ describe('PullRequests', function() { // act let pullRequests = new PullRequests(bitbucket, username, repoSlug); - let pullRequestsData = pullRequests.getPullRequests({ + pullRequests.getPullRequests({ destinationBranch: 'foobar' }); @@ -226,7 +243,7 @@ describe('PullRequests', function() { // act let pullRequests = new PullRequests(bitbucket, username, repoSlug); - let pullRequestsData = pullRequests.getPullRequests({ + pullRequests.getPullRequests({ state: 'MERGED', updatedOn: '6-06-6006' }); @@ -243,7 +260,7 @@ describe('PullRequests', function() { // act let pullRequests = new PullRequests(bitbucket, username, repoSlug); - let pullRequestsData = pullRequests.getPullRequests({ + pullRequests.getPullRequests({ state: 'MERGED', updatedOn: '6-06-6006', destinationBranch: 'foobar'