diff --git a/.travis.yml b/.travis.yml index 3297dfd..1dfdfa7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,17 @@ language: "node_js" node_js: + - "9" + - "8" + - "7" + - "6" - "5" - "4" - - "3" # io.js - - "2" # io.js - - "1" # io.js - - "0.12" - - "0.10" - - "0.8" - - "0.6" before_install: - "npm install make-node@0.3.x -g" + - "npm install -g istanbul" + - "npm install -g coveralls" - "preinstall-compat" script: diff --git a/README.md b/README.md index 67a827d..4d869de 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,8 @@ [![Quality](https://img.shields.io/codeclimate/github/jaredhanson/passport-github.svg?label=quality)](https://codeclimate.com/github/jaredhanson/passport-github) [![Dependencies](https://img.shields.io/david/jaredhanson/passport-github.svg)](https://david-dm.org/jaredhanson/passport-github) - [Passport](http://passportjs.org/) strategy for authenticating with [GitHub](https://github.com/) -using the OAuth 2.0 API. +using the OAuth 3.0 API. This module lets you authenticate using GitHub in your Node.js applications. By plugging into Passport, GitHub authentication can be easily and @@ -15,7 +14,7 @@ unobtrusively integrated into any application or framework that supports [Connect](http://www.senchalabs.org/connect/)-style middleware, including [Express](http://expressjs.com/). -## Install +## Installation ```bash $ npm install passport-github @@ -35,14 +34,11 @@ configure a callback URL which matches the route in your application. #### Configure Strategy The GitHub authentication strategy authenticates users using a GitHub account -and OAuth 2.0 tokens. The client ID and secret obtained when creating an -application are supplied as options when creating the strategy. The strategy -also requires a `verify` callback, which receives the access token and optional -refresh token, as well as `profile` which contains the authenticated user's -GitHub profile. The `verify` callback must call `cb` providing a user to -complete authentication. +and OAuth 3.0 tokens. The strategy requires a `verify` callback, which accepts +these credentials and calls `done` providing a user, as well as `options` +specifying a client ID, client secret, and callback URL. -```js +```javascript var GitHubStrategy = require('passport-github').Strategy; passport.use(new GitHubStrategy({ @@ -50,9 +46,9 @@ passport.use(new GitHubStrategy({ clientSecret: GITHUB_CLIENT_SECRET, callbackURL: "http://127.0.0.1:3000/auth/github/callback" }, - function(accessToken, refreshToken, profile, cb) { + function(accessToken, refreshToken, profile, done) { User.findOrCreate({ githubId: profile.id }, function (err, user) { - return cb(err, user); + return done(err, user); }); } )); @@ -96,6 +92,7 @@ expected to have corresponding test cases. Ensure that the complete test suite passes by executing: ```bash +$ npm install --only=dev $ make test ``` @@ -109,18 +106,13 @@ $ make test-cov $ make view-cov ``` -## Support +[![Build Status](https://secure.travis-ci.org/cfsghost/passport-github.png)](http://travis-ci.org/cfsghost/passport-github) -#### Funding - -This software is provided to you as open source, free of charge. The time and -effort to develop and maintain this project is dedicated by [@jaredhanson](https://github.com/jaredhanson). -If you (or your employer) benefit from this project, please consider a financial -contribution. Your contribution helps continue the efforts that produce this -and other open source software. +## Support -Funds are accepted via [PayPal](https://paypal.me/jaredhanson), [Venmo](https://venmo.com/jaredhanson), -and [other](http://jaredhanson.net/pay) methods. Any amount is appreciated. + - [Jared Hanson](http://github.com/jaredhanson) + - [Fred Chien](http://github.com/cfsghost) + - [Benjamin Spriggs](https://github.com/benjspriggs) ## License diff --git a/lib/strategy.js b/lib/strategy.js index 0f4b07f..1ff1768 100644 --- a/lib/strategy.js +++ b/lib/strategy.js @@ -5,7 +5,6 @@ var OAuth2Strategy = require('passport-oauth2') , InternalOAuthError = require('passport-oauth2').InternalOAuthError , APIError = require('./errors/apierror'); - /** * `Strategy` constructor. * @@ -18,10 +17,12 @@ var OAuth2Strategy = require('passport-oauth2') * credentials are not valid. If an exception occured, `err` should be set. * * Options: + * - `baseURL` your GitHub organization base url (defaults to 'https://github.com') + * - `apiURL` your GitHub organization api url (defaults to 'https://api.github.com', usually constructed with `${baseURL}/api/v3`) * - `clientID` your GitHub application's Client ID * - `clientSecret` your GitHub application's Client Secret * - `callbackURL` URL to which GitHub will redirect the user after granting authorization - * - `scope` array of permission scopes to request. valid scopes include: + * - `scope` array of permission scopes to request. Valid scopes include: * 'user', 'public_repo', 'repo', 'gist', or none. * (see http://developer.github.com/v3/oauth/#scopes for more info) * — `userAgent` All API requests MUST include a valid User Agent string. @@ -32,7 +33,7 @@ var OAuth2Strategy = require('passport-oauth2') * * passport.use(new GitHubStrategy({ * clientID: '123-456-789', - * clientSecret: 'shhh-its-a-secret' + * clientSecret: 'shhh-its-a-secret', * callbackURL: 'https://www.example.net/auth/github/callback', * userAgent: 'myapp.com' * }, @@ -50,8 +51,12 @@ var OAuth2Strategy = require('passport-oauth2') */ function Strategy(options, verify) { options = options || {}; - options.authorizationURL = options.authorizationURL || 'https://github.com/login/oauth/authorize'; - options.tokenURL = options.tokenURL || 'https://github.com/login/oauth/access_token'; + + + options.baseURL = options.baseURL || 'https://github.com'; + options.apiURL = options.apiURL || 'https://api.github.com'; + options.authorizationURL = options.authorizationURL || `${options.baseURL}/login/oauth/authorize`; + options.tokenURL = options.tokenURL || `${options.baseURL}/login/oauth/access_token`; options.scopeSeparator = options.scopeSeparator || ','; options.customHeaders = options.customHeaders || {}; @@ -60,10 +65,11 @@ function Strategy(options, verify) { } OAuth2Strategy.call(this, options, verify); - this.name = 'github'; - this._userProfileURL = options.userProfileURL || 'https://api.github.com/user'; + this.name = options.name || 'github'; + this._userProfileURL = options.userProfileURL || `${options.apiURL}/user`; + this._userEmailURL = options.userEmailURL || `${options.apiURL}/user/emails`; this._oauth2.useAuthorizationHeaderforGET(true); - + // NOTE: GitHub returns an HTTP 200 OK on error responses. As a result, the // underlying `oauth` implementation understandably does not parse the // response as an error. This code swizzles the implementation to @@ -87,7 +93,6 @@ function Strategy(options, verify) { // Inherit from `OAuth2Strategy`. util.inherits(Strategy, OAuth2Strategy); - /** * Retrieve user profile from GitHub. * @@ -108,26 +113,26 @@ Strategy.prototype.userProfile = function(accessToken, done) { var self = this; this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) { var json; - + if (err) { if (err.data) { try { json = JSON.parse(err.data); } catch (_) {} } - + if (json && json.message) { return done(new APIError(json.message)); } return done(new InternalOAuthError('Failed to fetch user profile', err)); } - + try { json = JSON.parse(body); } catch (ex) { return done(new Error('Failed to parse user profile')); } - + var profile = Profile.parse(json); profile.provider = 'github'; profile._raw = body; @@ -141,7 +146,7 @@ Strategy.prototype.userProfile = function(accessToken, done) { // information that was obtained. return done(null, profile); } - + var json; try { json = JSON.parse(body); @@ -150,15 +155,15 @@ Strategy.prototype.userProfile = function(accessToken, done) { // information that was obtained. return done(null, profile); } - - + + if (!json.length) { return done(null, profile); } - + profile.emails = profile.emails || []; var publicEmail = profile.emails[0]; - + (json).forEach(function(email) { if (publicEmail && publicEmail.value == email.email) { profile.emails[0].primary = email.primary; @@ -174,8 +179,7 @@ Strategy.prototype.userProfile = function(accessToken, done) { done(null, profile); } }); -} - +}; // Expose constructor. module.exports = Strategy; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..57a15bf --- /dev/null +++ b/package-lock.json @@ -0,0 +1,336 @@ +{ + "name": "passport-github", + "version": "1.1.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "dev": true, + "requires": { + "assertion-error": "1.1.0", + "check-error": "1.0.2", + "deep-eql": "3.0.1", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.7" + } + }, + "chai-passport-strategy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chai-passport-strategy/-/chai-passport-strategy-1.0.1.tgz", + "integrity": "sha1-2rGASPbqeG7JjLlNS3nAxi/z8gQ=", + "dev": true + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "4.0.7" + } + }, + "diff": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", + "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", + "dev": true, + "requires": { + "is-object": "1.0.1", + "merge-descriptors": "1.0.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "dev": true + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "dev": true + }, + "make-node": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/make-node/-/make-node-0.3.5.tgz", + "integrity": "sha1-LTVN240+zfWg1btMrbuqRGHK3jo=", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.0.tgz", + "integrity": "sha512-ukB2dF+u4aeJjc6IGtPNnJXfeby5d4ZqySlIBT0OEyva/DrMjVm5HkQxKnHDLKEfEQBsEnwTg9HHhtPHJdTd8w==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.3.1", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + } + }, + "module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "passport-oauth2": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", + "integrity": "sha1-9i+BWDy+EmCb585vFguTlaJ7hq0=", + "requires": { + "oauth": "0.9.15", + "passport-strategy": "1.0.0", + "uid2": "0.0.3", + "utils-merge": "1.0.1" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "proxyquire": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-1.8.0.tgz", + "integrity": "sha1-AtUUpb7ZhvBMuyCTrxZ0FTX3ntw=", + "dev": true, + "requires": { + "fill-keys": "1.0.2", + "module-not-found-error": "1.0.1", + "resolve": "1.1.7" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + }, + "type-detect": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.7.tgz", + "integrity": "sha512-4Rh17pAMVdMWzktddFhISRnUnFIStObtUMNGzDwlA6w/77bmGv3aBbRdCmQR6IjzfkTo9otnW+2K/cDRhKSxDA==", + "dev": true + }, + "uid2": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", + "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/package.json b/package.json index 22d748d..2d65c0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "passport-github", - "version": "1.1.0", + "version": "1.1.1", "description": "GitHub authentication strategy for Passport.", "keywords": [ "passport", @@ -15,6 +15,16 @@ "email": "jaredhanson@gmail.com", "url": "http://www.jaredhanson.net/" }, + "contributors": [ + { + "name": "Fred Chien", + "email": "cfsghost@gmail.com" + }, + { + "name": "Benajmin Spriggs", + "email": "ben@sprico.com" + } + ], "repository": { "type": "git", "url": "http://github.com/jaredhanson/passport-github.git" @@ -35,13 +45,13 @@ }, "devDependencies": { "make-node": "0.3.x", - "mocha": "1.x.x", - "chai": "2.x.x", + "mocha": "5.x.x", + "chai": "4.x.x", "chai-passport-strategy": "1.x.x", - "proxyquire": "1.4.x" + "proxyquire": "1.8.x" }, "engines": { - "node": ">= 0.4.0" + "node": ">= 0.8.0" }, "scripts": { "test": "node_modules/.bin/mocha --require test/bootstrap/node test/*.test.js" diff --git a/test/strategy.profile.test.js b/test/strategy.profile.test.js index 43b68af..c091faa 100644 --- a/test/strategy.profile.test.js +++ b/test/strategy.profile.test.js @@ -5,77 +5,90 @@ var GitHubStrategy = require('../lib/strategy'); describe('Strategy#userProfile', function() { - + describe('fetched from default endpoint', function() { var strategy = new GitHubStrategy({ clientID: 'ABC123', clientSecret: 'secret' }, function() {}); - + + // mock strategy._oauth2.get = function(url, accessToken, callback) { - if (url != 'https://api.github.com/user') { return callback(new Error('wrong url argument')); } + var testcases = { + 'https://api.github.com/user': '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "email": "octocat@github.com", "avatar_url": "https://avatars1.githubusercontent.com/u/583231?v=3&s=460", "html_url": "https://github.com/octocat" }', + 'https://api.github.com/user/emails': '[ { "email": "octocat@github.com", "verified": true, "primary": true } ]' + }; + + var body = testcases[url] || null; + if (!body) + return callback(new Error('wrong url argument')); + if (accessToken != 'token') { return callback(new Error('wrong token argument')); } - - var body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "email": "octocat@github.com", "html_url": "https://github.com/octocat" }'; - callback(null, body, undefined); - }; - - - var profile; - - before(function(done) { - strategy.userProfile('token', function(err, p) { - if (err) { return done(err); } - profile = p; - done(); + strategy._oauth2.get = function(url, accessToken, callback) { + if (url != 'https://api.github.com/user') { return callback(new Error('wrong url argument')); } + if (accessToken != 'token') { return callback(new Error('wrong token argument')); } + + var body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "email": "octocat@github.com", "html_url": "https://github.com/octocat" }'; + callback(null, body, undefined); + }; + + + var profile; + + before(function(done) { + strategy.userProfile('token', function(err, p) { + if (err) { return done(err); } + profile = p; + done(); + }); }); - }); - - it('should parse profile', function() { - expect(profile.provider).to.equal('github'); - - expect(profile.id).to.equal('1'); - expect(profile.username).to.equal('octocat'); - expect(profile.displayName).to.equal('monalisa octocat'); - expect(profile.profileUrl).to.equal('https://github.com/octocat'); - expect(profile.emails).to.have.length(1); - expect(profile.emails[0].value).to.equal('octocat@github.com'); - }); - - it('should set raw property', function() { - expect(profile._raw).to.be.a('string'); - }); - - it('should set json property', function() { - expect(profile._json).to.be.an('object'); - }); + + it('should parse profile', function() { + expect(profile.provider).to.equal('github'); + + expect(profile.id).to.equal('1'); + expect(profile.username).to.equal('octocat'); + expect(profile.displayName).to.equal('monalisa octocat'); + expect(profile.profileUrl).to.equal('https://github.com/octocat'); + expect(profile.emails).to.have.length(1); + expect(profile.emails[0].value).to.equal('octocat@github.com'); + }); + + it('should set raw property', function() { + expect(profile._raw).to.be.a('string'); + }); + + it('should set json property', function() { + expect(profile._json).to.be.an('object'); + }); + }; }); // fetched from default endpoint - + describe('fetched from default endpoint and then fetching emails, where user has a publicly visible email address', function() { var strategy = new GitHubStrategy({ clientID: 'ABC123', clientSecret: 'secret', scope: [ 'user:email' ] }, function() {}); - + strategy._oauth2._request = function(method, url, headers, body, accessToken, callback) { var body; switch (url) { - case 'https://api.github.com/user': - body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "email": "octocat@github.com", "html_url": "https://github.com/octocat" }'; - break; - case 'https://api.github.com/user/emails': - body = '[{"email":"octocat@github.com","primary":true,"verified":true}]'; - break; - default: - return callback(new Error('wrong url argument')); + case 'https://api.github.com/user': + body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "email": "octocat@github.com", "html_url": "https://github.com/octocat" }'; + break; + case 'https://api.github.com/user/emails': + body = '[{"email":"octocat@github.com","primary":true,"verified":true}]'; + break; + default: + return callback(new Error('wrong url argument')); } callback(null, body, undefined); }; - - + + var profile; - + before(function(done) { strategy.userProfile('token', function(err, p) { if (err) { return done(err); } @@ -83,10 +96,10 @@ describe('Strategy#userProfile', function() { done(); }); }); - + it('should parse profile', function() { expect(profile.provider).to.equal('github'); - + expect(profile.id).to.equal('1'); expect(profile.username).to.equal('octocat'); expect(profile.displayName).to.equal('monalisa octocat'); @@ -96,41 +109,41 @@ describe('Strategy#userProfile', function() { expect(profile.emails[0].primary).to.equal(true); expect(profile.emails[0].verified).to.equal(true); }); - + it('should set raw property', function() { expect(profile._raw).to.be.a('string'); }); - + it('should set json property', function() { expect(profile._json).to.be.an('object'); }); }); // fetched from default endpoint and then fetching emails, where user has a publicly visible email address - + describe('fetched from default endpoint and then fetching emails, where user does not have a publicly visible email address', function() { var strategy = new GitHubStrategy({ clientID: 'ABC123', clientSecret: 'secret', scope: [ 'user:email' ] }, function() {}); - + strategy._oauth2._request = function(method, url, headers, body, accessToken, callback) { var body; switch (url) { - case 'https://api.github.com/user': - body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "html_url": "https://github.com/octocat" }'; - break; - case 'https://api.github.com/user/emails': - body = '[{"email":"octocat@github.com","primary":true,"verified":true}]'; - break; - default: - return callback(new Error('wrong url argument')); + case 'https://api.github.com/user': + body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "html_url": "https://github.com/octocat" }'; + break; + case 'https://api.github.com/user/emails': + body = '[{"email":"octocat@github.com","primary":true,"verified":true}]'; + break; + default: + return callback(new Error('wrong url argument')); } callback(null, body, undefined); }; - - + + var profile; - + before(function(done) { strategy.userProfile('token', function(err, p) { if (err) { return done(err); } @@ -138,10 +151,10 @@ describe('Strategy#userProfile', function() { done(); }); }); - + it('should parse profile', function() { expect(profile.provider).to.equal('github'); - + expect(profile.id).to.equal('1'); expect(profile.username).to.equal('octocat'); expect(profile.displayName).to.equal('monalisa octocat'); @@ -151,41 +164,41 @@ describe('Strategy#userProfile', function() { expect(profile.emails[0].primary).to.equal(true); expect(profile.emails[0].verified).to.equal(true); }); - + it('should set raw property', function() { expect(profile._raw).to.be.a('string'); }); - + it('should set json property', function() { expect(profile._json).to.be.an('object'); }); }); // fetched from default endpoint and then fetching emails, where user does not have a publicly visible email address - + describe('fetched from default endpoint and then fetching emails, where user does not have any publicly visible or privately available email addresses', function() { var strategy = new GitHubStrategy({ clientID: 'ABC123', clientSecret: 'secret', scope: [ 'user:email' ] }, function() {}); - + strategy._oauth2._request = function(method, url, headers, body, accessToken, callback) { var body; switch (url) { - case 'https://api.github.com/user': - body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "html_url": "https://github.com/octocat" }'; - break; - case 'https://api.github.com/user/emails': - body = '[]'; - break; - default: - return callback(new Error('wrong url argument')); + case 'https://api.github.com/user': + body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "html_url": "https://github.com/octocat" }'; + break; + case 'https://api.github.com/user/emails': + body = '[]'; + break; + default: + return callback(new Error('wrong url argument')); } callback(null, body, undefined); }; - - + + var profile; - + before(function(done) { strategy.userProfile('token', function(err, p) { if (err) { return done(err); } @@ -193,51 +206,51 @@ describe('Strategy#userProfile', function() { done(); }); }); - + it('should parse profile', function() { expect(profile.provider).to.equal('github'); - + expect(profile.id).to.equal('1'); expect(profile.username).to.equal('octocat'); expect(profile.displayName).to.equal('monalisa octocat'); expect(profile.profileUrl).to.equal('https://github.com/octocat'); expect(profile.emails).to.be.undefined; }); - + it('should set raw property', function() { expect(profile._raw).to.be.a('string'); }); - + it('should set json property', function() { expect(profile._json).to.be.an('object'); }); }); // fetched from default endpoint and then fetching emails, where user does not have any publicly visible or privately available email addresses - + describe('fetched from default endpoint and then fetching emails, where user has a publicly visible email address but user:email scope has not been granted', function() { var strategy = new GitHubStrategy({ clientID: 'ABC123', clientSecret: 'secret', scope: [ 'user:email' ] }, function() {}); - + strategy._oauth2._request = function(method, url, headers, body, accessToken, callback) { var body; switch (url) { - case 'https://api.github.com/user': - body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "email": "octocat@github.com", "html_url": "https://github.com/octocat" }'; - break; - case 'https://api.github.com/user/emails': - return callback({ statusCode: 404, - data: '{"message":"Not Found","documentation_url":"https://developer.github.com/v3"}' }); - default: - return callback(new Error('wrong url argument')); + case 'https://api.github.com/user': + body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "email": "octocat@github.com", "html_url": "https://github.com/octocat" }'; + break; + case 'https://api.github.com/user/emails': + return callback({ statusCode: 404, + data: '{"message":"Not Found","documentation_url":"https://developer.github.com/v3"}' }); + default: + return callback(new Error('wrong url argument')); } callback(null, body, undefined); }; - - + + var profile; - + before(function(done) { strategy.userProfile('token', function(err, p) { if (err) { return done(err); } @@ -245,10 +258,10 @@ describe('Strategy#userProfile', function() { done(); }); }); - + it('should parse profile', function() { expect(profile.provider).to.equal('github'); - + expect(profile.id).to.equal('1'); expect(profile.username).to.equal('octocat'); expect(profile.displayName).to.equal('monalisa octocat'); @@ -258,41 +271,41 @@ describe('Strategy#userProfile', function() { expect(profile.emails[0].primary).to.equal(undefined); expect(profile.emails[0].verified).to.equal(undefined); }); - + it('should set raw property', function() { expect(profile._raw).to.be.a('string'); }); - + it('should set json property', function() { expect(profile._json).to.be.an('object'); }); }); // fetched from default endpoint and then fetching emails, where user has a publicly visible email address but user:email scope has not been granted - + describe('fetched from default endpoint and then fetching emails, where user has a publicly visible email address but emails response is malformed', function() { var strategy = new GitHubStrategy({ clientID: 'ABC123', clientSecret: 'secret', scope: [ 'user:email' ] }, function() {}); - + strategy._oauth2._request = function(method, url, headers, body, accessToken, callback) { var body; switch (url) { - case 'https://api.github.com/user': - body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "email": "octocat@github.com", "html_url": "https://github.com/octocat" }'; - break; - case 'https://api.github.com/user/emails': - body = 'Hello, world.'; - break; - default: - return callback(new Error('wrong url argument')); + case 'https://api.github.com/user': + body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "email": "octocat@github.com", "html_url": "https://github.com/octocat" }'; + break; + case 'https://api.github.com/user/emails': + body = 'Hello, world.'; + break; + default: + return callback(new Error('wrong url argument')); } callback(null, body, undefined); }; - - + + var profile; - + before(function(done) { strategy.userProfile('token', function(err, p) { if (err) { return done(err); } @@ -300,10 +313,10 @@ describe('Strategy#userProfile', function() { done(); }); }); - + it('should parse profile', function() { expect(profile.provider).to.equal('github'); - + expect(profile.id).to.equal('1'); expect(profile.username).to.equal('octocat'); expect(profile.displayName).to.equal('monalisa octocat'); @@ -313,34 +326,34 @@ describe('Strategy#userProfile', function() { expect(profile.emails[0].primary).to.equal(undefined); expect(profile.emails[0].verified).to.equal(undefined); }); - + it('should set raw property', function() { expect(profile._raw).to.be.a('string'); }); - + it('should set json property', function() { expect(profile._json).to.be.an('object'); }); }); // fetched from default endpoint and then fetching emails, where user has a publicly visible email address but emails response is malformed - + describe('fetched from custom endpoint', function() { var strategy = new GitHubStrategy({ clientID: 'ABC123', clientSecret: 'secret', userProfileURL: 'https://github.corpDomain/api/v3/user' }, function() {}); - + strategy._oauth2.get = function(url, accessToken, callback) { if (url != 'https://github.corpDomain/api/v3/user') { return callback(new Error('wrong url argument')); } if (accessToken != 'token') { return callback(new Error('wrong token argument')); } - - var body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "email": "octocat@github.com", "html_url": "https://github.com/octocat" }'; + + var body = '{ "login": "octocat", "id": 1, "name": "monalisa octocat", "email": "octocat@github.com", "html_url": "https://github.com/octocat", "avatar_url": "https://github.com/images/error/octocat_happy.gif" }'; callback(null, body, undefined); }; - - + + var profile; - + before(function(done) { strategy.userProfile('token', function(err, p) { if (err) { return done(err); } @@ -348,38 +361,40 @@ describe('Strategy#userProfile', function() { done(); }); }); - + it('should parse profile', function() { expect(profile.provider).to.equal('github'); - + expect(profile.id).to.equal('1'); expect(profile.username).to.equal('octocat'); expect(profile.displayName).to.equal('monalisa octocat'); expect(profile.profileUrl).to.equal('https://github.com/octocat'); expect(profile.emails).to.have.length(1); expect(profile.emails[0].value).to.equal('octocat@github.com'); + expect(profile.photos).to.have.length(1); + expect(profile.photos[0].value).to.equal('https://github.com/images/error/octocat_happy.gif'); }); - + it('should set raw property', function() { expect(profile._raw).to.be.a('string'); }); - + it('should set json property', function() { expect(profile._json).to.be.an('object'); }); }); - + describe('error caused by invalid token', function() { var strategy = new GitHubStrategy({ - clientID: 'ABC123', - clientSecret: 'secret' - }, function() {}); - + clientID: 'ABC123', + clientSecret: 'secret' + }, function() {}); + strategy._oauth2.get = function(url, accessToken, callback) { var body = '{"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"}'; callback({ statusCode: 400, data: body }); }; - + var err, profile; before(function(done) { strategy.userProfile('token', function(e, p) { @@ -388,25 +403,25 @@ describe('Strategy#userProfile', function() { done(); }); }); - + it('should error', function() { expect(err).to.be.an.instanceOf(Error); expect(err.constructor.name).to.equal('APIError'); expect(err.message).to.equal('Bad credentials'); }); }); // error caused by invalid token - + describe('error caused by malformed response', function() { var strategy = new GitHubStrategy({ - clientID: 'ABC123', - clientSecret: 'secret' - }, function() {}); - + clientID: 'ABC123', + clientSecret: 'secret' + }, function() {}); + strategy._oauth2.get = function(url, accessToken, callback) { var body = 'Hello, world.'; callback(null, body, undefined); }; - + var err, profile; before(function(done) { strategy.userProfile('token', function(e, p) { @@ -415,26 +430,26 @@ describe('Strategy#userProfile', function() { done(); }); }); - + it('should error', function() { expect(err).to.be.an.instanceOf(Error); expect(err.message).to.equal('Failed to parse user profile'); }); }); // error caused by malformed response - + describe('internal error', function() { var strategy = new GitHubStrategy({ clientID: 'ABC123', clientSecret: 'secret' }, function() {}); - + strategy._oauth2.get = function(url, accessToken, callback) { return callback(new Error('something went wrong')); } - - + + var err, profile; - + before(function(done) { strategy.userProfile('wrong-token', function(e, p) { err = e; @@ -442,7 +457,7 @@ describe('Strategy#userProfile', function() { done(); }); }); - + it('should error', function() { expect(err).to.be.an.instanceOf(Error); expect(err.constructor.name).to.equal('InternalOAuthError'); @@ -450,10 +465,10 @@ describe('Strategy#userProfile', function() { expect(err.oauthError).to.be.an.instanceOf(Error); expect(err.oauthError.message).to.equal('something went wrong'); }); - + it('should not load profile', function() { expect(profile).to.be.undefined; }); }); // internal error - + });