From 4268802ebf9ee9597951ed614cc76bf42c75e601 Mon Sep 17 00:00:00 2001 From: Clay Reimann Date: Thu, 12 May 2016 16:27:49 -0500 Subject: [PATCH] feature(Issue): add Milestone API Closes #68 Closes #268 --- CHANGELOG.md | 5 +++ lib/Issue.js | 56 +++++++++++++++++++++++++ lib/Requestable.js | 19 +++++---- test/issue.spec.js | 100 ++++++++++++++++++++++++++++++++++++--------- 4 files changed, 153 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fec4bcf..5650c231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## master ### Features +Added Milestone API +* `Issue.listMilestones` +* `Issue.getMilestone` +* `Issue.editMilestone` +* `Issue.deleteMilestone` ### Fixes diff --git a/lib/Issue.js b/lib/Issue.js index 706c3b60..d1e35c89 100644 --- a/lib/Issue.js +++ b/lib/Issue.js @@ -123,6 +123,62 @@ class Issue extends Requestable { getIssue(issue, cb) { return this._request('GET', `/repos/${this.__repository}/issues/${issue}`, null, cb); } + + /** + * List the milestones for the repository + * @see https://developer.github.com/v3/issues/milestones/#list-milestones-for-a-repository + * @param {Object} options - filtering options + * @param {Requestable.callback} [cb] - will receive the array of milestones + * @return {Promise} - the promise for the http request + */ + listMilestones(options, cb) { + return this._request('GET', `/repos/${this.__repository}/milestones`, options, cb); + } + + /** + * Get a milestone + * @see https://developer.github.com/v3/issues/milestones/#get-a-single-milestone + * @param {string} milestone - the id of the milestone to fetch + * @param {Requestable.callback} [cb] - will receive the array of milestones + * @return {Promise} - the promise for the http request + */ + getMilestone(milestone, cb) { + return this._request('GET', `/repos/${this.__repository}/milestones/${milestone}`, null, cb); + } + + /** + * Create a new milestone + * @see https://developer.github.com/v3/issues/milestones/#create-a-milestone + * @param {Object} milestoneData - the milestone definition + * @param {Requestable.callback} [cb] - will receive the array of milestones + * @return {Promise} - the promise for the http request + */ + createMilestone(milestoneData, cb) { + return this._request('POST', `/repos/${this.__repository}/milestones`, milestoneData, cb); + } + + /** + * Edit a milestone + * @see https://developer.github.com/v3/issues/milestones/#update-a-milestone + * @param {string} milestone - the id of the milestone to edit + * @param {Object} milestoneData - the updates to make to the milestone + * @param {Requestable.callback} [cb] - will receive the array of milestones + * @return {Promise} - the promise for the http request + */ + editMilestone(milestone, milestoneData, cb) { + return this._request('PATCH', `/repos/${this.__repository}/milestones/${milestone}`, milestoneData, cb); + } + + /** + * Delete a milestone (this is distinct from closing a milestone) + * @see https://developer.github.com/v3/issues/milestones/#delete-a-milestone + * @param {string} milestone - the id of the milestone to delete + * @param {Requestable.callback} [cb] - will receive the array of milestones + * @return {Promise} - the promise for the http request + */ + deleteMilestone(milestone, cb) { + return this._request('DELETE', `/repos/${this.__repository}/milestones/${milestone}`, null, cb); + } } module.exports = Issue; diff --git a/lib/Requestable.js b/lib/Requestable.js index e447a7f7..b42dbe66 100644 --- a/lib/Requestable.js +++ b/lib/Requestable.js @@ -228,13 +228,14 @@ module.exports = Requestable; // ////////////////////////// // // Private helper functions // // ////////////////////////// // -function buildError(path, response) { - return { - path: path, - request: response.config, - response: response, - status: response.status - }; +class ResponseError extends Error { + constructor(path, response) { + super(`error making request ${response.config.method} ${response.config.url}`); + this.path = path; + this.request = response.config; + this.response = response; + this.status = response.status; + } } const METHODS_WITH_NO_BODY = ['GET', 'HEAD', 'DELETE']; @@ -256,10 +257,12 @@ function getNextPage(linksHeader = '') { function callbackErrorOrThrow(cb, path) { return function handler(response) { log(`error making request ${response.config.method} ${response.config.url} ${JSON.stringify(response.data)}`); - let error = buildError(path, response); + let error = new ResponseError(path, response); if (cb) { + log('going to error callback'); cb(error); } else { + log('throwing error'); throw error; } }; diff --git a/test/issue.spec.js b/test/issue.spec.js index bc0b224e..bc88a623 100644 --- a/test/issue.spec.js +++ b/test/issue.spec.js @@ -7,8 +7,6 @@ import {assertSuccessful} from './helpers/callbacks'; describe('Issue', function() { let github; let remoteIssues; - let remoteIssueId; - let remoteIssueCommentId; before(function() { github = new Github({ @@ -21,6 +19,9 @@ describe('Issue', function() { }); describe('reading', function() { + let remoteIssueId; + let milestoneId; + it('should list issues', function(done) { remoteIssues.listIssues({}, assertSuccessful(done, function(err, issues) { expect(issues).to.be.an.array(); @@ -37,9 +38,31 @@ describe('Issue', function() { done(); })); }); + + it('should get all milestones', function(done) { + remoteIssues.listMilestones() + .then(({data: milestones}) => { + expect(milestones).to.be.an.array(); + milestoneId = milestones[0].number; + + done(); + }).catch(done); + }); + + it('should get a single milestone', function(done) { + remoteIssues.getMilestone(milestoneId) + .then(({data: milestone}) => { + expect(milestone).to.have.own('title', 'Default Milestone'); + done(); + }).catch(done); + }); }); - describe('creating/modifiying/editing/deleting', function() { + describe('creating/editing/deleting', function() { + let createdIssueId; + let issueCommentId; + let createdMilestoneId; + // 200ms between tests so that Github has a chance to settle beforeEach(function(done) { setTimeout(done, 200); @@ -52,6 +75,7 @@ describe('Issue', function() { }; remoteIssues.createIssue(newIssue, assertSuccessful(done, function(err, issue) { + createdIssueId = issue.number; expect(issue).to.have.own('url'); expect(issue).to.have.own('title', newIssue.title); expect(issue).to.have.own('body', newIssue.body); @@ -60,8 +84,21 @@ describe('Issue', function() { })); }); + it('should edit issue', function(done) { + const newProps = { + title: 'Edited title', + state: 'closed' + }; + + remoteIssues.editIssue(createdIssueId, newProps, assertSuccessful(done, function(err, issue) { + expect(issue).to.have.own('title', newProps.title); + + done(); + })); + }); + it('should post issue comment', function(done) { - remoteIssues.createIssueComment(remoteIssueId, 'Comment test', assertSuccessful(done, function(err, issue) { + remoteIssues.createIssueComment(createdIssueId, 'Comment test', assertSuccessful(done, function(err, issue) { expect(issue).to.have.own('body', 'Comment test'); done(); @@ -69,47 +106,72 @@ describe('Issue', function() { }); it('should list issue comments', function(done) { - remoteIssues.listIssueComments(remoteIssueId, assertSuccessful(done, function(err, comments) { + remoteIssues.listIssueComments(createdIssueId, assertSuccessful(done, function(err, comments) { expect(comments).to.be.an.array(); expect(comments[0]).to.have.own('body', 'Comment test'); - remoteIssueCommentId = comments[0].id + issueCommentId = comments[0].id; done(); })); }); it('should get a single issue comment', function(done) { - remoteIssues.getIssueComment(remoteIssueCommentId, assertSuccessful(done, function(err, comment) { + remoteIssues.getIssueComment(issueCommentId, assertSuccessful(done, function(err, comment) { expect(comment).to.have.own('body', 'Comment test'); done(); })); }); it('should edit issue comment', function(done) { - remoteIssues.editIssueComment(remoteIssueCommentId, 'Comment test edited', assertSuccessful(done, function(err, comment) { - expect(comment).to.have.own('body', 'Comment test edited'); + remoteIssues.editIssueComment(issueCommentId, 'Comment test edited', + assertSuccessful(done, function(err, comment) { + expect(comment).to.have.own('body', 'Comment test edited'); - done(); - })); + done(); + })); }); it('should delete issue comment', function(done) { - remoteIssues.deleteIssueComment(remoteIssueCommentId, assertSuccessful(done, function(err, response) { + remoteIssues.deleteIssueComment(issueCommentId, assertSuccessful(done, function(err, response) { expect(response).to.be.true; done(); })); }); - it('should edit issues title', function(done) { - const newProps = { - title: 'Edited title' + it('should create a milestone', function(done) { + let milestone = { + title: 'v42', + description: 'The ultimate version' }; - remoteIssues.editIssue(remoteIssueId, newProps, assertSuccessful(done, function(err, issue) { - expect(issue).to.have.own('title', newProps.title); + remoteIssues.createMilestone(milestone) + .then(({data: createdMilestone}) => { + expect(createdMilestone).to.have.own('number'); + expect(createdMilestone).to.have.own('title', milestone.title); - done(); - })); + createdMilestoneId = createdMilestone.number; + done(); + }).catch(done); + }); + it('should update a milestone', function(done) { + let milestone = { + description: 'Version 6 * 7' + }; + + remoteIssues.editMilestone(createdMilestoneId, milestone) + .then(({data: createdMilestone}) => { + expect(createdMilestone).to.have.own('number', createdMilestoneId); + expect(createdMilestone).to.have.own('describe', milestone.description); + + done(); + }).catch(done); + }); + it('should delete a milestone', function(done) { + remoteIssues.deleteMilestone(createdMilestoneId) + .then(({status}) => { + expect(status).to.equal(204); + done(); + }).catch(done); }); }); });