From 6ec1d0b5d1a8c52a3707a43a5dc5f0465f6bdf5a Mon Sep 17 00:00:00 2001 From: Mathieu Dutour Date: Wed, 30 Nov 2016 21:41:17 +0000 Subject: [PATCH] chore: fix tests and bump to 3.x (#401) * add getContributorStats * add labels methods * add updateStatus and updateRepository method * add project api and fix tests * improve release script * remove prepublish script * request all pages for cards * add getEmails * remove polyfill * drop support for node < 4 / test on node 4-6 * add coverage * remove clearRepo --- .gitignore | 3 +- .npmignore | 3 +- .travis.yml | 9 +- README.md | 116 +++++++--------- gulpfile.babel.js | 12 +- lib/GitHub.js | 10 ++ lib/Issue.js | 53 ++++++- lib/Markdown.js | 4 +- lib/Organization.js | 23 ++++ lib/Project.js | 236 ++++++++++++++++++++++++++++++++ lib/Repository.js | 114 +++++++++++---- lib/Requestable.js | 51 ++++--- lib/User.js | 10 ++ package.json | 53 ++++--- release.sh | 11 +- test/auth.spec.js | 6 +- test/gist.spec.js | 2 +- test/helpers/callbacks.js | 1 - test/helpers/helperFunctions.js | 43 ++++++ test/helpers/wait.js | 5 + test/issue.spec.js | 102 +++++++++++++- test/markdown.spec.js | 6 +- test/organization.spec.js | 53 +++++-- test/project.spec.js | 171 +++++++++++++++++++++++ test/rate-limit.spec.js | 2 +- test/repository.spec.js | 124 +++++++++++++---- test/search.spec.js | 12 +- test/team.spec.js | 71 ++++++++-- test/user.spec.js | 10 +- 29 files changed, 1079 insertions(+), 237 deletions(-) create mode 100644 lib/Project.js create mode 100644 test/helpers/helperFunctions.js create mode 100644 test/helpers/wait.js create mode 100644 test/project.spec.js diff --git a/.gitignore b/.gitignore index 6d462a19..db3cda2f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,8 @@ docs/ dist/ coverage/ node_modules/ - +.nyc_output/ +/out/ .DS_Store npm-debug.log sauce.json diff --git a/.npmignore b/.npmignore index 15fff57d..fe969272 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,7 @@ docs/ coverage/ node_modules/ - +lib/ +.nyc_output/ .DS_Store sauce.json diff --git a/.travis.yml b/.travis.yml index c00581ca..26f56aa3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,7 @@ node_js: - '6' - '5' - '4' - - '0.12' - + cache: directories: - node_modules @@ -14,10 +13,10 @@ before_script: - npm run lint # - npm run build # will need this when we do sauce testing of compiled files script: - - npm test + - npm run test-coverage # - npm run test-dist # test the compiled files -# after_success: -# - npm run codecov # disabled temporarialy while I work out how to generate accurate coverage of ES2015 code +after_success: + - npm run codecov before_deploy: - npm run build deploy: diff --git a/README.md b/README.md index 17ba8db5..919b5eb3 100644 --- a/README.md +++ b/README.md @@ -2,50 +2,19 @@ [![Downloads per month](https://img.shields.io/npm/dm/github-api.svg?maxAge=2592000)][npm-package] [![Latest version](https://img.shields.io/npm/v/github-api.svg?maxAge=3600)][npm-package] -[![Gitter](https://img.shields.io/gitter/room/michael/github.js.svg?maxAge=2592000)][gitter] -[![Travis](https://img.shields.io/travis/michael/github.svg?maxAge=60)][travis-ci] - +[![Gitter](https://img.shields.io/gitter/room/github-tools/github.js.svg?maxAge=2592000)][gitter] +[![Travis](https://img.shields.io/travis/github-tools/github.svg?maxAge=60)][travis-ci] +[![Codecov](https://img.shields.io/codecov/c/github/github-tools/github.svg?maxAge=2592000)][codecov] -Github.js provides a minimal higher-level wrapper around Github's API. It was concieved in the context of -[Prose][prose], a content editor for GitHub. +Github.js provides a minimal higher-level wrapper around Github's API. -## [Read the docs][docs] - -## Installation -Github.js is available from `npm` or [unpkg][unpkg]. - -```shell -npm install github-api -``` - -```html - - - - - -``` - -## Compatibility -Github.js is tested on Node: -* 6.x -* 5.x -* 4.x -* 0.12 - -## GitHub Tools - -The team behind Github.js has created a whole organization, called [GitHub Tools](https://github.com/github-tools), -dedicated to GitHub and its API. In the near future this repository could be moved under the GitHub Tools organization -as well. In the meantime, we recommend you to take a look at other projects of the organization. - -## Samples +## Usage ```javascript /* Data can be retrieved from the API either using callbacks (as in versions < 1.0) - or using a new promise-based API. For now the promise-based API just returns the - raw HTTP request promise; this might change in the next version. + or using a new promise-based API. The promise-based API returns the raw Axios + request promise. */ import GitHub from 'github-api'; @@ -62,57 +31,66 @@ gist.create({ } }).then(function({data}) { // Promises! - let gistJson = data; - gist.read(function(err, gist, xhr) { - // if no error occurred then err == null - - // gistJson === httpResponse.data - - // xhr === httpResponse - }); + let createdGist = data; + return gist.read(); +}).then(function({data}) { + let retrievedGist = data; + // do interesting things }); ``` ```javascript -import GitHub from 'github-api'; +var GitHub = require('github-api'); // basic auth -const gh = new GitHub({ +var gh = new GitHub({ username: 'FOO', password: 'NotFoo' + /* also acceptable: + token: 'MY_OAUTH_TOKEN' + */ }); -const me = gh.getUser(); +var me = gh.getUser(); // no user specified defaults to the user for whom credentials were provided me.listNotifications(function(err, notifications) { // do some stuff }); -const clayreimann = gh.getUser('clayreimann'); -clayreimann.listStarredRepos() - .then(function({data: reposJson}) { - // do stuff with reposJson - }); +var clayreimann = gh.getUser('clayreimann'); +clayreimann.listStarredRepos(function(err, repos) { + // look at all the starred repos! +}); ``` -```javascript -var GitHub = require('github-api'); +## API Documentation -// token auth -var gh = new GitHub({ - token: 'MY_OAUTH_TOKEN' -}); +[API documentation][docs] is hosted on github pages, and is generated from JSDoc; any contributions +should include updated JSDoc. + +## Installation +Github.js is available from `npm` or [unpkg][unpkg]. -var yahoo = gh.getOrganization('yahoo'); -yahoo.listRepos(function(err, repos) { - // look at all the repos! -}) +```shell +npm install github-api ``` -[codecov]: https://codecov.io/github/michael/github?branch=master +```html + + + + + +``` + +## Compatibility +`Github.js` is tested on Node.js: +* 6.x + +Note: `Github.js` uses Promise, hence it will not work in Node.js < 4 without polyfill. + +[codecov]: https://codecov.io/github/github-tools/github?branch=master [docs]: http://github-tools.github.io/github/ -[gitter]: https://gitter.im/michael/github +[gitter]: https://gitter.im/github-tools/github [npm-package]: https://www.npmjs.com/package/github-api/ [unpkg]: https://unpkg.com/github-api/ -[prose]: http://prose.io -[travis-ci]: https://travis-ci.org/michael/github -[xhr-link]: http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx +[travis-ci]: https://travis-ci.org/github-tools/github diff --git a/gulpfile.babel.js b/gulpfile.babel.js index c3d669f5..9965f7aa 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -13,7 +13,7 @@ import uglify from 'gulp-uglify'; const ALL_SOURCES = [ '*.js', 'lib/*.js', - 'test/*.js' + 'test/*.js', ]; gulp.task('lint', function() { @@ -33,13 +33,13 @@ gulp.task('build', [ 'build:external:min', 'build:bundled:debug', 'build:external:debug', - 'build:components' + 'build:components', ]); const bundledConfig = { debug: true, entries: 'lib/GitHub.js', - standalone: 'GitHub' + standalone: 'GitHub', }; const externalConfig = { debug: true, @@ -50,9 +50,9 @@ const externalConfig = { 'js-base64', 'es6-promise', 'debug', - 'utf8' + 'utf8', ], - bundleExternal: false + bundleExternal: false, }; gulp.task('build:bundled:min', function() { return buildBundle(bundledConfig, '.bundle.min.js', true); @@ -82,7 +82,7 @@ function buildBundle(options, extname, minify) { .pipe(source('GitHub.js')) .pipe(buffer()) .pipe(sourcemaps.init({ - loadMaps: true + loadMaps: true, })); if (minify) { diff --git a/lib/GitHub.js b/lib/GitHub.js index 944fc868..59cb94ff 100644 --- a/lib/GitHub.js +++ b/lib/GitHub.js @@ -15,6 +15,7 @@ import Repository from './Repository'; import Organization from './Organization'; import Team from './Team'; import Markdown from './Markdown'; +import Project from './Project'; /** * GitHub encapsulates the functionality to create various API wrapper objects. @@ -113,6 +114,15 @@ class GitHub { return new Markdown(this.__auth, this.__apiBase); } + /** + * Create a new Project wrapper + * @param {string} id - the id of the project + * @return {Markdown} + */ + getProject(id) { + return new Project(id, this.__auth, this.__apiBase); + } + /** * Computes the full repository name * @param {string} user - the username (or the full name) diff --git a/lib/Issue.js b/lib/Issue.js index 07fa2b1a..c0151b5f 100644 --- a/lib/Issue.js +++ b/lib/Issue.js @@ -150,7 +150,7 @@ class Issue extends Requestable { * 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 + * @param {Requestable.callback} [cb] - will receive the milestone * @return {Promise} - the promise for the http request */ getMilestone(milestone, cb) { @@ -161,7 +161,7 @@ class Issue extends Requestable { * 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 + * @param {Requestable.callback} [cb] - will receive the milestone * @return {Promise} - the promise for the http request */ createMilestone(milestoneData, cb) { @@ -173,7 +173,7 @@ class Issue extends Requestable { * @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 + * @param {Requestable.callback} [cb] - will receive the updated milestone * @return {Promise} - the promise for the http request */ editMilestone(milestone, milestoneData, cb) { @@ -184,7 +184,7 @@ class Issue extends Requestable { * 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 + * @param {Requestable.callback} [cb] - will receive the status * @return {Promise} - the promise for the http request */ deleteMilestone(milestone, cb) { @@ -201,6 +201,51 @@ class Issue extends Requestable { createLabel(labelData, cb) { return this._request('POST', `/repos/${this.__repository}/labels`, labelData, cb); } + + /** + * List the labels for the repository + * @see https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository + * @param {Object} options - filtering options + * @param {Requestable.callback} [cb] - will receive the array of labels + * @return {Promise} - the promise for the http request + */ + listLabels(options, cb) { + return this._request('GET', `/repos/${this.__repository}/labels`, options, cb); + } + + /** + * Get a label + * @see https://developer.github.com/v3/issues/labels/#get-a-single-label + * @param {string} label - the name of the label to fetch + * @param {Requestable.callback} [cb] - will receive the label + * @return {Promise} - the promise for the http request + */ + getLabel(label, cb) { + return this._request('GET', `/repos/${this.__repository}/labels/${label}`, null, cb); + } + + /** + * Edit a label + * @see https://developer.github.com/v3/issues/labels/#update-a-label + * @param {string} label - the name of the label to edit + * @param {Object} labelData - the updates to make to the label + * @param {Requestable.callback} [cb] - will receive the updated label + * @return {Promise} - the promise for the http request + */ + editLabel(label, labelData, cb) { + return this._request('PATCH', `/repos/${this.__repository}/labels/${label}`, labelData, cb); + } + + /** + * Delete a label + * @see https://developer.github.com/v3/issues/labels/#delete-a-label + * @param {string} label - the name of the label to delete + * @param {Requestable.callback} [cb] - will receive the status + * @return {Promise} - the promise for the http request + */ + deleteLabel(label, cb) { + return this._request('DELETE', `/repos/${this.__repository}/labels/${label}`, null, cb); + } } module.exports = Issue; diff --git a/lib/Markdown.js b/lib/Markdown.js index cb84851d..edc346cc 100644 --- a/lib/Markdown.js +++ b/lib/Markdown.js @@ -8,11 +8,11 @@ import Requestable from './Requestable'; /** - * RateLimit allows users to query their rate-limit status + * Renders html from Markdown text */ class Markdown extends Requestable { /** - * construct a RateLimit + * construct a Markdown * @param {Requestable.auth} auth - the credentials to authenticate to GitHub * @param {string} [apiBase] - the base Github API URL * @return {Promise} - the promise for the http request diff --git a/lib/Organization.js b/lib/Organization.js index 78354a8c..0a8177b4 100644 --- a/lib/Organization.js +++ b/lib/Organization.js @@ -93,6 +93,29 @@ class Organization extends Requestable { createTeam(options, cb) { return this._request('POST', `/orgs/${this.__name}/teams`, options, cb); } + + /** + * Get information about all projects + * @see https://developer.github.com/v3/projects/#list-organization-projects + * @param {Requestable.callback} [cb] - will receive the list of projects + * @return {Promise} - the promise for the http request + */ + listProjects(cb) { + return this._requestAllPages(`/orgs/${this.__name}/projects`, {AcceptHeader: 'inertia-preview'}, 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) { + options = options || {}; + options.AcceptHeader = 'inertia-preview'; + return this._request('POST', `/orgs/${this.__name}/projects`, options, cb); + } } module.exports = Organization; diff --git a/lib/Project.js b/lib/Project.js new file mode 100644 index 00000000..ab31a078 --- /dev/null +++ b/lib/Project.js @@ -0,0 +1,236 @@ +/** + * @file + * @copyright 2013 Michael Aufreiter (Development Seed) and 2016 Yahoo Inc. + * @license Licensed under {@link https://spdx.org/licenses/BSD-3-Clause-Clear.html BSD-3-Clause-Clear}. + * Github.js is freely distributable. + */ + +import Requestable from './Requestable'; + +/** + * Project encapsulates the functionality to create, query, and modify cards and columns. + */ +class Project extends Requestable { + /** + * Create a Project. + * @param {string} id - the id of the project + * @param {Requestable.auth} [auth] - information required to authenticate to Github + * @param {string} [apiBase=https://api.github.com] - the base Github API URL + */ + constructor(id, auth, apiBase) { + super(auth, apiBase, 'inertia-preview'); + this.__id = id; + } + + /** + * Get information about a project + * @see https://developer.github.com/v3/projects/#get-a-project + * @param {Requestable.callback} cb - will receive the project information + * @return {Promise} - the promise for the http request + */ + getProject(cb) { + return this._request('GET', `/projects/${this.__id}`, null, cb); + } + + /** + * Edit a project + * @see https://developer.github.com/v3/projects/#update-a-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(options, cb) { + return this._request('PATCH', `/projects/${this.__id}`, options, cb); + } + + /** + * Delete a project + * @see https://developer.github.com/v3/projects/#delete-a-project + * @param {Requestable.callback} cb - will receive true if the operation is successful + * @return {Promise} - the promise for the http request + */ + deleteProject(cb) { + return this._request('DELETE', `/projects/${this.__id}`, null, cb); + } + + /** + * Get information about all columns of a project + * @see https://developer.github.com/v3/projects/columns/#list-project-columns + * @param {Requestable.callback} [cb] - will receive the list of columns + * @return {Promise} - the promise for the http request + */ + listProjectColumns(cb) { + return this._requestAllPages(`/projects/${this.__id}/columns`, null, cb); + } + + /** + * Get information about a column + * @see https://developer.github.com/v3/projects/columns/#get-a-project-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', `/projects/columns/${colId}`, null, cb); + } + + /** + * Create a new column + * @see https://developer.github.com/v3/projects/columns/#create-a-project-column + * @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(options, cb) { + return this._request('POST', `/projects/${this.__id}/columns`, options, cb); + } + + /** + * Edit a column + * @see https://developer.github.com/v3/projects/columns/#update-a-project-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', `/projects/columns/${colId}`, options, cb); + } + + /** + * Delete a column + * @see https://developer.github.com/v3/projects/columns/#delete-a-project-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', `/projects/columns/${colId}`, null, cb); + } + + /** + * Move a column + * @see https://developer.github.com/v3/projects/columns/#move-a-project-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', + `/projects/columns/${colId}/moves`, + {position: position}, + cb + ); + } + + /** + * Get information about all cards of a project + * @see https://developer.github.com/v3/projects/cards/#list-project-cards + * @param {Requestable.callback} [cb] - will receive the list of cards + * @return {Promise} - the promise for the http request + */ + listProjectCards(cb) { + return this.listProjectColumns() + .then(({data}) => { + return Promise.all(data.map((column) => { + return this._requestAllPages(`/projects/columns/${column.id}/cards`, null); + })); + }).then((cardsInColumns) => { + const cards = cardsInColumns.reduce((prev, {data}) => { + prev.push(...data); + return prev; + }, []); + if (cb) { + cb(null, cards); + } + return cards; + }).catch((err) => { + if (cb) { + cb(err); + return; + } + throw err; + }); + } + + /** + * Get information about all cards of a column + * @see https://developer.github.com/v3/projects/cards/#list-project-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 + */ + listColumnCards(colId, cb) { + return this._requestAllPages(`/projects/columns/${colId}/cards`, null, cb); + } + + /** + * Get information about a card + * @see https://developer.github.com/v3/projects/cards/#get-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', `/projects/columns/cards/${cardId}`, null, cb); + } + + /** + * Create a new card + * @see https://developer.github.com/v3/projects/cards/#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', `/projects/columns/${colId}/cards`, options, cb); + } + + /** + * Edit a card + * @see https://developer.github.com/v3/projects/cards/#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', `/projects/columns/cards/${cardId}`, options, cb); + } + + /** + * Delete a card + * @see https://developer.github.com/v3/projects/cards/#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', `/projects/columns/cards/${cardId}`, null, cb); + } + + /** + * Move a card + * @see https://developer.github.com/v3/projects/cards/#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) { + return this._request( + 'POST', + `/projects/columns/cards/${cardId}/moves`, + {position: position, column_id: colId}, // eslint-disable-line camelcase + cb + ); + } +} + +module.exports = Project; diff --git a/lib/Repository.js b/lib/Repository.js index 301e1230..fb200397 100644 --- a/lib/Repository.js +++ b/lib/Repository.js @@ -8,7 +8,7 @@ import Requestable from './Requestable'; import Utf8 from 'utf8'; import { - Base64 + Base64, } from 'js-base64'; import debug from 'debug'; const log = debug('github:repository'); @@ -28,7 +28,7 @@ class Repository extends Requestable { this.__fullname = fullname; this.__currentTree = { branch: null, - sha: null + sha: null, }; } @@ -266,21 +266,21 @@ class Repository extends Requestable { log('contet is a string'); return { content: Utf8.encode(content), - encoding: 'utf-8' + encoding: 'utf-8', }; } else if (typeof Buffer !== 'undefined' && content instanceof Buffer) { log('We appear to be in Node'); return { content: content.toString('base64'), - encoding: 'base64' + encoding: 'base64', }; } else if (typeof Blob !== 'undefined' && content instanceof Blob) { log('We appear to be in the browser'); return { content: Base64.encode(content), - encoding: 'base64' + encoding: 'base64', }; } else { // eslint-disable-line @@ -306,8 +306,8 @@ class Repository extends Requestable { path: path, sha: blobSHA, mode: '100644', - type: 'blob' - }] + type: 'blob', + }], }; return this._request('POST', `/repos/${this.__fullname}/git/trees`, newTree, cb); @@ -324,7 +324,7 @@ class Repository extends Requestable { createTree(tree, baseSHA, cb) { return this._request('POST', `/repos/${this.__fullname}/git/trees`, { tree, - base_tree: baseSHA // eslint-disable-line + base_tree: baseSHA, // eslint-disable-line camelcase }, cb); } @@ -341,7 +341,7 @@ class Repository extends Requestable { let data = { message, tree, - parents: [parent] + parents: [parent], }; return this._request('POST', `/repos/${this.__fullname}/git/commits`, data, cb) @@ -363,11 +363,46 @@ class Repository extends Requestable { updateHead(ref, commitSHA, force, cb) { return this._request('PATCH', `/repos/${this.__fullname}/git/refs/${ref}`, { sha: commitSHA, - force: force + force: force, }, cb); } /** + * Update commit status + * @see https://developer.github.com/v3/repos/statuses/ + * @param {string} commitSHA - the SHA of the commit that should be updated + * @param {object} options - Commit status parameters + * @param {string} options.state - The state of the status. Can be one of: pending, success, error, or failure. + * @param {string} [options.target_url] - The target URL to associate with this status. + * @param {string} [options.description] - A short description of the status. + * @param {string} [options.context] - A string label to differentiate this status among CI systems. + * @param {Requestable.callback} cb - will receive the updated commit back + * @return {Promise} - the promise for the http request + */ + updateStatus(commitSHA, options, cb) { + return this._request('POST', `/repos/${this.__fullname}/statuses/${commitSHA}`, options, cb); + } + + /** + * Update repository information + * @see https://developer.github.com/v3/repos/#edit + * @param {object} options - New parameters that will be set to the repository + * @param {string} options.name - Name of the repository + * @param {string} [options.description] - A short description of the repository + * @param {string} [options.homepage] - A URL with more information about the repository + * @param {boolean} [options.private] - Either true to make the repository private, or false to make it public. + * @param {boolean} [options.has_issues] - Either true to enable issues for this repository, false to disable them. + * @param {boolean} [options.has_wiki] - Either true to enable the wiki for this repository, false to disable it. + * @param {boolean} [options.has_downloads] - Either true to enable downloads, false to disable them. + * @param {string} [options.default_branch] - Updates the default branch for this repository. + * @param {Requestable.callback} cb - will receive the updated repository back + * @return {Promise} - the promise for the http request + */ + updateRepository(options, cb) { + return this._request('PATCH', `/repos/${this.__fullname}`, options, cb); + } + + /** * Get information about the repository * @see https://developer.github.com/v3/repos/#get * @param {Requestable.callback} cb - will receive the information about the repository @@ -384,6 +419,16 @@ class Repository extends Requestable { * @return {Promise} - the promise for the http request */ getContributors(cb) { + return this._request('GET', `/repos/${this.__fullname}/contributors`, null, cb); + } + + /** + * List the contributor stats to the repository + * @see https://developer.github.com/v3/repos/#list-contributors + * @param {Requestable.callback} cb - will receive the list of contributors + * @return {Promise} - the promise for the http request + */ + getContributorStats(cb) { return this._request('GET', `/repos/${this.__fullname}/stats/contributors`, null, cb); } @@ -421,7 +466,7 @@ class Repository extends Requestable { getContents(ref, path, raw, cb) { path = path ? `${encodeURI(path)}` : ''; return this._request('GET', `/repos/${this.__fullname}/contents/${path}`, { - ref + ref, }, cb, raw); } @@ -435,7 +480,7 @@ class Repository extends Requestable { */ getReadme(ref, raw, cb) { return this._request('GET', `/repos/${this.__fullname}/readme`, { - ref + ref, }, cb, raw); } @@ -478,7 +523,7 @@ class Repository extends Requestable { let sha = response.data.object.sha; return this.createRef({ sha, - ref: `refs/heads/${newBranch}` + ref: `refs/heads/${newBranch}`, }, cb); }); } @@ -494,21 +539,6 @@ class Repository extends Requestable { return this._request('POST', `/repos/${this.__fullname}/pulls`, options, cb); } - /** - * Update a pull request - * @deprecated since version 2.4.0 - * @see https://developer.github.com/v3/pulls/#update-a-pull-request - * @param {number|string} number - the number of the pull request to update - * @param {Object} options - the pull request description - * @param {Requestable.callback} [cb] - will receive the pull request information - * @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.'); - - return this.updatePullRequest(number, options, cb); - } - /** * Update a pull request * @see https://developer.github.com/v3/pulls/#update-a-pull-request @@ -633,7 +663,7 @@ class Repository extends Requestable { const deleteCommit = { message: `Delete the file at '${path}'`, sha: response.data.sha, - branch + branch, }; return this._request('DELETE', `/repos/${this.__fullname}/contents/${path}`, deleteCommit, cb); }); @@ -694,7 +724,7 @@ class Repository extends Requestable { message, author: options.author, committer: options.committer, - content: shouldEncode ? Base64.encode(content) : content + content: shouldEncode ? Base64.encode(content) : content, }; return this.getSha(branch, filePath) @@ -803,6 +833,30 @@ 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/projects/#list-repository-projects + * @param {Requestable.callback} [cb] - will receive the list of projects + * @return {Promise} - the promise for the http request + */ + listProjects(cb) { + return this._requestAllPages(`/repos/${this.__fullname}/projects`, {AcceptHeader: 'inertia-preview'}, cb); + } + + /** + * Create a new project + * @see https://developer.github.com/v3/projects/#create-a-repository-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) { + options = options || {}; + options.AcceptHeader = 'inertia-preview'; + return this._request('POST', `/repos/${this.__fullname}/projects`, options, cb); + } + } module.exports = Repository; diff --git a/lib/Requestable.js b/lib/Requestable.js index 95c23bbd..8d39c04f 100644 --- a/lib/Requestable.js +++ b/lib/Requestable.js @@ -8,14 +8,9 @@ import axios from 'axios'; import debug from 'debug'; import {Base64} from 'js-base64'; -import {polyfill} from 'es6-promise'; const log = debug('github:request'); -if (typeof Promise === 'undefined') { - polyfill(); -} - /** * The error structure returned when a network call fails */ @@ -30,7 +25,7 @@ class ResponseError extends Error { super(message); this.path = path; this.request = response.config; - this.response = response; + this.response = (response || {}).response || response; this.status = response.status; } } @@ -51,14 +46,16 @@ class Requestable { * @param {Requestable.auth} [auth] - the credentials to authenticate to Github. If auth is * not provided request will be made unauthenticated * @param {string} [apiBase=https://api.github.com] - the base Github API URL + * @param {string} [AcceptHeader=v3] - the accept header for the requests */ - constructor(auth, apiBase) { + constructor(auth, apiBase, AcceptHeader) { this.__apiBase = apiBase || 'https://api.github.com'; this.__auth = { token: auth.token, username: auth.username, - password: auth.password + password: auth.password, }; + this.__AcceptHeader = AcceptHeader || 'v3'; if (auth.token) { this.__authorizationHeader = 'token ' + auth.token; @@ -88,14 +85,20 @@ 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 {string} AcceptHeader - the accept header for the request * @return {Object} - the headers to use in the request */ - __getRequestHeaders(raw) { + __getRequestHeaders(raw, AcceptHeader) { let headers = { - 'Accept': raw ? 'application/vnd.github.v3.raw+json' : 'application/vnd.github.v3+json', - 'Content-Type': 'application/json;charset=UTF-8' + 'Content-Type': 'application/json;charset=UTF-8', + 'Accept': 'application/vnd.github.' + (AcceptHeader || this.__AcceptHeader), }; + if (raw) { + headers.Accept += '.raw'; + } + headers.Accept += '+json'; + if (this.__authorizationHeader) { headers.Authorization = this.__authorizationHeader; } @@ -152,7 +155,13 @@ class Requestable { */ _request(method, path, data, cb, raw) { const url = this.__getURL(path); - const headers = this.__getRequestHeaders(raw); + + const AcceptHeader = (data || {}).AcceptHeader; + if (AcceptHeader) { + delete data.AcceptHeader; + } + const headers = this.__getRequestHeaders(raw, AcceptHeader); + let queryParams = {}; const shouldUseDataAsParams = data && (typeof data === 'object') && methodHasNoBody(method); @@ -167,7 +176,7 @@ class Requestable { headers: headers, params: queryParams, data: data, - responseType: raw ? 'text' : 'json' + responseType: raw ? 'text' : 'json', }; log(`${config.method} to ${config.url}`); @@ -175,7 +184,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); + } }); } @@ -198,7 +215,7 @@ class Requestable { } return true; }, function failure(response) { - if (response.status === 404) { + if (response.response.status === 404) { if (cb) { cb(null, false, response); } @@ -236,7 +253,7 @@ class Requestable { let message = `cannot figure out how to append ${response.data} to the result set`; throw new ResponseError(message, path, response); } - results.push.apply(results, thisGroup); + results.push(...thisGroup); const nextUrl = getNextPage(response.headers.link); if (nextUrl) { @@ -279,7 +296,7 @@ function callbackErrorOrThrow(cb, path) { return function handler(object) { let error; if (object.hasOwnProperty('config')) { - const {status, statusText, config: {method, url}} = object; + const {response: {status, statusText}, config: {method, url}} = object; let message = (`${status} error making request ${method} ${url}: "${statusText}"`); error = new ResponseError(message, path, object); log(`${message} ${JSON.stringify(object.data)}`); diff --git a/lib/User.js b/lib/User.js index be86c4fc..3f3b4bb6 100644 --- a/lib/User.js +++ b/lib/User.js @@ -132,6 +132,16 @@ class User extends Requestable { return this._requestAllPages(this.__getScopedUrl('starred'), requestOptions, cb); } + /** + * List email addresses for a user + * @see https://developer.github.com/v3/users/emails/#list-email-addresses-for-a-user + * @param {Requestable.callback} [cb] - will receive the list of emails + * @return {Promise} - the promise for the http request + */ + getEmails(cb) { + return this._request('GET', '/user/emails', null, cb); + } + /** * Have the authenticated user follow this user * @see https://developer.github.com/v3/users/followers/#follow-a-user diff --git a/package.json b/package.json index c942525d..f6ff43a1 100644 --- a/package.json +++ b/package.json @@ -8,17 +8,20 @@ "Ændrew Rininsland (http://www.aendrew.com)", "Aurelio De Rosa (http://www.audero.it/)", "Clay Reimann (http://clayreimann.me)", - "Michael Aufreiter (http://substance.io)" + "Michael Aufreiter (http://substance.io)", + "Mathieu Dutour (https://github.com/mathieudutour)" ], "readmeFilename": "README.md", "scripts": { "clean": "gulp clean", "build": "gulp build", "test": "mocha --opts ./mocha.opts test/*.spec.js", + "test-coverage": "NODE_ENV=test nyc mocha --opts ./mocha.opts test/*.spec.js", "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", + "codecov": "nyc report --reporter=text-lcov > coverage.lcov && codecov" }, "babel": { "presets": [ @@ -26,33 +29,38 @@ ], "plugins": [ [ - "transform-es2015-modules-umd", - { - "globals": { - "es6-promise": "Promise" - } - } + "add-module-exports", + "transform-es2015-modules-umd" ] ], "env": { "development": { "sourceMaps": "inline" + }, + "test": { + "plugins": [ + "istanbul" + ] } } }, + "nyc": { + "sourceMap": false, + "instrument": false + }, "files": [ - "dist/*", - "lib/*" + "dist/*" ], "dependencies": { - "axios": "^0.10.0", + "axios": "^0.15.2", "debug": "^2.2.0", - "es6-promise": "^3.0.2", "js-base64": "^2.1.9", "utf8": "^2.1.1" }, "devDependencies": { "babel-core": "^6.7.7", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-istanbul": "3.0.0", "babel-plugin-transform-es2015-modules-umd": "^6.5.0", "babel-preset-es2015": "^6.5.0", "babel-register": "^6.7.2", @@ -60,33 +68,34 @@ "browserify": "^13.0.0", "codecov": "^1.0.1", "del": "^2.2.0", - "eslint-config-google": "^0.5.0", - "eslint-plugin-mocha": "^2.2.0", + "eslint-config-google": "^0.7.0", + "eslint-plugin-mocha": "^4.7.0", "gulp": "^3.9.0", "gulp-babel": "^6.1.2", - "gulp-eslint": "^2.0.0", - "gulp-jscs": "^3.0.2", + "gulp-eslint": "^3.0.1", + "gulp-jscs": "^4.0.0", "gulp-jscs-stylish": "^1.3.0", "gulp-rename": "^1.2.2", - "gulp-sourcemaps": "^1.6.0", - "gulp-uglify": "^1.5.1", + "gulp-sourcemaps": "^2.2.0", + "gulp-uglify": "^2.0.0", "jsdoc": "^3.4.0", "minami": "^1.1.1", - "mocha": "^2.3.4", + "mocha": "^3.1.2", "must": "^0.13.1", - "nock": "^8.0.0", + "nock": "^9.0.2", + "nyc": "9.0.1", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0" }, "repository": { "type": "git", - "url": "git://github.com/michael/github.git" + "url": "git://github.com/github-tools/github.git" }, "keywords": [ "github", "api" ], "bugs": { - "url": "https://github.com/michael/github/issues" + "url": "https://github.com/github-tools/github/issues" } } diff --git a/release.sh b/release.sh index 0b3a3c37..63853dae 100755 --- a/release.sh +++ b/release.sh @@ -1,15 +1,18 @@ #!/bin/bash # This is the automated release script -# make sure all our dependencies are installed so we can publish docs -npm install - # guard against stupid if [ -z "$1" ]; then echo "You must specify a new version level: [patch, minor, major]"; exit 1; fi +# make sure all our dependencies are installed so we can publish docs +npm install + +# try to build to make sure we don't publish something really broken +npm run build + # bump the version echo "npm version $1" npm version $1 @@ -32,5 +35,5 @@ git checkout gh-pages mv out/* docs/ echo $VERSION >> _data/versions.csv git add . -git co -m "adding docs for v$VERSION" +git commit -m "adding docs for v$VERSION" git push diff --git a/test/auth.spec.js b/test/auth.spec.js index 2c03d766..bd0d8205 100644 --- a/test/auth.spec.js +++ b/test/auth.spec.js @@ -13,7 +13,7 @@ describe('Github', function() { github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, - auth: 'basic' + auth: 'basic', }); user = github.getUser(); @@ -70,7 +70,7 @@ describe('Github', function() { github = new Github({ username: testUser.USERNAME, password: 'fake124', - auth: 'basic' + auth: 'basic', }); user = github.getUser(); @@ -83,7 +83,7 @@ describe('Github', function() { it('should fail authentication and return err', function(done) { user.listNotifications(assertFailure(done, function(err) { - expect(err.status).to.be.equal(401, 'Return 401 status for bad auth'); + expect(err.response.status).to.be.equal(401, 'Return 401 status for bad auth'); expect(err.response.data.message).to.equal('Bad credentials'); done(); diff --git a/test/gist.spec.js b/test/gist.spec.js index 0fa476bd..c9c2e393 100644 --- a/test/gist.spec.js +++ b/test/gist.spec.js @@ -15,7 +15,7 @@ describe('Gist', function() { github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, - auth: 'basic' + auth: 'basic', }); }); diff --git a/test/helpers/callbacks.js b/test/helpers/callbacks.js index 112464ac..9c80ad98 100644 --- a/test/helpers/callbacks.js +++ b/test/helpers/callbacks.js @@ -7,7 +7,6 @@ export function assertSuccessful(done, cb) { try { expect(err).not.to.exist(err ? (err.response ? err.response.data : err) : 'No error'); expect(res).to.exist(); - expect(xhr).to.be.an.object(); if (cb) { setTimeout(function delay() { diff --git a/test/helpers/helperFunctions.js b/test/helpers/helperFunctions.js new file mode 100644 index 00000000..c35ed64f --- /dev/null +++ b/test/helpers/helperFunctions.js @@ -0,0 +1,43 @@ +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 github + .getRepo(repo.owner.login, repo.name) + .deleteRepo() + .then((removed) => { + if (removed) { + console.log(repo.full_name, 'deleted'); // eslint-disable-line + } + }); +} + +export function deleteTeam(team, github) { + return github + .getTeam(team.id) + .deleteTeam() + .then((removed) => { + if (removed) { + console.log('team', team.name, 'deleted'); //eslint-disable-line + } + }); +} + +export function deleteProject(project, github) { + return github + .getProject(project.id) + .deleteProject() + .then((removed) => { + if (removed) { + console.log('project', project.name, 'deleted'); //eslint-disable-line + } + }); +} diff --git a/test/helpers/wait.js b/test/helpers/wait.js new file mode 100644 index 00000000..680ea360 --- /dev/null +++ b/test/helpers/wait.js @@ -0,0 +1,5 @@ +export default function(delay = 1000) { + return () => new Promise((resolve) => { + setTimeout(() => resolve(), delay); + }); +} diff --git a/test/issue.spec.js b/test/issue.spec.js index e3d0bb78..5064c556 100644 --- a/test/issue.spec.js +++ b/test/issue.spec.js @@ -2,20 +2,43 @@ import expect from 'must'; import Github from '../lib/GitHub'; import testUser from './fixtures/user.json'; +import wait from './helpers/wait'; import {assertSuccessful} from './helpers/callbacks'; +import getTestRepoName from './helpers/getTestRepoName'; describe('Issue', function() { let github; + const testRepoName = getTestRepoName(); let remoteIssues; before(function() { github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, - auth: 'basic' + auth: 'basic', }); - remoteIssues = github.getIssues(testUser.USERNAME, 'TestRepo'); + return github + .getUser() + .createRepo({name: testRepoName}) + .then(wait(5000)) + .then(function() { + remoteIssues = github.getIssues(testUser.USERNAME, testRepoName); + return remoteIssues.createIssue({ + title: 'Test issue', + body: 'Test issue body', + }); + }) + .then(function() { + return remoteIssues.createMilestone({ + title: 'Default Milestone', + description: 'Test', + }); + }); + }); + + after(function() { + return github.getRepo(testUser.USERNAME, testRepoName).deleteRepo(); }); describe('reading', function() { @@ -69,6 +92,7 @@ describe('Issue', function() { let createdIssueId; let issueCommentId; let createdMilestoneId; + let createdLabel; // 200ms between tests so that Github has a chance to settle beforeEach(function(done) { @@ -78,7 +102,7 @@ describe('Issue', function() { it('should create issue', function(done) { const newIssue = { title: 'New issue', - body: 'New issue body' + body: 'New issue body', }; remoteIssues.createIssue(newIssue, assertSuccessful(done, function(err, issue) { @@ -94,7 +118,7 @@ describe('Issue', function() { it('should edit issue', function(done) { const newProps = { title: 'Edited title', - state: 'closed' + state: 'closed', }; remoteIssues.editIssue(createdIssueId, newProps, assertSuccessful(done, function(err, issue) { @@ -148,7 +172,7 @@ describe('Issue', function() { it('should create a milestone', function(done) { let milestone = { title: 'v42', - description: 'The ultimate version' + description: 'The ultimate version', }; remoteIssues.createMilestone(milestone) @@ -162,7 +186,7 @@ describe('Issue', function() { }); it('should update a milestone', function(done) { let milestone = { - description: 'Version 6 * 7' + description: 'Version 6 * 7', }; expect(createdMilestoneId).to.be.a.number(); @@ -182,5 +206,71 @@ describe('Issue', function() { done(); }).catch(done); }); + + it('should create a label', (done) => { + let label = { + name: 'test', + color: '123456', + }; + + remoteIssues.createLabel(label) + .then(({data: _createdLabel}) => { + expect(_createdLabel).to.have.own('name', label.name); + expect(_createdLabel).to.have.own('color', label.color); + + createdLabel = label.name; + done(); + }).catch(done); + }); + + it('should retrieve a single label', (done) => { + let label = { + name: 'test', + color: '123456', + }; + + remoteIssues.getLabel(label.name) + .then(({data: retrievedLabel}) => { + expect(retrievedLabel).to.have.own('name', label.name); + expect(retrievedLabel).to.have.own('color', label.color); + + done(); + }).catch(done); + }); + + it('should update a label', (done) => { + let label = { + color: '789abc', + }; + + expect(createdLabel).to.be.a.string(); + remoteIssues.editLabel(createdLabel, label) + .then(({data: updatedLabel}) => { + expect(updatedLabel).to.have.own('name', createdLabel); + expect(updatedLabel).to.have.own('color', label.color); + + done(); + }).catch(done); + }); + + it('should list labels', (done) => { + expect(createdLabel).to.be.a.string(); + + remoteIssues.listLabels({}, assertSuccessful(done, function(err, labels) { + expect(labels).to.be.an.array(); + const hasLabel = labels.some((label) => label.name === createdLabel); + expect(hasLabel).to.be.true(); + done(); + })); + }); + + it('should delete a label', (done) => { + expect(createdLabel).to.be.a.string(); + remoteIssues.deleteLabel(createdLabel) + .then(({status}) => { + expect(status).to.equal(204); + done(); + }).catch(done); + }); }); }); diff --git a/test/markdown.spec.js b/test/markdown.spec.js index 22f5d982..eef9fe08 100644 --- a/test/markdown.spec.js +++ b/test/markdown.spec.js @@ -11,7 +11,7 @@ describe('Markdown', function() { github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, - auth: 'basic' + auth: 'basic', }); markdown = github.getMarkdown(); @@ -19,7 +19,7 @@ describe('Markdown', function() { it('should convert markdown to html as plain Markdown', function(done) { const options = { - text: 'Hello world github/linguist#1 **cool**, and #1!' + text: 'Hello world github/linguist#1 **cool**, and #1!', }; markdown.render(options) @@ -33,7 +33,7 @@ describe('Markdown', function() { const options = { text: 'Hello world github/linguist#1 **cool**, and #1!', mode: 'gfm', - context: 'github/gollum' + context: 'github/gollum', }; markdown.render(options) .then(function({data: html}) { diff --git a/test/organization.spec.js b/test/organization.spec.js index 9d5f75f6..26fd3607 100644 --- a/test/organization.spec.js +++ b/test/organization.spec.js @@ -9,14 +9,19 @@ describe('Organization', function() { let github; const ORG_NAME = 'github-tools'; const MEMBER_NAME = 'clayreimann'; + let createdProject; before(function() { github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, - auth: 'basic' + auth: 'basic', }); + return; + }); + after(function() { + return github.getProject(createdProject.id).deleteProject(); }); describe('reading', function() { @@ -35,7 +40,7 @@ describe('Organization', function() { .then(function({data: members}) { expect(members).to.be.an.array(); - let hasClayReimann = members.reduce((found, member) => member.login === MEMBER_NAME || found, false); + const hasClayReimann = members.some((member) => member.login === MEMBER_NAME); expect(hasClayReimann).to.be.true(); done(); @@ -76,28 +81,48 @@ 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 create an organization team', function(done) { + const options = { + name: testRepoName, + description: 'Created by unit tests', + privacy: 'secret', + }; + + organization.createTeam(options, assertSuccessful(done, function(err, team) { + expect(team.name).to.equal(testRepoName); + expect(team.organization.login).to.equal(testUser.ORGANIZATION); // jscs:ignore + done(); + })); + }); + 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); + const hasTeam = data.some((member) => member.slug === testRepoName); expect(hasTeam).to.be.true(); }); }); - it('should create an organization team', function(done) { - const options = { + it('should create a project', function(done) { + organization.createProject({ name: testRepoName, - description: 'Created by unit tests', - privacy: 'secret' - }; + body: 'body', + }, assertSuccessful(done, function(err, project) { + createdProject = project; + expect(project).to.own('name', testRepoName); + expect(project).to.own('body', 'body'); + done(); + })); + }); - organization.createTeam(options, assertSuccessful(done, function(err, team) { - expect(team.name).to.equal(testRepoName); - expect(team.organization.login).to.equal(testUser.ORGANIZATION); // jscs:ignore + it('should list repo projects', function(done) { + organization.listProjects(assertSuccessful(done, function(err, projects) { + expect(projects).to.be.an.array(); + + const hasProject = projects.some((project) => project.name === testRepoName); + + expect(hasProject).to.be.true(); done(); })); }); diff --git a/test/project.spec.js b/test/project.spec.js new file mode 100644 index 00000000..cccf4e81 --- /dev/null +++ b/test/project.spec.js @@ -0,0 +1,171 @@ +import expect from 'must'; + +import Github from '../lib/GitHub'; +import wait from './helpers/wait'; +import testUser from './fixtures/user.json'; +import {assertSuccessful} from './helpers/callbacks'; +import getTestRepoName from './helpers/getTestRepoName'; + +describe('Project', function() { + let github; + const testRepoName = getTestRepoName(); + let project; + let columnId; + let cardId; + + before(function() { + github = new Github({ + username: testUser.USERNAME, + password: testUser.PASSWORD, + auth: 'basic', + }); + + return github + .getUser() + .createRepo({name: testRepoName}) + .then(wait(5000)) + .then(function() { + const remoteRepo = github.getRepo(testUser.USERNAME, testRepoName); + return remoteRepo.createProject({ + name: 'test-project', + body: 'body', + }); + }) + .then(function(_project) { + project = github.getProject(_project.data.id); + }); + }); + + after(function() { + return github.getRepo(testUser.USERNAME, testRepoName).deleteRepo(); + }); + + it('should get repo project', function(done) { + project.getProject(assertSuccessful(done, function(err, project) { + expect(project).to.own('name', 'test-project'); + done(); + })); + }); + + it('should update a project', function(done) { + project.updateProject({ + 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) { + project.createProjectColumn({ + 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) { + project.listProjectColumns(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) { + project.getProjectColumn(columnId, assertSuccessful(done, function(err, project) { + expect(project).to.own('name', 'test-column'); + done(); + })); + }); + + it('should update a repo project column', function(done) { + project.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) { + project.createProjectCard(columnId, { + note: 'test-card', + }, assertSuccessful(done, function(err, card) { + expect(card).to.own('note', 'test-card'); + cardId = card.id; + done(); + })); + }); + + it('should list cards of a project', function(done) { + project.listProjectCards(assertSuccessful(done, function(err, cards) { + expect(cards).to.be.an.array(); + expect(cards.length).to.equal(1); + done(); + })); + }); + + it('should list cards of a column', function(done) { + project.listColumnCards(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) { + project.getProjectCard(cardId, assertSuccessful(done, function(err, card) { + expect(card).to.own('note', 'test-card'); + done(); + })); + }); + + it('should update repo project card', function(done) { + project.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) { + project.moveProjectCard(cardId, 'top', columnId, assertSuccessful(done, function(err, result) { + expect(result).to.be(true); + done(); + })); + }); + + it('should move repo project column', function(done) { + project.moveProjectColumn(columnId, 'first', assertSuccessful(done, function(err, result) { + expect(result).to.be(true); + done(); + })); + }); + + it('should delete repo project card', function(done) { + project.deleteProjectCard(cardId, assertSuccessful(done, function(err, result) { + expect(result).to.be(true); + done(); + })); + }); + + it('should delete repo project column', function(done) { + project.deleteProjectColumn(columnId, assertSuccessful(done, function(err, result) { + expect(result).to.be(true); + done(); + })); + }); + + it('should delete repo project', function(done) { + project.deleteProject(assertSuccessful(done, function(err, result) { + expect(result).to.be(true); + done(); + })); + }); +}); diff --git a/test/rate-limit.spec.js b/test/rate-limit.spec.js index 23e12d14..c3a5b858 100644 --- a/test/rate-limit.spec.js +++ b/test/rate-limit.spec.js @@ -12,7 +12,7 @@ describe('RateLimit', function() { github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, - auth: 'basic' + auth: 'basic', }); rateLimit = github.getRateLimit(); diff --git a/test/repository.spec.js b/test/repository.spec.js index f747878d..ef5101cf 100644 --- a/test/repository.spec.js +++ b/test/repository.spec.js @@ -1,6 +1,7 @@ import expect from 'must'; import Github from '../lib/GitHub'; +import wait from './helpers/wait'; import testUser from './fixtures/user.json'; import loadImage from './fixtures/imageBlob'; import {assertSuccessful, assertFailure} from './helpers/callbacks'; @@ -8,18 +9,18 @@ import getTestRepoName from './helpers/getTestRepoName'; describe('Repository', function() { let github; - let remoteRepo; let user; let imageB64; let imageBlob; const testRepoName = getTestRepoName(); const v10SHA = '20fcff9129005d14cc97b9d59b8a3d37f4fb633b'; - const statusUrl = 'https://api.github.com/repos/michael/github/statuses/20fcff9129005d14cc97b9d59b8a3d37f4fb633b'; + const statusUrl = + 'https://api.github.com/repos/github-tools/github/statuses/20fcff9129005d14cc97b9d59b8a3d37f4fb633b'; before(function(done) { github = new Github({ username: testUser.USERNAME, - password: testUser.PASSWORD + password: testUser.PASSWORD, }); loadImage(function(b64, blob) { @@ -30,13 +31,15 @@ describe('Repository', function() { }); describe('reading', function() { + let remoteRepo; + before(function() { - remoteRepo = github.getRepo('michael', 'github'); + remoteRepo = github.getRepo('github-tools', 'github'); }); it('should get repo details', function(done) { remoteRepo.getDetails(assertSuccessful(done, function(err, repo) { - expect(repo).to.have.own('full_name', 'michael/github'); + expect(repo).to.have.own('full_name', 'github-tools/github'); done(); })); @@ -137,7 +140,7 @@ describe('Repository', function() { path: 'test', author: 'AurelioDeRosa', since, - until + until, }; remoteRepo.listCommits(options, assertSuccessful(done, function(err, commits) { @@ -166,7 +169,7 @@ describe('Repository', function() { it('should fail when null ref is passed', function(done) { remoteRepo.getSingleCommit(null, assertFailure(done, function(err) { - expect(err.status).to.be(404); + expect(err.response.status).to.be(404); done(); })); }); @@ -181,6 +184,23 @@ describe('Repository', function() { const contributor = contributors[0]; + expect(contributor).to.have.own('login'); + expect(contributor).to.have.own('contributions'); + + done(); + })); + }); + + it('should show repo contributor stats', function(done) { + remoteRepo.getContributorStats(assertSuccessful(done, function(err, contributors) { + if (!(contributors instanceof Array)) { + console.log(JSON.stringify(contributors, null, 2)); // eslint-disable-line + } + expect(contributors).to.be.an.array(); + expect(contributors.length).to.be.above(1); + + const contributor = contributors[0]; + expect(contributor).to.have.own('author'); expect(contributor).to.have.own('total'); expect(contributor).to.have.own('weeks'); @@ -224,10 +244,10 @@ describe('Repository', function() { }); it('should get a repo by fullname', function(done) { - const repoByName = github.getRepo('michael/github'); + const repoByName = github.getRepo('github-tools/github'); repoByName.getDetails(assertSuccessful(done, function(err, repo) { - expect(repo).to.have.own('full_name', 'michael/github'); + expect(repo).to.have.own('full_name', 'github-tools/github'); done(); })); @@ -265,6 +285,7 @@ describe('Repository', function() { const releaseBody = 'This is my 49 character long release description.'; let sha; let releaseId; + let remoteRepo; before(function() { user = github.getUser(); @@ -278,7 +299,7 @@ describe('Repository', function() { it('should create repo', function(done) { const repoDef = { - name: testRepoName + name: testRepoName, }; user.createRepo(repoDef, assertSuccessful(done, function(err, repo) { @@ -288,6 +309,22 @@ describe('Repository', function() { })); }); + it('should be able to edit repository information', function(done) { + const options = { + name: testRepoName, + description: 'New short description', + homepage: 'http://example.com', + }; + + remoteRepo.updateRepository(options, assertSuccessful(done, + function(err, repository) { + expect(repository).to.have.own('homepage', options.homepage); + expect(repository).to.have.own('description', options.description); + expect(repository).to.have.own('name', testRepoName); + done(); + })); + }); + it('should show repo collaborators', function(done) { remoteRepo.getCollaborators(assertSuccessful(done, function(err, collaborators) { if (!(collaborators instanceof Array)) { @@ -312,26 +349,27 @@ describe('Repository', function() { it('should write to repo', function(done) { remoteRepo.writeFile('master', fileName, initialText, initialMessage, assertSuccessful(done, function() { - remoteRepo.getContents('master', fileName, 'raw', assertSuccessful(done, function(err, fileText) { + wait()().then(() => remoteRepo.getContents('master', fileName, 'raw', + assertSuccessful(done, function(err, fileText) { expect(fileText).to.be(initialText); done(); - })); + }))); })); }); it('should rename files', function(done) { remoteRepo.writeFile('master', fileName, initialText, initialMessage, assertSuccessful(done, function() { - remoteRepo.move('master', fileName, 'new_name', assertSuccessful(done, function() { - remoteRepo.getContents('master', fileName, 'raw', assertFailure(done, function(err) { - expect(err.status).to.be(404); + wait()().then(() => remoteRepo.move('master', fileName, 'new_name', assertSuccessful(done, function() { + wait()().then(() => remoteRepo.getContents('master', fileName, 'raw', assertFailure(done, function(err) { + expect(err.response.status).to.be(404); remoteRepo.getContents('master', 'new_name', 'raw', assertSuccessful(done, function(err, fileText) { expect(fileText).to.be(initialText); done(); })); - })); - })); + }))); + }))); })); }); @@ -392,12 +430,30 @@ describe('Repository', function() { remoteRepo.getRef('heads/master', assertSuccessful(done, function(err, refSpec) { let newRef = { ref: 'refs/heads/new-test-branch', - sha: refSpec.object.sha + sha: refSpec.object.sha, }; remoteRepo.createRef(newRef, assertSuccessful(done)); })); }); + it('should update commit status', function(done) { + const status = { + state: 'success', + target_url: 'http://example.com', // eslint-disable-line camelcase + description: 'Build was successful!', + }; + remoteRepo.getRef('heads/master', assertSuccessful(done, function(err, refSpec) { + remoteRepo.updateStatus(refSpec.object.sha, status, assertSuccessful(done, + function(err, updated) { + expect(updated).to.have.own('state', status.state); + expect(updated).to.have.own('target_url', status.target_url); + expect(updated).to.have.own('description', status.description); + expect(updated).to.have.own('context', 'default'); + done(); + })); + })); + }); + it('should delete ref on repo', function(done) { remoteRepo.deleteRef('heads/new-test-branch', assertSuccessful(done)); }); @@ -424,7 +480,7 @@ describe('Repository', function() { }); it('should get pull requests on repo', function(done) { - const repo = github.getRepo('michael', 'github'); + const repo = github.getRepo('github-tools', 'github'); repo.getPullRequest(153, assertSuccessful(done, function(err, pr) { expect(pr).to.have.own('title'); @@ -444,7 +500,7 @@ describe('Repository', function() { it('should write author and committer to repo', function(done) { const options = { author: {name: 'Author Name', email: 'author@example.com'}, - committer: {name: 'Committer Name', email: 'committer@example.com'} + committer: {name: 'Committer Name', email: 'committer@example.com'}, }; remoteRepo.writeFile('dev', @@ -482,7 +538,7 @@ describe('Repository', function() { remoteRepo.getRef('heads/master', assertSuccessful(done, function(err, refSpec) { let newRef = { ref: 'refs/heads/testing-14', - sha: refSpec.object.sha + sha: refSpec.object.sha, }; remoteRepo.createRef(newRef, assertSuccessful(done, function() { @@ -503,7 +559,7 @@ describe('Repository', function() { it('should be able to write an image to the repo', function(done) { const opts = { - encode: false + encode: false, }; remoteRepo.writeFile('master', imageFileName, imageB64, initialMessage, opts, assertSuccessful(done, @@ -539,7 +595,7 @@ describe('Repository', function() { it('should fail on broken commit', function(done) { remoteRepo.commit('broken-parent-hash', 'broken-tree-hash', initialMessage, assertFailure(done, function(err) { - expect(err.status).to.be(422); + expect(err.response.status).to.be(422); done(); })); }); @@ -560,7 +616,7 @@ describe('Repository', function() { it('should edit a release', function(done) { const releaseDef = { name: releaseName, - body: releaseBody + body: releaseBody, }; remoteRepo.updateRelease(releaseId, releaseDef, assertSuccessful(done, function(err, release) { @@ -589,9 +645,29 @@ 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'); + 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(); + })); + }); }); describe('deleting', function() { + let remoteRepo; before(function() { remoteRepo = github.getRepo(testUser.USERNAME, testRepoName); }); diff --git a/test/search.spec.js b/test/search.spec.js index 93d852da..21a32935 100644 --- a/test/search.spec.js +++ b/test/search.spec.js @@ -5,14 +5,14 @@ import Github from '../lib/GitHub'; import testUser from './fixtures/user.json'; describe('Search', function() { - this.timeout(20 * 1000); + this.timeout(20 * 1000); // eslint-disable-line no-invalid-this let github; before(function() { github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, - auth: 'basic' + auth: 'basic', }); nock.load('test/fixtures/search.json'); }); @@ -22,7 +22,7 @@ describe('Search', function() { let search = github.search({ q: 'tetris language:assembly', sort: 'stars', - order: 'desc' + order: 'desc', }); return search.forRepositories(options) @@ -35,7 +35,7 @@ describe('Search', function() { it('should search code', function() { let options; let search = github.search({ - q: 'addClass in:file language:js repo:jquery/jquery' + q: 'addClass in:file language:js repo:jquery/jquery', }); return search.forCode(options) @@ -50,7 +50,7 @@ describe('Search', function() { let search = github.search({ q: 'windows pip label:bug language:python state:open ', sort: 'created', - order: 'asc' + order: 'asc', }); return search.forIssues(options) @@ -63,7 +63,7 @@ describe('Search', function() { it('should search users', function() { let options; let search = github.search({ - q: 'tom repos:>42 followers:>1000' + q: 'tom repos:>42 followers:>1000', }); return search.forUsers(options) diff --git a/test/team.spec.js b/test/team.spec.js index 1f712617..8a5ec8be 100644 --- a/test/team.spec.js +++ b/test/team.spec.js @@ -6,7 +6,7 @@ import {assertFailure} from './helpers/callbacks'; import getTestRepoName from './helpers/getTestRepoName'; const altUser = { - USERNAME: 'mtscout6-test' + USERNAME: 'mtscout6-test', }; function createTestTeam() { @@ -15,14 +15,14 @@ function createTestTeam() { const github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, - auth: 'basic' + auth: 'basic', }); const org = github.getOrganization(testUser.ORGANIZATION); return org.createTeam({ name, - privacy: 'closed' + privacy: 'closed', }).then(({data: result}) => { const team = github.getTeam(result.id); return {team, name}; @@ -37,10 +37,59 @@ describe('Team', function() { // Isolate tests that are based on a fixed team const github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, - auth: 'basic' + auth: 'basic', }); - team = github.getTeam(2027812); // github-api-tests/fixed-test-team-1 + 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 = org + .createTeam({ + name: 'fixed-test-repo-1', + repo_names: [testUser.ORGANIZATION + '/fixed-test-repo-1'], // eslint-disable-line camelcase + }) + .then(({data: team}) => team) + .catch(() => { + 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) { + throw new Error('missing fixed-test-repo-1'); + } + return team; + }); + }); + /* eslint-enable no-console */ + + 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() { @@ -56,10 +105,7 @@ describe('Team', function() { // Isolate tests that are based on a fixed team .then(function({data: members}) { expect(members).to.be.an.array(); - let hasTestUser = members.reduce( - (found, member) => member.login === testUser.USERNAME || found, - false - ); + const hasTestUser = members.some((member) => member.login === testUser.USERNAME); expect(hasTestUser).to.be.true(); }); @@ -68,10 +114,7 @@ describe('Team', function() { // Isolate tests that are based on a fixed team it('should get team repos', function() { return team.listRepos() .then(({data}) => { - const hasRepo = data.reduce( - (found, repo) => repo.name === 'fixed-test-repo-1' || found, - false - ); + const hasRepo = data.some((repo) => repo.name === 'fixed-test-repo-1'); expect(hasRepo).to.be.true(); }); @@ -80,7 +123,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..2296781c 100644 --- a/test/user.spec.js +++ b/test/user.spec.js @@ -10,7 +10,7 @@ describe('User', function() { github = new Github({ username: testUser.USERNAME, password: testUser.PASSWORD, - auth: 'basic' + auth: 'basic', }); user = github.getUser(); }); @@ -24,7 +24,7 @@ describe('User', function() { type: 'owner', sort: 'updated', per_page: 90, // eslint-disable-line - page: 10 + page: 10, }; user.listRepos(filterOpts, assertArray(done)); @@ -47,7 +47,7 @@ describe('User', function() { all: true, participating: true, since: '2015-01-01T00:00:00Z', - before: '2015-02-01T00:00:00Z' + before: '2015-02-01T00:00:00Z', }; user.listNotifications(filterOpts, assertArray(done)); @@ -68,4 +68,8 @@ describe('User', function() { it('should unfollow user', function(done) { user.unfollow('ingalls', assertSuccessful(done)); }); + + it('should list the email addresses of the user', function(done) { + user.getEmails(assertSuccessful(done)); + }); });