diff --git a/README.md b/README.md index 63dc22b..21c26e1 100644 --- a/README.md +++ b/README.md @@ -102,10 +102,15 @@ client.refreshTokens() ``` ## PullRequests -### getPullRequests(params, callback) +### getPullRequests(options, callback) Sends request to Bitbucket API in order to get all pull requests by given parameters. Returns a promise. -- `params` - Query string parameters for get request. Based on [Bitbucket documentation](https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Busername%7D/%7Brepo_slug%7D/pullrequests) -- `callback` - Usually used as a callback parameter for recursion if there is more than one page of pull requests +- `options` + - `q (optional)` - Search query for get request based on [Bitbucket documentation](https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Busername%7D/%7Brepo_slug%7D/pullrequests). If present search will be performed for this query. If missing at least one of the following fields is required and query will be build internally with `AND` operator: state, updatedOn, destinationBranch + - `state (required if q is missing)` - Pull request state: MERGED, SUPERSEDED, OPEN, DECLINED + - `updatedOn (optional)` - an unquoted ISO-8601 date time string with the timezone offset, milliseconds and entire time component being optional + - `destinationBranch (optional)` - destination branch name + - `page (optional)` - page number if you want to skip pages +- `pullRequestsList` - Previously stored pull requests. Used in recursion when there is next page or if you want to append an already existing list to newly pulled one Parameters must be an object with values based on Bitbucket API guidelines. In case of request failure because expired access token automatically calls `refreshTokens` from above and tries to refresh tokens. In case of success to refresh access token executes again `getPullRequests` with the same parameters. In case of failure to refresh access token throws an exception. @@ -197,4 +202,6 @@ pullRequests.getPullRequests({ # Known issues * All TODO across the code -* Tokens do not update automatically in config json \ No newline at end of file +* Tokens do not update automatically in config json +* Pull requests query to be build with different than AND operator +* Ability to pass standard date time or string to getPullRequests and internally convert them to ISO-8601 format diff --git a/lib/PullRequests.js b/lib/PullRequests.js index daa374f..8f5f15e 100644 --- a/lib/PullRequests.js +++ b/lib/PullRequests.js @@ -51,11 +51,20 @@ class PullRequests { /* * Gets all pull requests * - * @param {Object} parameters - Query parameters for GET request + * @param {Object} options - Search request parameters * @param {Object} pullRequestsList - Previously stored pull requests. Used in recursion when there is next page * @returns {Object|function} - serializedPullRequests if there is no next page return pull requests | this.getPullRequests if there is next page */ - getPullRequests(parameters, pullRequestsList) { + getPullRequests(options, pullRequestsList) { + let params = {}; + + if (options === undefined || !Object.keys(options).length) { + throw new Error('Missing search parameters'); + } + + 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 let body = { method: 'GET', @@ -63,7 +72,7 @@ class PullRequests { headers: { 'content-type': 'application/x-www-form-urlencoded' }, - qs: parameters, + qs: params, auth: { bearer: this.bitbucket.accessToken } @@ -73,14 +82,13 @@ class PullRequests { .then(data => { try { let pullRequests = JSON.parse(data); // Parses pull requests from API response - let serializedPullRequests = serializePullRequestsData.call(this, pullRequests.values, pullRequestsList); + let serializedPullRequests = serializePullRequestsData.call(this, pullRequests.values, pullRequestsObj); // If there is next page make a recursion if (pullRequests.next) { - parameters.page = parameters.page || 1; - parameters.page++; + options.page = ++params.page; - return this.getPullRequests(parameters, serializedPullRequests); + return this.getPullRequests(options, serializedPullRequests); } return serializedPullRequests; @@ -102,7 +110,7 @@ class PullRequests { // requires a return because it is a promise return this.bitbucket.refreshTokens() .then(() => { - return this.getPullRequests(parameters, pullRequestsObj); + return this.getPullRequests(options, pullRequestsObj); }) .catch(err => { throw new Error('PullRequests: Can not refresh access token. Stack trace: ' + err); @@ -111,6 +119,13 @@ class PullRequests { } } +/* + * 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 + * @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 @@ -151,4 +166,36 @@ function serializePullRequestsData(pullRequests, targetPullRequests) { return result; } +/* + * Builds request search query + * + * @params {String} params - Parameter for search query + * @returns {String} queryString - Generated search query + */ +function buildSearchQuery(params) { + let queryString = ''; + + if (params.state) { + queryString += 'state="' + params.state + '"'; + } + + if (params.updatedOn) { + if (queryString) { + queryString += ' AND updated_on >= ' + params.updatedOn; + } else { + queryString += 'updated_on >= ' + params.updatedOn; + } + } + + if (params.destinationBranch) { + if (queryString) { + queryString += ' AND destination.branch.name="' + params.destinationBranch + '"'; + } else { + queryString += 'destination.branch.name="' + params.destinationBranch + '"'; + } + } + + return queryString; +} + module.exports = PullRequests; \ No newline at end of file diff --git a/package.json b/package.json index 36336f2..937b0a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bitbucket-notifications", - "version": "1.2.0", + "version": "1.2.1", "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": { diff --git a/test/PullRequests.js b/test/PullRequests.js index 6eb0cbc..38487bb 100644 --- a/test/PullRequests.js +++ b/test/PullRequests.js @@ -130,6 +130,132 @@ describe('PullRequests', function() { }); }); + it('should throw an error if options are missing', function() { + // arrange + + // act + let pullRequests = new PullRequests(bitbucket, username, repoSlug); + let pullRequestsData = function() { + return pullRequests.getPullRequests(); + }; + + // assert + expect(pullRequestsData).to.throw('Missing search parameters'); + }); + + it('should throw an error if options are passed but empty', function() { + // arrange + + // act + let pullRequests = new PullRequests(bitbucket, username, repoSlug); + let pullRequestsData = function() { + return pullRequests.getPullRequests({}); + }; + + // assert + expect(pullRequestsData).to.throw('Missing search parameters'); + }); + + it('should use user search query instead of building new one', function() { + // arrange + + // act + let pullRequests = new PullRequests(bitbucket, username, repoSlug); + let pullRequestsData = pullRequests.getPullRequests({ + q: 'state="MERGED"', + state: 'OPEN' + }); + + // assert + expect(promise.getCall(0).args[0].qs).to.eql({ + q: 'state="MERGED"', + page: 1 + }); + }); + + it('should build new search query if q is missing', function() { + // arrange + + // act + let pullRequests = new PullRequests(bitbucket, username, repoSlug); + let pullRequestsData = pullRequests.getPullRequests({ + state: 'OPEN' + }); + + // assert + expect(promise.getCall(0).args[0].qs).to.eql({ + q: 'state="OPEN"', + page: 1 + }); + }); + + it('should build new search query with updated on parameter', function() { + // arrange + + // act + let pullRequests = new PullRequests(bitbucket, username, repoSlug); + let pullRequestsData = pullRequests.getPullRequests({ + updatedOn: '6-06-6006' + }); + + // assert + expect(promise.getCall(0).args[0].qs).to.eql({ + q: 'updated_on >= 6-06-6006', + page: 1 + }); + }); + + it('should build new search query with destination branch name parameter', function() { + // arrange + + // act + let pullRequests = new PullRequests(bitbucket, username, repoSlug); + let pullRequestsData = pullRequests.getPullRequests({ + destinationBranch: 'foobar' + }); + + // assert + expect(promise.getCall(0).args[0].qs).to.eql({ + q: 'destination.branch.name="foobar"', + page: 1 + }); + }); + + it('should append updatedOn parameter to state', function() { + // arrange + + // act + let pullRequests = new PullRequests(bitbucket, username, repoSlug); + let pullRequestsData = pullRequests.getPullRequests({ + state: 'MERGED', + updatedOn: '6-06-6006' + }); + + // assert + expect(promise.getCall(0).args[0].qs).to.eql({ + q: 'state="MERGED" AND updated_on >= 6-06-6006', + page: 1 + }); + }); + + it('should append branch name parameter to state and updatedOn', function() { + // arrange + + // act + let pullRequests = new PullRequests(bitbucket, username, repoSlug); + let pullRequestsData = pullRequests.getPullRequests({ + state: 'MERGED', + updatedOn: '6-06-6006', + destinationBranch: 'foobar' + }); + + // assert + expect(promise.getCall(0).args[0].qs).to.eql({ + q: 'state="MERGED" AND updated_on >= 6-06-6006 AND destination.branch.name="foobar"', + page: 1 + }); + }); + it('should throw an error if response is rejected and can not refresh tokens', function() { // arrange promise.rejects('bad request');