diff --git a/.npmignore b/.npmignore index 15fff57d..1b903b90 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,6 @@ docs/ coverage/ node_modules/ - +lib/ .DS_Store sauce.json diff --git a/lib/Repository.js b/lib/Repository.js index 301e1230..cad8e089 100644 --- a/lib/Repository.js +++ b/lib/Repository.js @@ -504,7 +504,10 @@ class Repository extends Requestable { * @return {Promise} - the promise for the http request */ updatePullRequst(number, options, cb) { - log('Deprecated: This method contains a typo and it has been deprecated. It will be removed in next major version. Use updatePullRequest() instead.'); + log( + 'Deprecated: This method contains a typo and it has been deprecated.' + + 'It will be removed in next major version. Use updatePullRequest() instead.' + ); return this.updatePullRequest(number, options, cb); } @@ -803,6 +806,215 @@ class Repository extends Requestable { mergePullRequest(number, options, cb) { return this._request('PUT', `/repos/${this.__fullname}/pulls/${number}/merge`, options, cb); } + + /** + * Get information about all projects + * @see https://developer.github.com/v3/repos/projects/#list-projects + * @param {Requestable.callback} [cb] - will receive the list of projects + * @return {Promise} - the promise for the http request + */ + listProjects(cb) { + return this._request('GET', `/repos/${this.__fullname}/projects`, null, cb); + } + + /** + * Get information about a project + * @see https://developer.github.com/v3/repos/projects/#list-a-project + * @param {string} projectNumber - the number of the project + * @param {Requestable.callback} cb - will receive the project information + * @return {Promise} - the promise for the http request + */ + getProject(projectNumber, cb) { + return this._request('GET', `/repos/${this.__fullname}/projects/${projectNumber}`, null, cb); + } + + /** + * Create a new project + * @see https://developer.github.com/v3/repos/projects/#create-a-project + * @param {Object} options - the description of the project + * @param {Requestable.callback} cb - will receive the newly created project + * @return {Promise} - the promise for the http request + */ + createProject(options, cb) { + return this._request('POST', `/repos/${this.__fullname}/projects`, options, cb); + } + + /** + * Edit a project + * @see https://developer.github.com/v3/repos/projects/#update-a-project + * @param {string} projectNumber - the number of the project + * @param {Object} options - the description of the project + * @param {Requestable.callback} cb - will receive the modified project + * @return {Promise} - the promise for the http request + */ + updateProject(projectNumber, options, cb) { + return this._request('PATCH', `/repos/${this.__fullname}/projects/${projectNumber}`, options, cb); + } + + /** + * Delete a project + * @see https://developer.github.com/v3/repos/projects/#delete-a-project + * @param {string} projectNumber - the project to be deleted + * @param {Requestable.callback} cb - will receive true if the operation is successful + * @return {Promise} - the promise for the http request + */ + deleteProject(projectNumber, cb) { + return this._request('DELETE', `/repos/${this.__fullname}/projects/${projectNumber}`, null, cb); + } + + /** + * Get information about all columns of a project + * @see https://developer.github.com/v3/repos/projects/#list-columns + * @param {string} projectNumber - the number of the project + * @param {Requestable.callback} [cb] - will receive the list of columns + * @return {Promise} - the promise for the http request + */ + listProjectColumns(projectNumber, cb) { + return this._request('GET', `/repos/${this.__fullname}/projects/${projectNumber}/columns`, null, cb); + } + + /** + * Get information about a column + * @see https://developer.github.com/v3/repos/projects/#get-a-column + * @param {string} colId - the id of the column + * @param {Requestable.callback} cb - will receive the column information + * @return {Promise} - the promise for the http request + */ + getProjectColumn(colId, cb) { + return this._request('GET', `/repos/${this.__fullname}/projects/columns/${colId}`, null, cb); + } + + /** + * Create a new column + * @see https://developer.github.com/v3/repos/projects/#create-a-column + * @param {string} projectNumber - the project number + * @param {Object} options - the description of the column + * @param {Requestable.callback} cb - will receive the newly created column + * @return {Promise} - the promise for the http request + */ + createProjectColumn(projectNumber, options, cb) { + return this._request('POST', `/repos/${this.__fullname}/projects/${projectNumber}/columns`, options, cb); + } + + /** + * Edit a column + * @see https://developer.github.com/v3/repos/projects/#update-a-column + * @param {string} colId - the column id + * @param {Object} options - the description of the column + * @param {Requestable.callback} cb - will receive the modified column + * @return {Promise} - the promise for the http request + */ + updateProjectColumn(colId, options, cb) { + return this._request('PATCH', `/repos/${this.__fullname}/projects/columns/${colId}`, options, cb); + } + + /** + * Delete a column + * @see https://developer.github.com/v3/repos/projects/#delete-a-column + * @param {string} colId - the column to be deleted + * @param {Requestable.callback} cb - will receive true if the operation is successful + * @return {Promise} - the promise for the http request + */ + deleteProjectColumn(colId, cb) { + return this._request('DELETE', `/repos/${this.__fullname}/projects/columns/${colId}`, null, cb); + } + + /** + * Move a column + * @see https://developer.github.com/v3/repos/projects/#move-a-column + * @param {string} colId - the column to be moved + * @param {string} position - can be one of first, last, or after:, + * where is the id value of a column in the same project. + * @param {Requestable.callback} cb - will receive true if the operation is successful + * @return {Promise} - the promise for the http request + */ + moveProjectColumn(colId, position, cb) { + return this._request( + 'POST', + `/repos/${this.__fullname}/projects/columns/${colId}/moves`, + {position: position}, + cb + ); + } + + /** + * Get information about all cards of a column + * @see https://developer.github.com/v3/repos/projects/#list-projects-cards + * @param {string} colId - the id of the column + * @param {Requestable.callback} [cb] - will receive the list of cards + * @return {Promise} - the promise for the http request + */ + listProjectCards(colId, cb) { + return this._request('GET', `/repos/${this.__fullname}/projects/columns/${colId}/cards`, null, cb); + } + + /** + * Get information about a card + * @see https://developer.github.com/v3/repos/projects/#list-a-project-card + * @param {string} cardId - the id of the card + * @param {Requestable.callback} cb - will receive the card information + * @return {Promise} - the promise for the http request + */ + getProjectCard(cardId, cb) { + return this._request('GET', `/repos/${this.__fullname}/projects/columns/cards/${cardId}`, null, cb); + } + + /** + * Create a new card + * @see https://developer.github.com/v3/repos/projects/#create-a-project-card + * @param {string} colId - the column id + * @param {Object} options - the description of the card + * @param {Requestable.callback} cb - will receive the newly created card + * @return {Promise} - the promise for the http request + */ + createProjectCard(colId, options, cb) { + return this._request('POST', `/repos/${this.__fullname}/projects/columns/${colId}/cards`, options, cb); + } + + /** + * Edit a card + * @see https://developer.github.com/v3/repos/projects/#update-a-project-card + * @param {string} cardId - the card id + * @param {Object} options - the description of the card + * @param {Requestable.callback} cb - will receive the modified card + * @return {Promise} - the promise for the http request + */ + updateProjectCard(cardId, options, cb) { + return this._request('PATCH', `/repos/${this.__fullname}/projects/columns/cards/${cardId}`, options, cb); + } + + /** + * Delete a card + * @see https://developer.github.com/v3/repos/projects/#delete-a-project-card + * @param {string} cardId - the card to be deleted + * @param {Requestable.callback} cb - will receive true if the operation is successful + * @return {Promise} - the promise for the http request + */ + deleteProjectCard(cardId, cb) { + return this._request('DELETE', `/repos/${this.__fullname}/projects/columns/cards/${cardId}`, null, cb); + } + + /** + * Move a card + * @see https://developer.github.com/v3/repos/projects/#move-a-project-card + * @param {string} cardId - the card to be moved + * @param {string} position - can be one of top, bottom, or after:, + * where is the id value of a card in the same project. + * @param {string} colId - the id value of a column in the same project. + * @param {Requestable.callback} cb - will receive true if the operation is successful + * @return {Promise} - the promise for the http request + */ + moveProjectCard(cardId, position, colId, cb) { + /* eslint-disable quote-props*/ + return this._request( + 'POST', + `/repos/${this.__fullname}/projects/columns/cards/${cardId}/moves`, + {'position': position, 'column_id': colId}, + cb + ); + /* eslint-enable */ + } + } module.exports = Repository; diff --git a/lib/Requestable.js b/lib/Requestable.js index 95c23bbd..3d9bc3e0 100644 --- a/lib/Requestable.js +++ b/lib/Requestable.js @@ -88,14 +88,22 @@ class Requestable { * Compute the headers required for an API request. * @private * @param {boolean} raw - if the request should be treated as JSON or as a raw request + * @param {boolean} preview - set true to allow the preview api * @return {Object} - the headers to use in the request */ - __getRequestHeaders(raw) { + __getRequestHeaders(raw, preview) { let headers = { - 'Accept': raw ? 'application/vnd.github.v3.raw+json' : 'application/vnd.github.v3+json', 'Content-Type': 'application/json;charset=UTF-8' }; + if (preview && raw) { + headers.Accept = 'application/vnd.github.inertia-preview.raw+json'; + } else if (preview) { + headers.Accept = 'application/vnd.github.inertia-preview+json'; + } else { + headers.Accept = (raw) ? 'application/vnd.github.v3.raw+json' : 'application/vnd.github.v3+json'; + } + if (this.__authorizationHeader) { headers.Authorization = this.__authorizationHeader; } @@ -152,7 +160,15 @@ class Requestable { */ _request(method, path, data, cb, raw) { const url = this.__getURL(path); - const headers = this.__getRequestHeaders(raw); + + let headers; + + // Enable preview api only for projects calls + if (path.includes('projects')) { + headers = this.__getRequestHeaders(raw, true); + } else { + headers = this.__getRequestHeaders(raw); + } let queryParams = {}; const shouldUseDataAsParams = data && (typeof data === 'object') && methodHasNoBody(method); @@ -175,7 +191,15 @@ class Requestable { if (cb) { requestPromise.then((response) => { - cb(null, response.data || true, response); + if (response.data && Object.keys(response.data).length > 0) { + // When data has results + cb(null, response.data, response); + } else if (config.method !== 'GET' && Object.keys(response.data).length < 1) { + // True when successful submit a request and receive a empty object + cb(null, (response.status < 300), response); + } else { + cb(null, response.data, response); + } }); } diff --git a/package.json b/package.json index c942525d..d112a372 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "test-verbose": "DEBUG=github* npm test", "lint": "gulp lint", "make-docs": "node_modules/.bin/jsdoc -c .jsdoc.json --verbose", - "release": "./release.sh" + "release": "./release.sh", + "prepublish": "npm run build" }, "babel": { "presets": [ @@ -41,8 +42,7 @@ } }, "files": [ - "dist/*", - "lib/*" + "dist/*" ], "dependencies": { "axios": "^0.10.0", diff --git a/test/helpers/clearRepo.js b/test/helpers/clearRepo.js new file mode 100644 index 00000000..566e1ed7 --- /dev/null +++ b/test/helpers/clearRepo.js @@ -0,0 +1,30 @@ +import {getNextPage, deleteRepo} from './helperFunctions.js'; + +module.exports = function(github, cb) { + let user = github.getUser(); + + // Override default function to delete repo on each loop + user._requestAllPages = function(path, options, cb) { + return this._request('GET', path, options) + .then((response) => { + if (response.data instanceof Array) { + let deletions = response.data + .map((repo) => deleteRepo(repo, github)) + + Promise.all(deletions).then(() => { + console.log('all user repos removed'); + const nextUrl = getNextPage(response.headers.link); + if (nextUrl) { + console.log('next page'); + return this._requestAllPages(nextUrl, options, cb); + } + + cb(null); + }); + } + }).catch(cb); + }; + + // Read all repos (so delete with _requestAllPages inner call); + user.listRepos(cb); +} diff --git a/test/helpers/clearTeams.js b/test/helpers/clearTeams.js new file mode 100644 index 00000000..adebbf34 --- /dev/null +++ b/test/helpers/clearTeams.js @@ -0,0 +1,32 @@ +import {getNextPage, deleteTeam} from './helperFunctions.js'; + +module.exports = function(github, organization, cb) { + let org = github.getOrganization(organization); + + // Override default function to delete team on each loop + org._requestAllPages = function(path, options, cb) { + return this._request('GET', path, options) + .then((response) => { + if (response.data instanceof Array) { + let deletions = response.data + .map((team) => { + deleteTeam(team, github) + }); + + Promise.all(deletions).then(() => { + console.log('all org teams removed'); + const nextUrl = getNextPage(response.headers.link); + if (nextUrl) { + console.log('next page'); + return this._requestAllPages(nextUrl, options, cb); + } + + cb(null); + }); + } + }).catch(cb); + }; + + // Read all repos (so delete with _requestAllPages inner call); + org.getTeams(cb); +} diff --git a/test/helpers/helperFunctions.js b/test/helpers/helperFunctions.js new file mode 100644 index 00000000..5b8dbc1c --- /dev/null +++ b/test/helpers/helperFunctions.js @@ -0,0 +1,34 @@ +export function getNextPage(linksHeader = '') { + const links = linksHeader.split(/\s*,\s*/); // splits and strips the urls + return links.reduce(function(nextUrl, link) { + if (link.search(/rel="next"/) !== -1) { + return (link.match(/<(.*)>/) || [])[1]; + } + + return nextUrl; + }, undefined); +} + +export function deleteRepo(repo, github) { + return new Promise((resolve, reject) => { + github + .getRepo(repo.owner.login, repo.name) + .deleteRepo() + .then((removed) => { + if(removed) console.log(repo.full_name, 'deleted'); + resolve(); + }); + }); +} + +export function deleteTeam(team, github) { + return new Promise((resolve, reject) => { + github + .getTeam(team.id) + .deleteTeam() + .then((removed) => { + if(removed) console.log('team', team.name, 'deleted'); + resolve(); + }); + }); +} diff --git a/test/issue.spec.js b/test/issue.spec.js index e3d0bb78..5299bee3 100644 --- a/test/issue.spec.js +++ b/test/issue.spec.js @@ -3,19 +3,36 @@ import expect from 'must'; import Github from '../lib/GitHub'; import testUser from './fixtures/user.json'; import {assertSuccessful} from './helpers/callbacks'; +import getTestRepoName from './helpers/getTestRepoName'; describe('Issue', function() { let github; + const testRepoName = getTestRepoName(); let remoteIssues; - before(function() { + before(function(done) { github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, auth: 'basic' }); - remoteIssues = github.getIssues(testUser.USERNAME, 'TestRepo'); + github + .getUser() + .createRepo({name: testRepoName}) + .then(function() { + remoteIssues = github.getIssues(testUser.USERNAME, testRepoName); + return remoteIssues.createIssue({ + title: 'Test issue', + body: 'Test issue body' + }); + }) + .then(function() { + remoteIssues.createMilestone({ + title: 'Default Milestone', + description: 'Test' + }, done); + }).catch(done); }); describe('reading', function() { diff --git a/test/organization.spec.js b/test/organization.spec.js index 9d5f75f6..a78682b1 100644 --- a/test/organization.spec.js +++ b/test/organization.spec.js @@ -4,19 +4,21 @@ import Github from '../lib/GitHub'; import testUser from './fixtures/user.json'; import {assertSuccessful, assertArray} from './helpers/callbacks'; import getTestRepoName from './helpers/getTestRepoName'; +import clearTeams from './helpers/clearTeams'; describe('Organization', function() { let github; const ORG_NAME = 'github-tools'; const MEMBER_NAME = 'clayreimann'; - before(function() { + before(function(done) { github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, auth: 'basic' }); + clearTeams(github, testUser.ORGANIZATION, done); }); describe('reading', function() { @@ -76,18 +78,6 @@ describe('Organization', function() { })); }); - // TODO: The longer this is in place the slower it will get if we don't cleanup random test teams - it('should list the teams in the organization', function() { - return organization.getTeams() - .then(({data}) => { - const hasTeam = data.reduce( - (found, member) => member.slug === 'fixed-test-team-1' || found, - false); - - expect(hasTeam).to.be.true(); - }); - }); - it('should create an organization team', function(done) { const options = { name: testRepoName, @@ -101,5 +91,16 @@ describe('Organization', function() { done(); })); }); + + it('should list the teams in the organization', function() { + return organization.getTeams() + .then(({data}) => { + const hasTeam = data.reduce( + (found, member) => member.slug === testRepoName || found, + false); + + expect(hasTeam).to.be.true(); + }); + }); }); }); diff --git a/test/repository.spec.js b/test/repository.spec.js index f747878d..6657799b 100644 --- a/test/repository.spec.js +++ b/test/repository.spec.js @@ -265,6 +265,9 @@ describe('Repository', function() { const releaseBody = 'This is my 49 character long release description.'; let sha; let releaseId; + let projectNumber; + let columnId; + let cardId; before(function() { user = github.getUser(); @@ -589,6 +592,147 @@ describe('Repository', function() { it('should delete a release', function(done) { remoteRepo.deleteRelease(releaseId, assertSuccessful(done)); }); + + it('should create a project', function(done) { + remoteRepo.createProject({ + name: 'test-project', + body: 'body' + }, assertSuccessful(done, function(err, project) { + expect(project).to.own('name', 'test-project'); + expect(project).to.own('body', 'body'); + projectNumber = project.number; + done(); + })); + }); + + it('should list repo projects', function(done) { + remoteRepo.listProjects(assertSuccessful(done, function(err, projects) { + expect(projects).to.be.an.array(); + expect(projects.length).to.equal(1); + done(); + })); + }); + + it('should get repo project', function(done) { + remoteRepo.getProject(projectNumber, assertSuccessful(done, function(err, project) { + expect(project).to.own('name', 'test-project'); + done(); + })); + }); + + it('should update a project', function(done) { + remoteRepo.updateProject(projectNumber, { + name: 'another-test-project', + body: 'another-body' + }, assertSuccessful(done, function(err, project) { + expect(project).to.own('name', 'another-test-project'); + expect(project).to.own('body', 'another-body'); + done(); + })); + }); + + it('should create a repo project column', function(done) { + remoteRepo.createProjectColumn(projectNumber, { + name: 'test-column' + }, assertSuccessful(done, function(err, column) { + expect(column).to.own('name', 'test-column'); + columnId = column.id; + done(); + })); + }); + + it('should list repo project columns', function(done) { + remoteRepo.listProjectColumns(projectNumber, assertSuccessful(done, function(err, columns) { + expect(columns).to.be.an.array(); + expect(columns.length).to.equal(1); + done(); + })); + }); + + it('should get repo project column', function(done) { + remoteRepo.getProjectColumn(columnId, assertSuccessful(done, function(err, project) { + expect(project).to.own('name', 'test-column'); + done(); + })); + }); + + it('should update a repo project column', function(done) { + remoteRepo.updateProjectColumn(columnId, { + name: 'another-test-column' + }, assertSuccessful(done, function(err, column) { + expect(column).to.own('name', 'another-test-column'); + done(); + })); + }); + + it('should create repo project card', function(done) { + remoteRepo.createProjectCard(columnId, { + note: 'test-card' + }, assertSuccessful(done, function(err, card) { + expect(card).to.own('note', 'test-card'); + cardId = card.id; + done(); + })); + }); + + it('should list repo project cards', function(done) { + remoteRepo.listProjectCards(columnId, assertSuccessful(done, function(err, cards) { + expect(cards).to.be.an.array(); + expect(cards.length).to.equal(1); + done(); + })); + }); + + it('should get repo project card', function(done) { + remoteRepo.getProjectCard(cardId, assertSuccessful(done, function(err, card) { + expect(card).to.own('note', 'test-card'); + done(); + })); + }); + + it('should update repo project card', function(done) { + remoteRepo.updateProjectCard(cardId, { + note: 'another-test-card' + }, assertSuccessful(done, function(err, card) { + expect(card).to.own('note', 'another-test-card'); + done(); + })); + }); + + it('should move repo project card', function(done) { + remoteRepo.moveProjectCard(cardId, 'top', columnId, assertSuccessful(done, function(err, result) { + expect(result).to.be(true); + done(); + })); + }); + + it('should move repo project column', function(done) { + remoteRepo.moveProjectColumn(columnId, 'first', assertSuccessful(done, function(err, result) { + expect(result).to.be(true); + done(); + })); + }); + + it('should delete repo project card', function(done) { + remoteRepo.deleteProjectCard(cardId, assertSuccessful(done, function(err, result) { + expect(result).to.be(true); + done(); + })); + }); + + it('should delete repo project column', function(done) { + remoteRepo.deleteProjectColumn(columnId, assertSuccessful(done, function(err, result) { + expect(result).to.be(true); + done(); + })); + }); + + it('should delete repo project', function(done) { + remoteRepo.deleteProject(projectNumber, assertSuccessful(done, function(err, result) { + expect(result).to.be(true); + done(); + })); + }); }); describe('deleting', function() { diff --git a/test/team.spec.js b/test/team.spec.js index 1f712617..0a7e725f 100644 --- a/test/team.spec.js +++ b/test/team.spec.js @@ -20,7 +20,8 @@ function createTestTeam() { const org = github.getOrganization(testUser.ORGANIZATION); - return org.createTeam({ + return org + .createTeam({ name, privacy: 'closed' }).then(({data: result}) => { @@ -39,8 +40,59 @@ describe('Team', function() { // Isolate tests that are based on a fixed team password: testUser.PASSWORD, auth: 'basic' }); + const org = github.getOrganization(testUser.ORGANIZATION); + + /* eslint-disable no-console */ + // The code below add a fixed-test-repo-1 + let promiseRepo = new Promise((resolve) => { + org + .createRepo({name: 'fixed-test-repo-1'}) + .then(resolve, () => { + console.log('skiped fixed-test-repo-1 creation'); + resolve(); + }); + }); + + // The code below add a fixed-test-repo-1 + let promiseTeam = new Promise((resolve, reject) => { + org + .createTeam({ + name: 'fixed-test-repo-1', + repo_names: [testUser.ORGANIZATION + '/fixed-test-repo-1'] // eslint-disable-line camelcase + }) + .then(({data: team}) => resolve(team), () => { + console.log('skiped fixed-test-repo-1 creation'); + // Team already exists, fetch the team + return org.getTeams(); + }) + .then(({data: teams}) => { + let team = teams + .filter((team) => team.name === 'fixed-test-repo-1') + .pop(); + if (team) { + resolve(team); + } else { + reject(new Error('missing fixed-test-repo-1')); + } + }); + }); + /* eslint-enable no-console */ - team = github.getTeam(2027812); // github-api-tests/fixed-test-team-1 + return promiseRepo.then(() => { + return promiseTeam + .then((t) => { + team = github.getTeam(t.id); + return team; + }) + .then((team) => { + let setupTeam = [ + team.addMembership(altUser.USERNAME), + team.addMembership(testUser.USERNAME), + team.manageRepo(testUser.ORGANIZATION, 'fixed-test-repo-1') + ]; + return Promise.all(setupTeam); + }); + }); }); it('should get membership for a given user', function() { @@ -80,7 +132,7 @@ describe('Team', function() { // Isolate tests that are based on a fixed team it('should get team', function() { return team.getTeam() .then(({data}) => { - expect(data.name).to.equal('Fixed Test Team 1'); + expect(data.name).to.equal('fixed-test-repo-1'); }); }); diff --git a/test/user.spec.js b/test/user.spec.js index 81181389..4c38337a 100644 --- a/test/user.spec.js +++ b/test/user.spec.js @@ -1,18 +1,21 @@ import Github from '../lib/GitHub'; import testUser from './fixtures/user.json'; import {assertSuccessful, assertArray} from './helpers/callbacks'; +import clearRepo from './helpers/clearRepo'; describe('User', function() { let github; let user; - before(function() { + before(function(done) { github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, auth: 'basic' }); user = github.getUser(); + + clearRepo(github, done); }); it('should get user repos', function(done) {