diff --git a/.editorconfig b/.editorconfig index a5a6e6eb..ec408cf1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,7 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +max_line_length = 120 [{*.yml,package.json}] indent_size = 2 diff --git a/.travis.yml b/.travis.yml index 3ee3bdd3..ae92414d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: false language: node_js node_js: - '5' diff --git a/lib/GitHub.js b/lib/GitHub.js index 7bc0b63f..25f9aee7 100644 --- a/lib/GitHub.js +++ b/lib/GitHub.js @@ -13,6 +13,7 @@ import Search from './Search'; import RateLimit from './RateLimit'; import Repository from './Repository'; import Organization from './Organization'; +import Team from './Team'; import Markdown from './Markdown'; /** @@ -59,6 +60,15 @@ class GitHub { return new Organization(organization, this.__auth, this.__apiBase); } + /** + * create a new Team wrapper + * @param {string} teamId - the name of the team + * @return {team} + */ + getTeam(teamId) { + return new Team(teamId, this.__auth, this.__apiBase); + } + /** * Create a new Repository wrapper * @param {string} user - the user who owns the respository diff --git a/lib/Organization.js b/lib/Organization.js index ac264186..78354a8c 100644 --- a/lib/Organization.js +++ b/lib/Organization.js @@ -67,6 +67,32 @@ class Organization extends Requestable { listMembers(options, cb) { return this._request('GET', `/orgs/${this.__name}/members`, options, cb); } + + /** + * List the Teams in the Organization + * @see https://developer.github.com/v3/orgs/teams/#list-teams + * @param {Requestable.callback} [cb] - will receive the list of teams + * @return {Promise} - the promise for the http request + */ + getTeams(cb) { + return this._requestAllPages(`/orgs/${this.__name}/teams`, undefined, cb); + } + + /** + * Create a team + * @see https://developer.github.com/v3/orgs/teams/#create-team + * @param {object} options - Team creation parameters + * @param {string} options.name - The name of the team + * @param {string} [options.description] - Team description + * @param {string} [options.repo_names] - Repos to add the team to + * @param {string} [options.privacy=secret] - The level of privacy the team should have. Can be either one + * of: `secret`, or `closed` + * @param {Requestable.callback} [cb] - will receive the created team + * @return {Promise} - the promise for the http request + */ + createTeam(options, cb) { + return this._request('POST', `/orgs/${this.__name}/teams`, options, cb); + } } module.exports = Organization; diff --git a/lib/Requestable.js b/lib/Requestable.js index c7078fe4..8823ba8f 100644 --- a/lib/Requestable.js +++ b/lib/Requestable.js @@ -168,10 +168,11 @@ class Requestable { * @param {string} path - the path to request * @param {Object} data - any query parameters for the request * @param {Requestable.callback} cb - the callback that will receive `true` or `false` + * @param {method} [method=GET] - HTTP Method to use * @return {Promise} - the promise for the http request */ - _request204or404(path, data, cb) { - return this._request('GET', path, data) + _request204or404(path, data, cb, method = 'GET') { + return this._request(method, path, data) .then(function success(response) { if (cb) { cb(null, true, response); diff --git a/lib/Team.js b/lib/Team.js new file mode 100644 index 00000000..fc1d8666 --- /dev/null +++ b/lib/Team.js @@ -0,0 +1,160 @@ +/** + * @file + * @copyright 2016 Matt Smith (Development Seed) + * @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'; +import debug from 'debug'; +const log = debug('github:team'); + +/** + * A Team allows scoping of API requests to a particular Github Organization Team. + */ +class Team extends Requestable { + /** + * Create a Team. + * @param {string} [teamId] - the id for the team + * @param {Requestable.auth} [auth] - information required to authenticate to Github + * @param {string} [apiBase=https://api.github.com] - the base Github API URL + */ + constructor(teamId, auth, apiBase) { + super(auth, apiBase); + this.__teamId = teamId; + } + + /** + * Get Team information + * @see https://developer.github.com/v3/orgs/teams/#get-team + * @param {Requestable.callback} [cb] - will receive the team + * @return {Promise} - the promise for the http request + */ + getTeam(cb) { + log(`Fetching Team ${this.__teamId}`); + return this._request('Get', `/teams/${this.__teamId}`, undefined, cb); + } + + /** + * List the Team's repositories + * @see https://developer.github.com/v3/orgs/teams/#list-team-repos + * @param {Requestable.callback} [cb] - will receive the list of repositories + * @return {Promise} - the promise for the http request + */ + getRepos(cb) { + log(`Fetching repositories for Team ${this.__teamId}`); + return this._requestAllPages(`/teams/${this.__teamId}/repos`, undefined, cb); + } + + /** + * Edit Team information + * @see https://developer.github.com/v3/orgs/teams/#edit-team + * @param {object} options - Parameters for team edit + * @param {string} options.name - The name of the team + * @param {string} [options.description] - Team description + * @param {string} [options.repo_names] - Repos to add the team to + * @param {string} [options.privacy=secret] - The level of privacy the team should have. Can be either one + * of: `secret`, or `closed` + * @param {Requestable.callback} [cb] - will receive the updated team + * @return {Promise} - the promise for the http request + */ + editTeam(options, cb) { + log(`Editing Team ${this.__teamId}`); + return this._request('PATCH', `/teams/${this.__teamId}`, options, cb); + } + + /** + * List the users who are members of the Team + * @see https://developer.github.com/v3/orgs/teams/#list-team-members + * @param {object} options - Parameters for listing team users + * @param {string} [options.role=all] - can be one of: `all`, `maintainer`, or `member` + * @param {Requestable.callback} [cb] - will receive the list of users + * @return {Promise} - the promise for the http request + */ + listMembers(options, cb) { + log(`Getting members of Team ${this.__teamId}`); + return this._requestAllPages(`/teams/${this.__teamId}/members`, options, cb); + } + + /** + * Get Team membership status for a user + * @see https://developer.github.com/v3/orgs/teams/#get-team-membership + * @param {string} username - can be one of: `all`, `maintainer`, or `member` + * @param {Requestable.callback} [cb] - will receive the membership status of a user + * @return {Promise} - the promise for the http request + */ + getMembership(username, cb) { + log(`Getting membership of user ${username} in Team ${this.__teamId}`); + return this._request('GET', `/teams/${this.__teamId}/memberships/${username}`, undefined, cb); + } + + /** + * Add a member to the Team + * @see https://developer.github.com/v3/orgs/teams/#add-team-membership + * @param {string} username - can be one of: `all`, `maintainer`, or `member` + * @param {object} options - Parameters for adding a team member + * @param {string} [options.role=member] - The role that this user should have in the team. Can be one + * of: `member`, or `maintainer` + * @param {Requestable.callback} [cb] - will receive the membership status of added user + * @return {Promise} - the promise for the http request + */ + addMembership(username, options, cb) { + log(`Adding user ${username} to Team ${this.__teamId}`); + return this._request('PUT', `/teams/${this.__teamId}/memberships/${username}`, options, cb); + } + + /** + * Get repo management status for team + * @see https://developer.github.com/v3/orgs/teams/#remove-team-membership + * @param {string} owner - Organization name + * @param {string} repo - Repo name + * @param {Requestable.callback} [cb] - will receive the membership status of added user + * @return {Promise} - the promise for the http request + */ + isManagedRepo(owner, repo, cb) { + log(`Getting repo management by Team ${this.__teamId} for repo ${owner}/${repo}`); + return this._request204or404(`/teams/${this.__teamId}/repos/${owner}/${repo}`, undefined, cb); + } + + /** + * Add or Update repo management status for team + * @see https://developer.github.com/v3/orgs/teams/#add-or-update-team-repository + * @param {string} owner - Organization name + * @param {string} repo - Repo name + * @param {object} options - Parameters for adding or updating repo management for the team + * @param {string} [options.permission] - The permission to grant the team on this repository. Can be one + * of: `pull`, `push`, or `admin` + * @param {Requestable.callback} [cb] - will receive the membership status of added user + * @return {Promise} - the promise for the http request + */ + manageRepo(owner, repo, options, cb) { + log(`Adding or Updating repo management by Team ${this.__teamId} for repo ${owner}/${repo}`); + return this._request204or404(`/teams/${this.__teamId}/repos/${owner}/${repo}`, options, cb, 'PUT'); + } + + /** + * Remove repo management status for team + * @see https://developer.github.com/v3/orgs/teams/#remove-team-repository + * @param {string} owner - Organization name + * @param {string} repo - Repo name + * @param {Requestable.callback} [cb] - will receive the membership status of added user + * @return {Promise} - the promise for the http request + */ + unmanageRepo(owner, repo, cb) { + log(`Remove repo management by Team ${this.__teamId} for repo ${owner}/${repo}`); + return this._request204or404(`/teams/${this.__teamId}/repos/${owner}/${repo}`, undefined, cb, 'DELETE'); + } + + /** + * Delete Team + * @see https://developer.github.com/v3/orgs/teams/#delete-team + * @param {Requestable.callback} [cb] - will receive the list of repositories + * @return {Promise} - the promise for the http request + */ + deleteTeam(cb) { + log(`Deleting Team ${this.__teamId}`); + return this._request204or404(`/teams/${this.__teamId}`, undefined, cb, 'DELETE'); + } +} + +module.exports = Team; diff --git a/package.json b/package.json index a245c817..e7d1fb35 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "codecov": "^1.0.1", "del": "^2.2.0", "eslint-config-google": "^0.5.0", + "eslint-plugin-mocha": "^2.2.0", "gulp": "^3.9.0", "gulp-babel": "^6.1.2", "gulp-eslint": "^2.0.0", diff --git a/test/.eslintrc.yaml b/test/.eslintrc.yaml index 59a40fe9..2582b354 100644 --- a/test/.eslintrc.yaml +++ b/test/.eslintrc.yaml @@ -1,6 +1,9 @@ --- extends: ../.eslintrc.yaml + plugins: + - mocha env: mocha: true rules: handle-callback-err: off + mocha/no-exclusive-tests: 2 diff --git a/test/auth.spec.js b/test/auth.spec.js index 806a844d..2c03d766 100644 --- a/test/auth.spec.js +++ b/test/auth.spec.js @@ -51,7 +51,7 @@ describe('Github', function() { done(); } catch (e) { try { - if (err && err.request.headers['x-ratelimit-remaining'] === '0') { + if (err && err.response.headers['x-ratelimit-remaining'] === '0') { done(); return; } diff --git a/test/organization.spec.js b/test/organization.spec.js index 79464ef9..9d5f75f6 100644 --- a/test/organization.spec.js +++ b/test/organization.spec.js @@ -42,12 +42,11 @@ describe('Organization', function() { }).catch(done); }); - it('should test for membership', function(done) { - organization.isMember(MEMBER_NAME) + it('should test for membership', function() { + return organization.isMember(MEMBER_NAME) .then(function(isMember) { expect(isMember).to.be.true(); - done(); - }).catch(done); + }); }); }); @@ -59,7 +58,7 @@ describe('Organization', function() { organization = github.getOrganization(testUser.ORGANIZATION); }); - it('should create an organisation repo', function(done) { + it('should create an organization repo', function(done) { const options = { name: testRepoName, description: 'test create organization repo', @@ -76,5 +75,31 @@ describe('Organization', function() { done(); })); }); + + // 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, + 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(); + })); + }); }); }); diff --git a/test/team.spec.js b/test/team.spec.js new file mode 100644 index 00000000..9d03c04a --- /dev/null +++ b/test/team.spec.js @@ -0,0 +1,144 @@ +import expect from 'must'; + +import Github from '../lib/GitHub'; +import testUser from './fixtures/user.json'; +import {assertFailure} from './helpers/callbacks'; +import getTestRepoName from './helpers/getTestRepoName'; + +const altUser = { + USERNAME: 'mtscout6-test' +}; + +function createTestTeam() { + const name = getTestRepoName(); + + const github = new Github({ + username: testUser.USERNAME, + password: testUser.PASSWORD, + auth: 'basic' + }); + + const org = github.getOrganization(testUser.ORGANIZATION); + + return org.createTeam({ + name, + privacy: 'closed' + }).then(({data: result}) => { + const team = github.getTeam(result.id); + return {team, name}; + }); +} + +let team; +let name; + +describe('Team', function() { // Isolate tests that are based on a fixed team + before(function() { + const github = new Github({ + username: testUser.USERNAME, + password: testUser.PASSWORD, + auth: 'basic' + }); + + team = github.getTeam(2027812); // github-api-tests/fixed-test-team-1 + }); + + it('should get membership for a given user', function() { + return team.getMembership(altUser.USERNAME) + .then(function({data}) { + expect(data.state).to.equal('active'); + expect(data.role).to.equal('member'); + }); + }); + + it('should list the users in the team', function() { + return team.listMembers() + .then(function({data: members}) { + expect(members).to.be.an.array(); + + let hasTestUser = members.reduce( + (found, member) => member.login === testUser.USERNAME || found, + false); + + expect(hasTestUser).to.be.true(); + }); + }); + + it('should get team repos', function() { + return team.getRepos() + .then(({data}) => { + const hasRepo = data.reduce( + (found, repo) => repo.name === 'fixed-test-repo-1' || found, + false); + + expect(hasRepo).to.be.true(); + }); + }); + + it('should get team', function() { + return team.getTeam() + .then(({data}) => { + expect(data.name).to.equal('Fixed Test Team 1'); + }); + }); + + it('should check if team manages repo', function() { + return team.isManagedRepo(testUser.ORGANIZATION, 'fixed-test-repo-1') + .then((result) => { + expect(result).to.be.true(); + }); + }); +}); + +describe('Team', function() { // Isolate tests that need a new team per test + beforeEach(function() { + return createTestTeam() + .then((x) => { + team = x.team; + name = x.name; + }); + }); + + // Test for Team deletion + afterEach(function(done) { + team.deleteTeam() + .then(() => team.getTeam(assertFailure(done))); + }); + + it('should update team', function() { + const newName = `${name}-updated`; + return team.editTeam({name: newName}) + .then(function({data}) { + expect(data.name).to.equal(newName); + }); + }); + + it('should add membership for a given user', function() { + return team.addMembership(testUser.USERNAME) + .then(({data}) => { + const {state, role} = data; + expect(state === 'active' || state === 'pending').to.be.true(); + expect(role).to.equal('member'); + }); + }); + + it('should add membership as a maintainer for a given user', function() { + return team.addMembership(altUser.USERNAME, {role: 'maintainer'}) + .then(({data}) => { + const {state, role} = data; + expect(state === 'active' || state === 'pending').to.be.true(); + expect(role).to.equal('maintainer'); + }); + }); + + it('should add/remove team management of repo', function() { + return team.manageRepo(testUser.ORGANIZATION, 'fixed-test-repo-1', {permission: 'pull'}) + .then((result) => { + expect(result).to.be.true(); + return team.unmanageRepo(testUser.ORGANIZATION, 'fixed-test-repo-1'); + }) + .then((result) => { + expect(result).to.be.true(); + }); + }); +});