Commit 08709e12 authored by Eric Eastwood's avatar Eric Eastwood
parent 7be6dbf9
# 19.13.0 - *upcoming* # 19.13.0 - *upcoming*
- Add GitLab issue decorations, https://gitlab.com/gitlab-org/gitter/webapp/merge_requests/1077
Developer facing: Developer facing:
- Update to Mocha@5.x for better debugging, `--inspect` (node inspector devtools), https://gitlab.com/gitlab-org/gitter/webapp/merge_requests/1212 - Update to Mocha@5.x for better debugging, `--inspect` (node inspector devtools), https://gitlab.com/gitlab-org/gitter/webapp/merge_requests/1212
......
...@@ -42,17 +42,19 @@ function wrapFunction(cache, moduleName, func, funcName, getInstanceIdFunc) { ...@@ -42,17 +42,19 @@ function wrapFunction(cache, moduleName, func, funcName, getInstanceIdFunc) {
var args = Array.prototype.slice.apply(arguments); var args = Array.prototype.slice.apply(arguments);
var self = this; var self = this;
var instanceId = getInstanceIdFunc ? getInstanceIdFunc(this) : ''; var instanceIdPromise = Promise.resolve(getInstanceIdFunc ? getInstanceIdFunc(this) : '');
var key = generateKey(moduleName, instanceId, funcName, args); return instanceIdPromise.then(function(instanceId) {
var key = generateKey(moduleName, instanceId, funcName, args);
return new Promise(function(resolve, reject) {
cache.lookup(key, function(cb) { return new Promise(function(resolve, reject) {
func.apply(self, args).nodeify(cb); cache.lookup(key, function(cb) {
}, function(err, result) { func.apply(self, args).nodeify(cb);
if (err) return reject(err); }, function(err, result) {
resolve(result); if (err) return reject(err);
}); resolve(result);
});
});
}); });
}; };
......
...@@ -111,6 +111,22 @@ describe('cache-wrapper', function() { ...@@ -111,6 +111,22 @@ describe('cache-wrapper', function() {
wrapped.addPrefix('world'); wrapped.addPrefix('world');
}); });
it('looks up correct key when getInstanceId returns promise', function(done) {
var wrapper = getWrapper(function(key) {
assert.equal(key, 'my-module:hello:addPrefix:world');
done();
});
var Wrapped = wrapper('my-module', Klass, {
getInstanceId: function(obj) {
return Promise.resolve(obj.prefix);
}
});
var wrapped = new Wrapped('hello');
wrapped.addPrefix('world');
});
it('calls method correctly on cache miss', function(done) { it('calls method correctly on cache miss', function(done) {
var wrapper = getWrapper(function(key, cachedFunc, cb) { var wrapper = getWrapper(function(key, cachedFunc, cb) {
cachedFunc(cb); cachedFunc(cb);
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
"gitter-web-text-processor": "file:../text-processor", "gitter-web-text-processor": "file:../text-processor",
"gitter-web-persistence-utils": "file:../persistence-utils", "gitter-web-persistence-utils": "file:../persistence-utils",
"gitter-web-unread-items": "file:../unread-items", "gitter-web-unread-items": "file:../unread-items",
"gitter-markdown-processor": "^11.4.0", "gitter-markdown-processor": "^12.1.0",
"gitter-web-shared": "file:../../shared", "gitter-web-shared": "file:../../shared",
"gitter-web-rooms": "file:../rooms", "gitter-web-rooms": "file:../rooms",
"gitter-web-permissions": "file:../permissions", "gitter-web-permissions": "file:../permissions",
......
"use strict"; "use strict";
var StatusError = require('statuserror');
var wrap = require('./github-cache-wrapper'); var wrap = require('./github-cache-wrapper');
var tentacles = require('./tentacles-client'); var tentacles = require('./tentacles-client');
var userTokenSelector = require('./user-token-selector').full; var userTokenSelector = require('./user-token-selector').full;
function standardizeResponse(response) {
return {
id: response.id,
iid: response.number,
title: response.title,
body: response.body,
state: response.state,
labels: (response.labels || []).map(function(label) {
return label.name;
}),
author: response.user && {
id: response.user.id,
username: response.user.login,
//displayName: n/a,
avatarUrl: response.user.avatar_url
},
assignee: response.assignee && {
id: response.assignee.id,
username: response.assignee.login,
//displayName: n/a,
avatarUrl: response.assignee.avatar_url
},
createdAt: response.created_at,
updatedAt: response.updated_at
};
}
function GitHubIssueService(user) { function GitHubIssueService(user) {
this.user = user; this.user = user;
this.accessToken = userTokenSelector(user); this.accessToken = userTokenSelector(user);
} }
GitHubIssueService.prototype.getIssue = function(repo, issueNumber) { GitHubIssueService.prototype.getIssue = function(repo, issueNumber) {
return tentacles.issue.get(repo, issueNumber, { accessToken: this.accessToken }); return tentacles.issue.get(repo, issueNumber, { accessToken: this.accessToken })
.then((response) => {
if(response) {
return standardizeResponse(response);
}
throw new StatusError(404, `Unable to fetch GitHub issue ${repo}#${issueNumber}`);
});
}; };
......
"use strict"; "use strict";
var wrap = require('./github-cache-wrapper'); var wrap = require('./github-cache-wrapper');
var tentacles = require('./tentacles-client'); var IssueService = require('./github-issue-service');
var userTokenSelector = require('./user-token-selector').full;
function GitHubIssueStateService(user) { function GitHubIssueStateService(user) {
this.user = user; this.issueService = new IssueService(user);
this.accessToken = userTokenSelector(user);
} }
GitHubIssueStateService.prototype.getIssueState = function(repo, issueNumber) { GitHubIssueStateService.prototype.getIssueState = function(repo, issueNumber) {
return tentacles.issue.get(repo, issueNumber, { accessToken: this.accessToken }) return this.issueService.getIssue(repo, issueNumber)
.then(function(issue) { .then(function(issue) {
if (!issue) return '';
return issue.state; return issue.state;
}); });
}; };
......
...@@ -26,7 +26,7 @@ describe('github-issue-service #slow #github', function() { ...@@ -26,7 +26,7 @@ describe('github-issue-service #slow #github', function() {
.then(() => underTest.getIssue(fixtureLoader.GITTER_INTEGRATION_REPO_FULL, 1)) .then(() => underTest.getIssue(fixtureLoader.GITTER_INTEGRATION_REPO_FULL, 1))
.then(function(f) { .then(function(f) {
assert(f); assert(f);
assert.strictEqual(f.number, 1); assert.strictEqual(f.iid, 1);
}) })
.nodeify(done); .nodeify(done);
}); });
...@@ -48,19 +48,25 @@ describe('github-issue-service #slow #github', function() { ...@@ -48,19 +48,25 @@ describe('github-issue-service #slow #github', function() {
.nodeify(done); .nodeify(done);
}*/); }*/);
it('return empty for missing issue', function(done) { it('return error for missing issue', function() {
var repoService = new GitHubRepoService(FAKE_USER); var repoService = new GitHubRepoService(FAKE_USER);
var underTest = new GitHubIssueService(FAKE_USER); var underTest = new GitHubIssueService(FAKE_USER);
repoService.getRepo(fixtureLoader.GITTER_INTEGRATION_REPO_FULL) return repoService.getRepo(fixtureLoader.GITTER_INTEGRATION_REPO_FULL)
.then((repo) => { .then((repo) => {
assert.strictEqual(repo.private, false); assert.strictEqual(repo.private, false);
}) })
.then(() => underTest.getIssue(fixtureLoader.GITTER_INTEGRATION_REPO_FULL, 999999)) .then(() => underTest.getIssue(fixtureLoader.GITTER_INTEGRATION_REPO_FULL, 999999))
.then(function(f) { .then(() => {
assert.strictEqual(f, null); assert.fail('Shouldn\'t be able to fetch issue in unauthorized private project');
}) })
.nodeify(done); .catch((err) => {
if(err instanceof assert.AssertionError) {
throw err;
}
assert.strictEqual(err.status, 404);
});
}); });
it('return the state for an anonymous user', function(done) { it('return the state for an anonymous user', function(done) {
...@@ -74,7 +80,7 @@ describe('github-issue-service #slow #github', function() { ...@@ -74,7 +80,7 @@ describe('github-issue-service #slow #github', function() {
.then(() => underTest.getIssue(fixtureLoader.GITTER_INTEGRATION_REPO_FULL, 1)) .then(() => underTest.getIssue(fixtureLoader.GITTER_INTEGRATION_REPO_FULL, 1))
.then(function(f) { .then(function(f) {
assert(f); assert(f);
assert.strictEqual(f.number, 1); assert.strictEqual(f.iid, 1);
}) })
.nodeify(done); .nodeify(done);
}); });
......
...@@ -13,24 +13,29 @@ describe('github-issue-state-search #slow #github', function() { ...@@ -13,24 +13,29 @@ describe('github-issue-state-search #slow #github', function() {
githubToken: fixtureLoader.GITTER_INTEGRATION_USER_SCOPE_TOKEN githubToken: fixtureLoader.GITTER_INTEGRATION_USER_SCOPE_TOKEN
}; };
it('return the state', function(done) { it('return the state', function() {
var underTest = new GitHubIssueStateService(FAKE_USER); var underTest = new GitHubIssueStateService(FAKE_USER);
underTest.getIssueState(fixtureLoader.GITTER_INTEGRATION_REPO_FULL, 1) return underTest.getIssueState(fixtureLoader.GITTER_INTEGRATION_REPO_FULL, 1)
.then(function(f) { .then(function(f) {
assert.strictEqual(f, 'open'); assert.strictEqual(f, 'open');
}) });
.nodeify(done);
}); });
it('return empty for missing issue', function(done) { it('throw error for missing issue', function() {
var underTest = new GitHubIssueStateService(FAKE_USER); var underTest = new GitHubIssueStateService(FAKE_USER);
underTest.getIssueState(fixtureLoader.GITTER_INTEGRATION_REPO_FULL, 999999) return underTest.getIssueState(fixtureLoader.GITTER_INTEGRATION_REPO_FULL, 999999)
.then(function(f) { .then(() => {
assert.strictEqual(f, ''); assert.fail('Shouldn\'t be able to fetch missing issue');
}) })
.nodeify(done); .catch((err) => {
if(err instanceof assert.AssertionError) {
throw err;
}
assert.strictEqual(err.status, 404);
});
}); });
}); });
'use strict';
var Promise = require('bluebird');
var identityService = require('gitter-web-identity');
module.exports = function(user) {
if(!user) return Promise.resolve();
return identityService.getIdentityForUser(user, identityService.GITLAB_IDENTITY_PROVIDER)
.then(function(glIdentity) {
if(!glIdentity) return null;
return glIdentity.accessToken;
});
};
'use strict';
var env = require('gitter-web-env');
var nconf = env.config;
// A comma-separated list of GitLab private-tokens
var accessTokenPoolString = nconf.get('gitlab:public_token_pool') || '';
var accessTokenPool = accessTokenPoolString.split(',');
var usageIterator = 0;
module.exports = function() {
var accessToken = null;
if(accessTokenPool.length > 0) {
accessToken = accessTokenPool[usageIterator++ % accessTokenPool.length];
}
return accessToken;
};
'use strict';
module.exports = {
GitLabIssuableService: require('./issuable-service'),
GitLabIssuableStateService: require('./issuable-state-service')
};
'use strict';
var { Issues, MergeRequests } = require('gitlab/dist/es5');
var cacheWrapper = require('gitter-web-cache-wrapper');
var avatars = require('gitter-web-avatars');
var getGitlabAccessTokenFromUser = require('./get-gitlab-access-token-from-user');
var getPublicTokenFromPool = require('./get-public-token-from-pool');
function standardizeResponse(response) {
var state = '';
if(response.state === 'opened' || response.state === 'reopened') {
state = 'open';
}
else if(response.state === 'closed') {
state = 'closed';
}
return {
id: response.id,
iid: response.iid,
title: response.title,
body: response.description,
state: state,
labels: response.labels || [],
author: {
id: response.author && response.author.id,
username: response.author && response.author.username,
displayName: response.author && response.author.name,
avatarUrl: response.author && response.author.avatar_url || avatars.getDefault()
},
assignee: (response.assignees && response.assignees.length > 0) && {
id: response.assignees[0].id,
username: response.assignees[0].username,
displayName: response.assignees[0].name,
avatarUrl: response.assignees[0].avatar_url
},
createdAt: response.created_at,
updatedAt: response.updated_at
};
}
function GitLabIssuableService(user, type) {
this.type = type || 'issues';
this.user = user;
this.getAccessTokenPromise = getGitlabAccessTokenFromUser(user);
}
GitLabIssuableService.prototype.getIssue = function(project, iid) {
var type = this.type;
return this.getAccessTokenPromise
.then(function(accessToken) {
const gitlabLibOpts = {
url: 'https://gitlab.com',
oauthToken: accessToken,
token: getPublicTokenFromPool()
};
var resource = new Issues(gitlabLibOpts);
if(type === 'mr') {
resource = new MergeRequests(gitlabLibOpts);
}
return resource.show(project, iid);
})
.then(standardizeResponse);
}
module.exports = cacheWrapper('GitLabIssuableService', GitLabIssuableService, {
getInstanceId: function(gitLabIssuableService) {
return gitLabIssuableService.getAccessTokenPromise;
}
});
"use strict";
var cacheWrapper = require('gitter-web-cache-wrapper');
var IssuableService = require('./issuable-service');
var getGitlabAccessTokenFromUser = require('./get-gitlab-access-token-from-user');
function GitLabIssuableStateService(user, type) {
this.type = type || 'issues';
this.issuableService = new IssuableService(user, this.type);
this.getAccessTokenPromise = getGitlabAccessTokenFromUser(user);
}
GitLabIssuableStateService.prototype.getIssueState = function(project, iid) {
return this.issuableService.getIssue(project, iid)
.then(function(issuable) {
return issuable.state;
});
};
module.exports = cacheWrapper('GitLabIssuableStateService', GitLabIssuableStateService, {
getInstanceId: function(gitLabIssuableStateService) {
return gitLabIssuableStateService.getAccessTokenPromise;
}
});
{
"name": "gitter-web-gitlab",
"version": "1.0.0",
"main": "lib/index.js",
"directories": {
"lib": "lib",
"test": "test"
},
"scripts": {
"test": "mocha test/"
},
"private": true,
"dependencies": {
"bluebird": "^3.2.1",
"debug": "^2.2.0",
"gitlab": "^3.6.0",
"gitter-web-cache-wrapper": "file:../cache-wrapper",
"gitter-web-github": "file:../github",
"gitter-web-identity": "file:../identity"
},
"devDependencies": {
"mocha": "^5.2.0"
}
}
{
"env": {
"commonjs": true,
"node": true,
"mocha": true
},
"plugins": [
"mocha"
],
"rules": {
"mocha/no-exclusive-tests": "error",
"node/no-unpublished-require": "off"
}
}
'use strict';
const Promise = require('bluebird');
const assert = require('assert');
const proxyquireNoCallThru = require('proxyquire').noCallThru();
//const GitLabIssuableService = require('..').GitLabIssuableService;
const fixtureLoader = require('gitter-web-test-utils/lib/test-fixtures');
describe('gitlab-issue-service #slow #gitlab', function() {
fixtureLoader.ensureIntegrationEnvironment(
'GITLAB_USER_USERNAME',
'GITLAB_USER_TOKEN',
'GITLAB_PUBLIC_PROJECT1_URI',
'GITLAB_PRIVATE_PROJECT1_URI',
'GITLAB_UNAUTHORIZED_PRIVATE_PROJECT1_URI'
);
const FAKE_USER = {
username: 'FAKE_USER',
};
let oauthToken = null;
let GitLabIssuableService;
beforeEach(() => {
GitLabIssuableService = proxyquireNoCallThru('../lib/issuable-service', {
'./get-gitlab-access-token-from-user': function() {
return Promise.resolve(oauthToken);
}
});
});
afterEach(() => {
oauthToken = null;
});
describe('as a GitLab user', () => {
beforeEach(() => {
oauthToken = fixtureLoader.GITLAB_USER_TOKEN;
});
it('should fetch public issue', () => {
const glService = new GitLabIssuableService(FAKE_USER, 'issues');
return glService.getIssue(fixtureLoader.GITLAB_PUBLIC_PROJECT1_URI, 1)
.then((issue) => {
assert.strictEqual(issue.iid, 1);
assert.strictEqual(issue.state, 'open');
assert.strictEqual(issue.author.username, fixtureLoader.GITLAB_USER_USERNAME);
});
});
it('shouldn\'t fetch missing issue', () => {
const glService = new GitLabIssuableService(FAKE_USER, 'issues');
return glService.getIssue(fixtureLoader.GITLAB_PUBLIC_PROJECT1_URI, 999999)
.then(() => {
assert.fail('Shouldn\'t be able to fetch missing issue');
})
.catch((err) => {
if(err instanceof assert.AssertionError) {
throw err;
}
assert.strictEqual(err.statusCode, 404);
});
});
it('should fetch confidential issue', () => {
const glService = new GitLabIssuableService(FAKE_USER, 'issues');
return glService.getIssue(fixtureLoader.GITLAB_PUBLIC_PROJECT1_URI, 2)
.then((issue) => {
assert.strictEqual(issue.iid, 2);
assert.strictEqual(issue.state, 'open');
assert.strictEqual(issue.author.username, fixtureLoader.GITLAB_USER_USERNAME);
});
});
it('should fetch private issue', () => {
const glService = new GitLabIssuableService(FAKE_USER, 'issues');
return glService.getIssue(fixtureLoader.GITLAB_PRIVATE_PROJECT1_URI, 1)
.then((issue) => {
assert.strictEqual(issue.iid, 1);
assert.strictEqual(issue.state, 'open');
assert.strictEqual(issue.author.username, fixtureLoader.GITLAB_USER_USERNAME);
});
});
it('shouldn\'t fetch issue in unauthroized private project', () => {
const glService = new GitLabIssuableService(FAKE_USER, 'issues');
return glService.getIssue(fixtureLoader.GITLAB_UNAUTHORIZED_PRIVATE_PROJECT1_URI, 1)
.then(() => {
assert.fail('Shouldn\'t be able to fetch issue in unauthorized private project');
})
.catch((err) => {
if(err instanceof assert.AssertionError) {
throw err;
}
assert.strictEqual(err.statusCode, 404);
});
});
});
describe('using public token pool', () => {
beforeEach(() => {
oauthToken = null;
});
it('should fetch public issue', () => {
const glService = new GitLabIssuableService(FAKE_USER, 'issues');
return glService.getIssue(fixtureLoader.GITLAB_PUBLIC_PROJECT1_URI, 1)
.then((issue) => {
assert.strictEqual(issue.iid, 1);
assert.strictEqual(issue.state, 'open');
assert.strictEqual(issue.author.username, fixtureLoader.GITLAB_USER_USERNAME);
});
});
it('shouldn\'t fetch confidential issue', () => {
const glService = new GitLabIssuableService(FAKE_USER, 'issues');
return glService.getIssue(fixtureLoader.GITLAB_PUBLIC_PROJECT1_URI, 2)
.then(() => {
assert.fail('Shouldn\'t be able to fetch confidential issue');
})
.catch((err) => {
if(err instanceof assert.AssertionError) {
throw err;
}
assert.strictEqual(err.statusCode, 404);
});
});
it('shouldn\'t fetch private issue', () => {
const glService = new GitLabIssuableService(FAKE_USER, 'issues');
return glService.getIssue(fixtureLoader.GITLAB_PRIVATE_PROJECT1_URI, 1)
.then(() => {
assert.fail('Shouldn\'t be able to fetch issue in unauthorized private project');
})
.catch((err) => {
if(err instanceof assert.AssertionError) {
throw err;
}
assert.strictEqual(err.statusCode, 404);
});
});
it('shouldn\'t fetch issue in unauthroized private project', () => {
const glService = new GitLabIssuableService(FAKE_USER, 'issues');
return glService.getIssue(fixtureLoader.GITLAB_UNAUTHORIZED_PRIVATE_PROJECT1_URI, 1)
.then(() => {
assert.fail('Shouldn\'t be able to fetch issue in unauthorized private project');
})
.catch((err) => {
if(err instanceof assert.AssertionError) {
throw err;
}
assert.strictEqual(err.statusCode, 404);
});
});
});
});
...@@ -177,8 +177,8 @@ module.exports = { ...@@ -177,8 +177,8 @@ module.exports = {
findPrimaryIdentityForUser: Promise.method(findPrimaryIdentityForUser), findPrimaryIdentityForUser: Promise.method(findPrimaryIdentityForUser),
removeForUser: Promise.method(removeForUser), removeForUser: Promise.method(removeForUser),
GITHUB_IDENTITY_PROVIDER: 'github',
GITLAB_IDENTITY_PROVIDER: 'gitlab', GITLAB_IDENTITY_PROVIDER: 'gitlab',
GITHUB_IDENTITY_PROVIDER: 'github',
GOOGLE_IDENTITY_PROVIDER: 'google', GOOGLE_IDENTITY_PROVIDER: 'google',
TWITTER_IDENTITY_PROVIDER: 'twitter', TWITTER_IDENTITY_PROVIDER: 'twitter',
LINKEDIN_IDENTITY_PROVIDER: 'linkedin', LINKEDIN_IDENTITY_PROVIDER: 'linkedin',
......
...@@ -35,6 +35,12 @@ var configurationMappings = { ...@@ -35,6 +35,12 @@ var configurationMappings = {
GITHUB_ANON_CLIENT_SECRET: "github:anonymous_app:client_secret", GITHUB_ANON_CLIENT_SECRET: "github:anonymous_app:client_secret",
TWITTER_CONSUMER_KEY: "twitteroauth:consumer_key", TWITTER_CONSUMER_KEY: "twitteroauth:consumer_key",
TWITTER_CONSUMER_SECRET: "twitteroauth:consumer_secret", TWITTER_CONSUMER_SECRET: "twitteroauth:consumer_secret",